Plan step 36, step 0 of the inputs stream: mirrors ActionStylesheetsTest's
five checks over the seventeen inputs components already rewritten, plus
pagination (its four views live under resources/views/pagination/**, not
components/, so it gets its own small versions of the two view-reading
checks). The "no class list" check also recognises the wrapper fields'
`:class="$attributes->get('class')"` (Blade's own prop-forwarding syntax,
the only way a component with no root element of its own hands the caller's
class to its field) and a `'class' => '…'` attribute-bag entry, and exempts
field.css's week-long autofill transition from the motion-token check.
The new test caught three already-rewritten components writing a bare
`md-visually-hidden` class, which a package view no longer does (only the
three interaction classes, or the caller's own class, reach a view): field's
character-count live region, search's result-count live region, and chip's
native checkbox under a bound filter chip. Fixed by giving each its own
data-md-* hook with the same visually-hidden declarations in its stylesheet,
and chip.css was also missing its `tooltip.css` import for the tooltip
chip.blade.php renders.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
480 lines
20 KiB
CSS
480 lines
20 KiB
CSS
/*
|
|
* M3's text fields — the chrome every form control here shares: outlined (the default) and filled.
|
|
*
|
|
* The states are selectors rather than classes: whether the label rests in the field or floats is a
|
|
* question about the control's value and focus, and `:has()` asks it without JavaScript, so it is
|
|
* right before Alpine starts, in a test, and after a Livewire morph rewrites the value underneath.
|
|
*
|
|
* The outlined field's notch is MUI's technique: a `<fieldset>` draws the outline and a hidden
|
|
* `<legend>`, carrying the label at its floated size, cuts the gap the label sits in, so the gap is
|
|
* right on any background. The filled field has no outline: a surface-container-highest box with
|
|
* extra-small top corners and an active indicator line, the label floating inside it
|
|
* (FilledTextFieldTokens / OutlinedTextFieldTokens, androidx Compose Material 3, Apache-2.0): 56px
|
|
* tall, 16px padding, 24px icons 16px from the text, the label in body-small once it floats.
|
|
* The `sm` (40px) and `xs` (32px) sizes are this package's, for an unlabelled field in a toolbar.
|
|
* Disabled is on-surface at M3's 38% content and 12% container opacities (tokens/state.css); the
|
|
* filled field's disabled container is on-surface at 4%, FilledTextFieldTokens'
|
|
* DisabledContainerOpacity, which has no system token.
|
|
*
|
|
* Anatomy (resources/views/components/field.blade.php):
|
|
*
|
|
* [data-md-field] the whole thing; `class` from the call site lands here;
|
|
* data-md-variant, data-md-size, data-md-invalid, data-md-floated,
|
|
* data-md-mono, data-md-full, data-md-readonly
|
|
* [data-md-field-box] the 56px row: icon, prefix, control, suffix, trailing
|
|
* [data-md-field-icon] the leading icon
|
|
* [data-md-field-affix] a prefix or suffix beside the value
|
|
* [data-md-field-control] the <input>, <select> or <textarea>
|
|
* [data-md-field-trailing] what ends the row; [data-md-field-error] is the error icon M3 asks
|
|
* for as the error state's second indicator; [data-md-field-button]
|
|
* a trailing icon button (clear, copy, reveal, open a picker)
|
|
* [data-md-field-outline] the fieldset and its legend (the filled field's indicator line)
|
|
* [data-md-field-label] the visible label
|
|
* [data-md-field-support-row] the row under the field
|
|
* [data-md-field-support] the hint, or the error in its place
|
|
* [data-md-field-counter] M3's character counter, `n/max`, at the end of that row
|
|
*
|
|
* A select's open list is not part of the field: it is the dropdown menu in components/menu.css.
|
|
*/
|
|
|
|
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
|
|
|
@import './icon.css';
|
|
|
|
@layer material.components {
|
|
[data-md-field] {
|
|
--field-height: 56px;
|
|
--field-pad: var(--md-sys-measurement-space200);
|
|
--field-icon: 24px;
|
|
--field-gap: var(--md-sys-measurement-space200);
|
|
--field-start: var(--field-pad);
|
|
--field-edge: var(--md-sys-color-outline);
|
|
--field-ink: var(--md-sys-color-on-surface-variant);
|
|
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
}
|
|
|
|
/* M3 § Text Fields, Behaviour: "compact breakpoints can let a text field span full width;
|
|
* medium/expanded should bound it with flexible margins/other containers — never let it span the
|
|
* full width of a large screen." The site names no number, so 40rem is this package's: it is
|
|
* under half the `large` breakpoint's 1200px, a little over half the `expanded` one's, and holds
|
|
* about the 70 characters body-large reads best at — a measure of text, so it is in rem and grows
|
|
* with the text. It is a ceiling, not a width — a narrower pane still gets a narrower field. A
|
|
* width rule from the call site beats it, because an application's CSS and a utility both
|
|
* outrank this layer, and `full` takes it off for a field that really is the width of its pane (a
|
|
* search-and-filter row, an editor). Below `medium` nothing is bounded. */
|
|
@media (width >= 600px) {
|
|
[data-md-field]:not([data-md-full]) {
|
|
max-width: 40rem;
|
|
}
|
|
}
|
|
|
|
/* A filled field's resting indicator line is on-surface-variant, where an outline is outline. */
|
|
[data-md-field][data-md-variant='filled'] {
|
|
--field-edge: var(--md-sys-color-on-surface-variant);
|
|
}
|
|
|
|
[data-md-field][data-md-size='sm'] {
|
|
--field-height: 40px;
|
|
--field-pad: 12px;
|
|
--field-icon: 20px;
|
|
--field-gap: var(--md-sys-measurement-space100);
|
|
}
|
|
|
|
[data-md-field][data-md-size='xs'] {
|
|
--field-height: 32px;
|
|
--field-pad: var(--md-sys-measurement-space125);
|
|
--field-icon: 16px;
|
|
--field-gap: var(--md-sys-measurement-space75);
|
|
}
|
|
|
|
[data-md-field]:has([data-md-field-icon]) {
|
|
--field-start: calc(var(--field-pad) + var(--field-icon) + var(--field-gap));
|
|
}
|
|
|
|
[data-md-field-box] {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--field-gap);
|
|
min-height: var(--field-height);
|
|
padding-inline: var(--field-pad);
|
|
border-radius: var(--md-sys-shape-corner-xs);
|
|
cursor: text;
|
|
}
|
|
|
|
[data-md-field-control] {
|
|
flex: 1 1 0%;
|
|
min-width: 0;
|
|
align-self: stretch;
|
|
background: transparent;
|
|
color: var(--md-sys-color-on-surface);
|
|
caret-color: var(--md-sys-color-primary);
|
|
outline: none;
|
|
font: var(--md-sys-typescale-body-lg);
|
|
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
|
}
|
|
|
|
[data-md-field][data-md-size='xs'] [data-md-field-control] {
|
|
font: var(--md-sys-typescale-body-md);
|
|
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
|
}
|
|
|
|
[data-md-field][data-md-mono] [data-md-field-control] {
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
|
}
|
|
|
|
[data-md-field-control]::placeholder {
|
|
color: var(--md-sys-color-on-surface-variant);
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Autofill paints its own background, which on a card is a pale block in the
|
|
wrong colour. Delaying the transition for a week keeps the field's own. */
|
|
[data-md-field-control]:-webkit-autofill {
|
|
-webkit-text-fill-color: var(--md-sys-color-on-surface);
|
|
transition: background-color 604800s, color 604800s;
|
|
}
|
|
|
|
/* A combobox's arrow turns over while its list is open, as a select's does. */
|
|
[data-md-field]:has([data-md-field-control][aria-expanded='true']) [data-md-field-arrow] {
|
|
rotate: 180deg;
|
|
}
|
|
|
|
[data-md-field-arrow] {
|
|
transition: rotate var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
|
}
|
|
|
|
[data-md-field-icon],
|
|
[data-md-field-trailing] {
|
|
flex: none;
|
|
width: var(--field-icon);
|
|
height: var(--field-icon);
|
|
color: var(--md-sys-color-on-surface-variant);
|
|
}
|
|
|
|
/* The error's second indicator: M3 asks that the colour change never stand alone. */
|
|
[data-md-field-error] {
|
|
color: var(--md-sys-color-error);
|
|
}
|
|
|
|
[data-md-field-icon],
|
|
[data-md-field-arrow] {
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* A trailing button — clear, copy, reveal — is an icon button: the icon in a state layer that
|
|
scales with the field's own icon size (40px at `md`, smaller at `sm`/`xs`), which the
|
|
foundation's fixed-size classes cannot draw; the layer doubles as the touch target, since it
|
|
already reaches 40px+ without a second pseudo. The hook stays `data-md-field-button` rather
|
|
than a class because datepicker and timepicker, not yet rewritten, still select it. */
|
|
[data-md-field-button] {
|
|
position: relative;
|
|
display: grid;
|
|
place-items: center;
|
|
margin-inline-end: calc(-1 * var(--md-sys-measurement-space50));
|
|
border-radius: var(--md-sys-shape-corner-full);
|
|
cursor: pointer;
|
|
outline: none;
|
|
}
|
|
|
|
[data-md-field-button]::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 50% auto auto 50%;
|
|
width: calc(var(--field-icon) + var(--md-sys-measurement-space200));
|
|
height: calc(var(--field-icon) + var(--md-sys-measurement-space200));
|
|
translate: -50% -50%;
|
|
border-radius: inherit;
|
|
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
[data-md-field-button]:hover::before {
|
|
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), transparent);
|
|
}
|
|
}
|
|
|
|
[data-md-field-button]:focus-visible::before {
|
|
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-focus-state-layer-opacity) * 100%), transparent);
|
|
outline: 3px solid var(--md-sys-color-secondary);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
[data-md-field-button]:active::before {
|
|
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-pressed-state-layer-opacity) * 100%), transparent);
|
|
}
|
|
|
|
[data-md-field-button]:disabled::before,
|
|
[data-md-field-button][aria-disabled='true']::before {
|
|
display: none;
|
|
}
|
|
|
|
[data-md-field-affix] {
|
|
flex: none;
|
|
color: var(--md-sys-color-on-surface-variant);
|
|
font: var(--md-sys-typescale-body-lg);
|
|
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
|
}
|
|
|
|
[data-md-field][data-md-size='xs'] [data-md-field-affix] {
|
|
font: var(--md-sys-typescale-body-md);
|
|
}
|
|
|
|
/* The legend is body-small's 16px line less its 5px above the outline: the fieldset starts
|
|
5px up so the notch's edge runs through the middle of the floated label. */
|
|
[data-md-field-outline] {
|
|
position: absolute;
|
|
inset: -0.3125rem 0 0;
|
|
margin: 0;
|
|
padding: 0 calc(var(--field-pad) - var(--md-sys-measurement-space50));
|
|
border: 1px solid var(--field-edge);
|
|
border-radius: inherit;
|
|
pointer-events: none;
|
|
min-width: 0;
|
|
transition: border-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
|
}
|
|
|
|
[data-md-field-outline] > legend {
|
|
display: block;
|
|
width: auto;
|
|
max-width: 100%;
|
|
height: 0.6875rem;
|
|
padding: 0;
|
|
overflow: hidden;
|
|
visibility: hidden;
|
|
white-space: nowrap;
|
|
font: var(--md-sys-typescale-body-sm);
|
|
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
|
transition: max-width var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
|
}
|
|
|
|
[data-md-field-outline] > legend > span {
|
|
display: inline-block;
|
|
padding-inline: var(--md-sys-measurement-space50);
|
|
}
|
|
|
|
[data-md-field-label] {
|
|
position: absolute;
|
|
inset-inline-start: var(--field-pad);
|
|
top: 0;
|
|
max-width: calc(100% - 2 * var(--field-pad));
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
translate: 0 -50%;
|
|
color: var(--field-ink);
|
|
font: var(--md-sys-typescale-body-sm);
|
|
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
|
pointer-events: none;
|
|
transition-property: top, inset-inline-start, font-size, line-height, color;
|
|
transition-duration: var(--md-sys-motion-effects-fast-duration);
|
|
transition-timing-function: var(--md-sys-motion-effects-fast);
|
|
}
|
|
|
|
/* `required` is marked from the control itself, in the label and in the notch,
|
|
so the gap is cut for the asterisk too. */
|
|
[data-md-field]:has([data-md-field-control]:required) [data-md-field-label]::after,
|
|
[data-md-field]:has([data-md-field-control]:required) [data-md-field-outline] > legend > span::after {
|
|
content: ' *';
|
|
}
|
|
|
|
/* Resting: a label, an empty control, and no focus. The label sits where the
|
|
text will go, at the text's size; the notch closes; the placeholder and the
|
|
prefix wait, because the label is standing where they would be. A select and
|
|
a date always hold a value, so they mark themselves `data-md-floated`. */
|
|
[data-md-field]:not([data-md-floated]):has([data-md-field-label]):has([data-md-field-control]:placeholder-shown):not(:focus-within) {
|
|
[data-md-field-label] {
|
|
top: 50%;
|
|
inset-inline-start: var(--field-start);
|
|
font: var(--md-sys-typescale-body-lg);
|
|
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
|
}
|
|
|
|
[data-md-field-outline] > legend {
|
|
max-width: 0.01px;
|
|
}
|
|
|
|
[data-md-field-affix] {
|
|
visibility: hidden;
|
|
}
|
|
}
|
|
|
|
[data-md-field][data-md-size='xs']:not([data-md-floated]):has([data-md-field-label]):has([data-md-field-control]:placeholder-shown):not(:focus-within) [data-md-field-label] {
|
|
font: var(--md-sys-typescale-body-md);
|
|
}
|
|
|
|
[data-md-field]:has([data-md-field-label]):not(:focus-within) [data-md-field-control]::placeholder {
|
|
color: transparent;
|
|
}
|
|
|
|
/* Clearing is offered only once there is something to clear. */
|
|
[data-md-field]:has([data-md-field-control]:placeholder-shown) [data-md-field-clear] {
|
|
display: none;
|
|
}
|
|
|
|
/* Hover only where a pointer can hover, and never on a disabled field: M3 gives a disabled
|
|
control no state layer at all, and the disabled edge below is inherited, so a declaration
|
|
here would beat it whatever the selector weighs. */
|
|
@media (hover: hover) {
|
|
[data-md-field]:not(:has([data-md-field-control]:disabled)) [data-md-field-box]:hover {
|
|
--field-edge: var(--md-sys-color-on-surface);
|
|
}
|
|
}
|
|
|
|
[data-md-field-box]:focus-within {
|
|
--field-edge: var(--md-sys-color-primary);
|
|
--field-ink: var(--md-sys-color-primary);
|
|
}
|
|
|
|
[data-md-field-box]:focus-within [data-md-field-outline] {
|
|
border-width: 2px;
|
|
}
|
|
|
|
[data-md-field][data-md-invalid] {
|
|
--field-edge: var(--md-sys-color-error);
|
|
--field-ink: var(--md-sys-color-error);
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
[data-md-field][data-md-invalid]:not(:has([data-md-field-control]:disabled)) [data-md-field-box]:hover {
|
|
--field-edge: var(--md-sys-color-on-error-container);
|
|
}
|
|
}
|
|
|
|
[data-md-field][data-md-invalid] [data-md-field-box]:focus-within {
|
|
--field-edge: var(--md-sys-color-error);
|
|
--field-ink: var(--md-sys-color-error);
|
|
}
|
|
|
|
/* `data-md-readonly` marks a field the caller made read-only — on the field, not read off the
|
|
control, because a date picker makes its own input read-only too. Nothing here draws it
|
|
differently: M3 says a read-only field keeps "the same visual style as an editable field, but
|
|
clearly labeled read-only", and the native `readonly` attribute is what carries that. The hook
|
|
stays for an application that wants a mark of its own. */
|
|
|
|
[data-md-field]:has([data-md-field-control]:disabled) {
|
|
--field-edge: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-container-opacity) * 100%), transparent);
|
|
--field-ink: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
|
|
|
[data-md-field-box] {
|
|
cursor: default;
|
|
}
|
|
|
|
[data-md-field-control],
|
|
[data-md-field-icon],
|
|
[data-md-field-affix],
|
|
[data-md-field-trailing] {
|
|
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
|
}
|
|
}
|
|
|
|
/* The hint or the error, and — when the field has a maximum — M3's character counter at the far
|
|
end of the same row (specs: "Padding between supporting text and counter | 16dp", which the
|
|
two paddings between them already make). Supporting text sits 4px under the field. */
|
|
[data-md-field-support-row] {
|
|
display: flex;
|
|
align-items: baseline;
|
|
}
|
|
|
|
[data-md-field-support] {
|
|
min-width: 0;
|
|
padding: var(--md-sys-measurement-space50) var(--field-pad) 0;
|
|
color: var(--md-sys-color-on-surface-variant);
|
|
font: var(--md-sys-typescale-body-sm);
|
|
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
|
}
|
|
|
|
[data-md-field][data-md-invalid] [data-md-field-support] {
|
|
color: var(--md-sys-color-error);
|
|
}
|
|
|
|
[data-md-field-counter] {
|
|
flex: none;
|
|
margin-inline-start: auto;
|
|
padding-block-start: var(--md-sys-measurement-space50);
|
|
padding-inline-end: var(--field-pad);
|
|
color: var(--md-sys-color-on-surface-variant);
|
|
font: var(--md-sys-typescale-body-sm);
|
|
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
/* Past the maximum the count is an error, and says so in the error colour. */
|
|
[data-md-field-counter][data-md-over] {
|
|
color: var(--md-sys-color-error);
|
|
}
|
|
|
|
/* The counter's spoken form: the same visually-hidden clip the text classes give
|
|
`md-visually-hidden`, kept as a hook because a package view writes no class but the
|
|
interaction classes (plan step 36). */
|
|
[data-md-field-live] {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
padding: 0;
|
|
margin: -1px;
|
|
overflow: hidden;
|
|
clip-path: inset(50%);
|
|
white-space: nowrap;
|
|
border-width: 0;
|
|
}
|
|
|
|
/* ---- The filled text field ---------------------------------------------------------------- */
|
|
|
|
[data-md-field][data-md-variant='filled'] [data-md-field-box] {
|
|
border-radius: var(--md-sys-shape-corner-xs) var(--md-sys-shape-corner-xs) 0 0;
|
|
background-color: var(--md-sys-color-surface-container-highest);
|
|
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
[data-md-field][data-md-variant='filled'] [data-md-field-box]:hover {
|
|
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), var(--md-sys-color-surface-container-highest));
|
|
}
|
|
}
|
|
|
|
/* The outline becomes the active indicator: one line along the bottom. */
|
|
[data-md-field][data-md-variant='filled'] [data-md-field-outline] {
|
|
inset: auto 0 0;
|
|
padding: 0;
|
|
border: 0;
|
|
border-block-end: 1px solid var(--field-edge);
|
|
border-radius: 0;
|
|
}
|
|
|
|
[data-md-field][data-md-variant='filled'] [data-md-field-outline] > legend {
|
|
display: none;
|
|
}
|
|
|
|
[data-md-field][data-md-variant='filled'] [data-md-field-box]:focus-within [data-md-field-outline] {
|
|
border-block-end-width: 2px;
|
|
}
|
|
|
|
/* The label floats inside the box, above the value, and the value sits below it. */
|
|
[data-md-field][data-md-variant='filled'] [data-md-field-label] {
|
|
top: var(--md-sys-measurement-space200);
|
|
inset-inline-start: var(--field-start);
|
|
max-width: calc(100% - var(--field-start) - var(--field-pad));
|
|
}
|
|
|
|
[data-md-field][data-md-variant='filled']:has([data-md-field-label]) [data-md-field-control] {
|
|
padding-block: var(--md-sys-measurement-space300) var(--md-sys-measurement-space100);
|
|
}
|
|
|
|
[data-md-field][data-md-variant='filled']:has([data-md-field-label]) [data-md-field-affix] {
|
|
align-self: stretch;
|
|
padding-block: var(--md-sys-measurement-space300) var(--md-sys-measurement-space100);
|
|
}
|
|
|
|
[data-md-field][data-md-variant='filled']:has([data-md-field-control]:disabled) {
|
|
--field-edge: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
|
|
|
[data-md-field-box] {
|
|
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 4%, transparent);
|
|
}
|
|
}
|
|
}
|