Files
livewire-material/resources/css/components/timepicker.css
T
Andreas Reinhold / reiniandClaude Opus 5 0341f9d4ca Draw the text field's chrome without Tailwind
<x-field> renders data-md-field with its props and parts as data-md-*
attributes (box, control, label, outline, support, counter, trailing
buttons), and field.css moves into material.components on tokens: px
geometry, spacing and state tokens, the 600px breakpoint (plan step 36).
Its icons take a size prop, 24/20/16 by the field's size.

Every control the field wraps now marks itself data-md-field-control,
so the inputs, the pickers, choices, field.js, menu.css's select and
listbox rules and timepicker.css follow the renamed hooks. Browser tests
cover the error icon, the disabled field's hover, the counter and the
width bound from 600px.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 14:08:22 +02:00

486 lines
18 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* M3's time picker (resources/views/components/timepicker.blade.php, resources/js/timepicker.js):
* the dial, the input variant, and the dialog around them.
*
* Sizes and roles are androidx Compose Material 3's (TimePickerTokens, TimeInputTokens, TimePicker.kt
* and TimePickerDialog.kt at commit 27cf9a7d5788aa0f5f2d8b6699ce279560daf326, Apache-2.0):
*
* [data-timepicker-surface] surface-container-high, extra-large corner, elevation 3; 24px in
* [data-timepicker-title] label-medium, on-surface-variant, 20px above the content
* [data-timepicker-picker] the dial variant
* [data-timepicker-display] the time selector: hour and minute boxes 96×80 (display-large,
* small corner; primary-container when selected, otherwise
* surface-container-highest), a 24px separator, and the period
* selector 52×80 beside them, 4px away. On a 24-hour clock
* (data-cycle="24") there is no period selector and the boxes are
* 114px, TimeSelector24HVerticalContainerWidth.
* [data-timepicker-dial] 256px, surface-container-highest; numbers in body-large at radius
* 101 (the 24-hour inner ring at 69), a primary selector — a 2px line,
* an 8px centre and a 48px handle — with the number under the handle
* in on-primary; 36px below the display, 24px above the actions
* [data-timepicker-inputs] the input variant: fields 96×72 in display-medium, "Hour" and
* "Minute" (or the error) below in body-small, the period 52×72
* [data-timepicker-actions] a 48px row: the mode toggle, then Cancel and OK
*
* The period selector keeps Compose's current shape (ComposeMaterial3Flags.isUpdatedTimepickerToggleEnabled,
* on by default): two buttons 4px apart, round and surface-container-lowest when off, a 12px corner
* when on, not the outlined pair of the tokens. The selected colour is M3's own tertiary-container
* with a bold label (PeriodSelectorSelectedContainerColor), which keeps AM/PM apart from the
* primary-container hour and minute boxes. They are radios, so the state is `aria-checked`.
*
* In a landscape window with no room for the upright dial the dial variant lies on its side, as
* Compose's HorizontalTimePicker does: the display with the period selector (216×38, 16px under it)
* on the start side, the dial 36px after it, the actions under both; and the dial shrinks to 238 or
* 200px when the window is shorter still (ClockFaceSizeModifier). The input variant always stands.
*
* The switch is orientation and viewport *height*, never a width breakpoint: M3 says the picker
* "swaps orientation/variant based on device orientation and viewport height"
* (docs/reference/m3/components-navigation-selection-inputs.md § Time pickers/Behaviour), and no
* window size class applies. 35rem is what the upright dial needs — 33rem of surface (3 padding,
* 2.25 title, 7.25 display, 17.5 dial, 3 actions) inside the dialog's `100dvh - 2rem` — so a
* landscape phone lies the dial down and a landscape tablet or desktop, which has the height,
* leaves it standing.
*
* The selector's angle is a registered property, so a single transition turns the line, carries the
* handle round and moves the clip that inks the number under it, on the default spatial spring. The
* numbers cross-fade between hours and minutes on the default effects spring.
*/
@property --timepicker-angle {
syntax: '<angle>';
inherits: true;
initial-value: 0deg;
}
@layer components {
[data-timepicker-field] [data-md-field-control] {
cursor: pointer;
caret-color: transparent;
}
[data-timepicker-dialog] {
max-width: calc(100vw - 2rem);
max-height: calc(100dvh - 2rem);
}
[data-timepicker-surface] {
display: grid;
grid-template-columns: max-content;
justify-content: center;
padding: 1.5rem;
border-radius: var(--md-sys-shape-corner-xl);
background-color: var(--md-sys-color-surface-container-high);
box-shadow: var(--md-sys-elevation-3);
color: var(--md-sys-color-on-surface);
}
[data-timepicker-title] {
padding-bottom: 1.25rem;
color: var(--md-sys-color-on-surface-variant);
font: var(--md-sys-typescale-label-md);
letter-spacing: var(--md-sys-typescale-label-md-tracking);
}
[data-timepicker-picker] {
display: flex;
flex-direction: column;
align-items: center;
}
/* The mode is an attribute rather than x-show, which shows only on the next frame: focus moves
into the variant that was just switched to within the same tick. */
[data-timepicker-surface]:not([data-mode="input"]) :is([data-timepicker-typing], [data-timepicker-when="input"]),
[data-timepicker-surface][data-mode="input"] :is([data-timepicker-picker], [data-timepicker-when="dial"]) {
display: none;
}
[data-timepicker-when] {
display: contents;
}
/* ---- The time selector and the period selector ------------------------------------------ */
[data-timepicker-display] {
display: flex;
margin-bottom: 2.25rem;
}
[data-timepicker-numbers],
[data-timepicker-inputs] {
display: flex;
align-items: flex-start;
direction: ltr;
}
[data-timepicker-box] {
display: grid;
place-items: center;
width: 6rem;
height: 5rem;
border-radius: var(--md-sys-shape-corner-sm);
background-color: var(--md-sys-color-surface-container-highest);
color: var(--md-sys-color-on-surface);
font: var(--md-sys-typescale-display-lg);
letter-spacing: var(--md-sys-typescale-display-lg-tracking);
font-variant-numeric: tabular-nums;
cursor: pointer;
transition-property: background-color, color;
transition-duration: var(--md-sys-motion-effects-fast-duration);
transition-timing-function: var(--md-sys-motion-effects-fast);
}
[data-timepicker-box][aria-pressed="true"] {
background-color: var(--md-sys-color-primary-container);
color: var(--md-sys-color-on-primary-container);
}
/* A 24-hour clock has no period selector beside the boxes, and M3 widens them into the room
(TimePickerTokens.TimeSelector24HVerticalContainerWidth = 114dp). Landscape keeps 96px: the
display is a 216px column there. */
[data-timepicker-display][data-cycle="24"] [data-timepicker-box] {
width: 7.125rem;
}
[data-timepicker-separator] {
display: grid;
place-items: center;
width: 1.5rem;
height: 5rem;
color: var(--md-sys-color-on-surface);
font: var(--md-sys-typescale-display-lg);
translate: 0 -0.25rem;
user-select: none;
}
[data-timepicker-period] {
display: flex;
flex-direction: column;
gap: 0.25rem;
width: 3.25rem;
height: 5rem;
margin-inline-start: 0.25rem;
}
[data-timepicker-period] > button {
flex: 1 1 0%;
min-width: 0;
border-radius: var(--md-sys-shape-corner-full);
background-color: var(--md-sys-color-surface-container-lowest);
color: var(--md-sys-color-on-surface-variant);
font: var(--md-sys-typescale-title-md);
letter-spacing: var(--md-sys-typescale-title-md-tracking);
cursor: pointer;
transition-property: border-radius, background-color, color;
transition-duration: var(--md-sys-motion-spatial-fast-duration);
transition-timing-function: var(--md-sys-motion-spatial-fast);
}
[data-timepicker-period] > button:active,
[data-timepicker-period] > button[aria-checked="true"] {
border-radius: 0.75rem;
}
/* Tertiary, not the primary the hour and minute boxes take: M3 gives the two selectors different
emphases (PeriodSelectorSelectedContainerColor = TertiaryContainer). */
[data-timepicker-period] > button[aria-checked="true"] {
background-color: var(--md-sys-color-tertiary-container);
color: var(--md-sys-color-on-tertiary-container);
font-weight: var(--md-ref-typeface-weight-bold);
}
[data-timepicker-period] > button:disabled {
cursor: default;
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent);
}
/* ---- The dial ----------------------------------------------------------------------------- */
[data-timepicker-dial] {
--dial: 16rem;
--unit: calc(var(--dial) / 256);
--outer: calc(101 * var(--unit));
--inner: calc(69 * var(--unit));
--handle: calc(48 * var(--unit));
--reach: var(--outer);
position: relative;
flex: none;
width: var(--dial);
height: var(--dial);
margin-bottom: 1.5rem;
border-radius: var(--md-sys-shape-corner-full);
background-color: var(--md-sys-color-surface-container-highest);
color: var(--md-sys-color-on-surface);
font: var(--md-sys-typescale-body-lg);
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
cursor: pointer;
touch-action: none;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
outline: none;
direction: ltr;
transition: --timepicker-angle var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default);
}
[data-timepicker-dial]:focus-visible {
outline: 3px solid var(--md-sys-color-secondary);
outline-offset: 2px;
}
/* A drag keeps the handle under the pointer; the spring only settles it on release. */
[data-timepicker-dial][data-dragging] {
transition: none;
}
[data-timepicker-dial][data-inner] {
--reach: var(--inner);
}
[data-timepicker-labels],
[data-timepicker-ink],
[data-timepicker-set] {
position: absolute;
inset: 0;
}
[data-timepicker-set] {
transition-property: opacity, visibility;
transition-duration: var(--md-sys-motion-effects-default-duration);
transition-timing-function: var(--md-sys-motion-effects-default);
}
[data-timepicker-dial]:not([data-cycle="24"]) [data-timepicker-set="hour24"],
[data-timepicker-dial][data-cycle="24"] [data-timepicker-set="hour12"] {
display: none;
}
[data-timepicker-dial][data-view="minute"] :is([data-timepicker-set="hour12"], [data-timepicker-set="hour24"]),
[data-timepicker-dial]:not([data-view="minute"]) [data-timepicker-set="minute"] {
opacity: 0;
visibility: hidden;
}
[data-timepicker-set] > span {
--ring: var(--outer);
position: absolute;
left: calc(50% + var(--x) * var(--ring));
top: calc(50% + var(--y) * var(--ring));
display: grid;
place-items: center;
width: 3rem;
height: 3rem;
translate: -50% -50%;
border-radius: var(--md-sys-shape-corner-full);
font-variant-numeric: tabular-nums;
}
[data-timepicker-set] > span[data-inner] {
--ring: var(--inner);
}
[data-timepicker-set] > span[data-disabled] {
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
}
/* The selector: the line from the centre to the handle's edge, the centre dot, the handle. */
[data-timepicker-selector] {
position: absolute;
inset: 0;
pointer-events: none;
}
[data-timepicker-track] {
position: absolute;
left: calc(50% - 1px);
bottom: 50%;
width: 2px;
height: calc(var(--reach) - var(--handle) / 2);
background-color: var(--md-sys-color-primary);
transform-origin: 50% 100%;
rotate: var(--timepicker-angle);
}
[data-timepicker-centre],
[data-timepicker-handle] {
position: absolute;
translate: -50% -50%;
border-radius: var(--md-sys-shape-corner-full);
background-color: var(--md-sys-color-primary);
}
[data-timepicker-centre] {
left: 50%;
top: 50%;
width: 0.5rem;
height: 0.5rem;
}
[data-timepicker-handle] {
left: calc(50% + sin(var(--timepicker-angle)) * var(--reach));
top: calc(50% - cos(var(--timepicker-angle)) * var(--reach));
width: var(--handle);
height: var(--handle);
}
/* The numbers again, in on-primary, cut to the handle: what Compose draws with BlendMode.DstOver. */
[data-timepicker-ink] {
color: var(--md-sys-color-on-primary);
pointer-events: none;
clip-path: circle(calc(var(--handle) / 2) at calc(50% + sin(var(--timepicker-angle)) * var(--reach)) calc(50% - cos(var(--timepicker-angle)) * var(--reach)));
}
[data-timepicker-ink] [data-timepicker-set] > span[data-disabled] {
color: inherit;
}
/* ---- The input variant -------------------------------------------------------------------- */
[data-timepicker-inputs] [data-timepicker-separator] {
height: 4.5rem;
}
[data-timepicker-inputs] [data-timepicker-period] {
height: 4.5rem;
}
[data-timepicker-column] {
width: 6rem;
}
[data-timepicker-input] {
display: block;
width: 6rem;
height: 4.5rem;
padding: 0;
border: 0;
border-radius: var(--md-sys-shape-corner-sm);
outline: none;
background-color: var(--md-sys-color-surface-container-highest);
color: var(--md-sys-color-on-surface);
caret-color: var(--md-sys-color-primary);
font: var(--md-sys-typescale-display-md);
letter-spacing: var(--md-sys-typescale-display-md-tracking);
font-variant-numeric: tabular-nums;
text-align: center;
transition-property: background-color, color, box-shadow;
transition-duration: var(--md-sys-motion-effects-fast-duration);
transition-timing-function: var(--md-sys-motion-effects-fast);
}
[data-timepicker-input]:focus {
background-color: var(--md-sys-color-primary-container);
color: var(--md-sys-color-on-primary-container);
box-shadow: inset 0 0 0 2px var(--md-sys-color-primary);
}
[data-timepicker-input][aria-invalid="true"] {
background-color: var(--md-sys-color-error-container);
color: var(--md-sys-color-error);
caret-color: var(--md-sys-color-error);
box-shadow: inset 0 0 0 1px var(--md-sys-color-error);
}
[data-timepicker-input][aria-invalid="true"]:focus {
box-shadow: inset 0 0 0 2px var(--md-sys-color-error);
}
/* SupportingText: two lines' room, 7px under the field; the error in its place. */
[data-timepicker-support] {
min-height: 2rem;
padding-top: 0.4375rem;
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-timepicker-support][data-error],
[data-timepicker-range-error] {
color: var(--md-sys-color-error);
}
[data-timepicker-range-error] {
max-width: 17rem;
padding-bottom: 0.5rem;
font: var(--md-sys-typescale-body-sm);
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
}
[data-timepicker-actions] {
display: flex;
align-items: center;
gap: 0.5rem;
min-height: 3rem;
}
[data-timepicker-actions] > [data-timepicker-spacer] {
flex: 1 1 0%;
}
/* ---- Landscape ---------------------------------------------------------------------------- */
@media (orientation: landscape) and (height < 35rem) {
[data-timepicker-surface][data-mode="dial"] {
grid-template-columns: 13.5rem auto;
grid-template-areas:
"display dial"
"actions actions";
column-gap: 2.25rem;
padding: 1rem 1.5rem 0.5rem;
}
[data-timepicker-surface][data-mode="dial"] [data-timepicker-title] {
grid-area: display;
align-self: start;
margin-top: 0.5rem;
padding: 0;
}
[data-timepicker-surface][data-mode="dial"] [data-timepicker-picker] {
display: contents;
}
[data-timepicker-surface][data-mode="dial"] [data-timepicker-display] {
grid-area: display;
flex-direction: column;
align-self: center;
margin: 0;
}
[data-timepicker-surface][data-mode="dial"] [data-timepicker-display][data-cycle="24"] [data-timepicker-box] {
width: 6rem;
}
[data-timepicker-surface][data-mode="dial"] [data-timepicker-display] [data-timepicker-period] {
flex-direction: row;
width: 13.5rem;
height: 2.375rem;
margin: 1rem 0 0;
}
[data-timepicker-surface][data-mode="dial"] [data-timepicker-dial] {
grid-area: dial;
margin: 0;
}
[data-timepicker-surface][data-mode="dial"] [data-timepicker-actions] {
grid-area: actions;
margin-top: 0.25rem;
}
}
@media (orientation: landscape) and (max-height: 22.75rem) {
[data-timepicker-surface][data-mode="dial"] [data-timepicker-dial] {
--dial: 14.875rem;
}
}
@media (orientation: landscape) and (max-height: 21.625rem) {
[data-timepicker-surface][data-mode="dial"] [data-timepicker-dial] {
--dial: 12.5rem;
}
}
}