Files
livewire-material/resources/css/components/timepicker.css
T
Andreas Reinhold / reiniandClaude Opus 5 459678c341 Size the timepicker in px, as its landscape queries already are
Plan step 36 review of d46b07c1. The landscape queries moved from rem
to px (560/364/346, equal at the default 16px) while every size they
make room for stayed in rem, so with a larger browser text size the
dial grew past a threshold that no longer moved with it. The sizes are
px now, as M3 gives them in dp, spacing the measurement tokens, the
selected period corner corner-md, and disabled the 38% and 12% state
tokens; the supporting text's two-line room stays in rem, being text.
The header's arithmetic is in px, and its note on targets says why the
period halves render no md-touch-target instead of claiming nothing
here reaches 48px.

The landscape browser test expected the display and the dial to share a
top edge, but the display is centred in the dial's 256px row, 61px
lower; it now checks the display sits inside that row with the dial
36px after it. Feature tests pin the classes and the three queries.

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

523 lines
20 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-md-timepicker-surface] surface-container-high, extra-large corner, elevation 3; 24px in
* [data-md-timepicker-title] label-medium, on-surface-variant, 20px above the content
* [data-md-timepicker-picker] the dial variant
* [data-md-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-md-cycle="24") there is no period selector and the boxes are
* 114px, TimeSelector24HVerticalContainerWidth.
* [data-md-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-md-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-md-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 — so the media queries below name a viewport height and never a width.
* 560px is what the upright dial needs — 528px of surface (48 padding, 36 title, 116 display, 280
* dial, 48 actions) inside the dialog's `100dvh - 32px` — so a landscape phone lies the dial down
* and a landscape tablet or desktop, which has the height, leaves it standing. The queries and the
* sizes they make room for are both px (M3 gives the sizes in dp), so a larger text size moves
* neither.
*
* The hour and minute boxes and the period buttons render the foundation's `md-state-layer` and
* `md-focus-ring` (foundation/interaction.css) — each is its own interactive box with nothing drawn
* smaller inside it, so the classes need no refinement. The dial renders `md-focus-ring` alone (it
* draws no state layer of its own); nothing here copies its outline rule. Cancel, OK and the mode
* toggle are <x-button>, which draws from the classes already. `md-touch-target` is not rendered:
* the boxes, the dial and the inputs are well over 48px, and the period buttons are the two halves
* of M3's 52×80 (72 in the input variant, 216×38 lying down) selector, where a 48px target on each
* would reach over its neighbour.
*
* Disabled is on-surface at M3's 38% content and 12% container opacities (tokens/state.css).
*
* 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.
*/
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@import './field.css';
@import './icon.css';
@import './button.css';
@layer material.components {
@property --timepicker-angle {
syntax: '<angle>';
inherits: true;
initial-value: 0deg;
}
[data-md-timepicker-field] [data-md-field-control] {
cursor: pointer;
caret-color: transparent;
}
/* The dialog: M3's spatial-fast pop, a 32%-scrim backdrop, no default border or padding. */
[data-md-timepicker-dialog] {
max-width: calc(100vw - var(--md-sys-measurement-space400));
max-height: calc(100dvh - var(--md-sys-measurement-space400));
margin: auto;
padding: 0;
overflow: visible;
border: 0;
background: transparent;
color: var(--md-sys-color-on-surface);
opacity: 1;
scale: 1;
transition-property: opacity, scale;
transition-duration: var(--md-sys-motion-spatial-fast-duration);
transition-timing-function: var(--md-sys-motion-spatial-fast);
}
@starting-style {
[data-md-timepicker-dialog] {
opacity: 0;
scale: 0.95;
}
}
[data-md-timepicker-dialog]::backdrop {
background-color: color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent);
}
[data-md-timepicker-surface] {
display: grid;
grid-template-columns: max-content;
justify-content: center;
padding: var(--md-sys-measurement-space300);
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-md-timepicker-title] {
padding-bottom: 20px;
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-md-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-md-timepicker-surface]:not([data-md-mode='input']) :is([data-md-timepicker-typing], [data-md-timepicker-when='input']),
[data-md-timepicker-surface][data-md-mode='input'] :is([data-md-timepicker-picker], [data-md-timepicker-when='dial']) {
display: none;
}
[data-md-timepicker-when] {
display: contents;
}
/* ---- The time selector and the period selector ------------------------------------------ */
[data-md-timepicker-display] {
display: flex;
margin-bottom: 36px;
}
[data-md-timepicker-numbers],
[data-md-timepicker-inputs] {
display: flex;
align-items: flex-start;
direction: ltr;
}
[data-md-timepicker-box] {
display: grid;
place-items: center;
width: 96px;
height: 80px;
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-md-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-md-timepicker-display][data-md-cycle='24'] [data-md-timepicker-box] {
width: 114px;
}
[data-md-timepicker-separator] {
display: grid;
place-items: center;
width: 24px;
height: 80px;
color: var(--md-sys-color-on-surface);
font: var(--md-sys-typescale-display-lg);
translate: 0 -4px;
user-select: none;
}
[data-md-timepicker-period] {
display: flex;
flex-direction: column;
gap: var(--md-sys-measurement-space50);
width: 52px;
height: 80px;
margin-inline-start: var(--md-sys-measurement-space50);
}
[data-md-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-md-timepicker-period] > button:active,
[data-md-timepicker-period] > button[aria-checked='true'] {
border-radius: var(--md-sys-shape-corner-md);
}
/* Tertiary, not the primary the hour and minute boxes take: M3 gives the two selectors different
emphases (PeriodSelectorSelectedContainerColor = TertiaryContainer). */
[data-md-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-md-timepicker-period] > button:disabled {
cursor: default;
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-container-opacity) * 100%), transparent);
}
/* ---- The dial ----------------------------------------------------------------------------- */
[data-md-timepicker-dial] {
--dial: 256px;
--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: var(--md-sys-measurement-space300);
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;
direction: ltr;
transition: --timepicker-angle var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default);
}
/* A drag keeps the handle under the pointer; the spring only settles it on release. */
[data-md-timepicker-dial][data-md-dragging] {
transition: none;
}
[data-md-timepicker-dial][data-md-inner] {
--reach: var(--inner);
}
[data-md-timepicker-labels],
[data-md-timepicker-ink],
[data-md-timepicker-set] {
position: absolute;
inset: 0;
}
[data-md-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-md-timepicker-dial]:not([data-md-cycle='24']) [data-md-timepicker-set='hour24'],
[data-md-timepicker-dial][data-md-cycle='24'] [data-md-timepicker-set='hour12'] {
display: none;
}
[data-md-timepicker-dial][data-md-view='minute'] :is([data-md-timepicker-set='hour12'], [data-md-timepicker-set='hour24']),
[data-md-timepicker-dial]:not([data-md-view='minute']) [data-md-timepicker-set='minute'] {
opacity: 0;
visibility: hidden;
}
[data-md-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: 48px;
height: 48px;
translate: -50% -50%;
border-radius: var(--md-sys-shape-corner-full);
font-variant-numeric: tabular-nums;
}
[data-md-timepicker-set] > span[data-md-inner] {
--ring: var(--inner);
}
[data-md-timepicker-set] > span[data-md-disabled] {
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
}
/* The selector: the line from the centre to the handle's edge, the centre dot, the handle. */
[data-md-timepicker-selector] {
position: absolute;
inset: 0;
pointer-events: none;
}
[data-md-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-md-timepicker-centre],
[data-md-timepicker-handle] {
position: absolute;
translate: -50% -50%;
border-radius: var(--md-sys-shape-corner-full);
background-color: var(--md-sys-color-primary);
}
[data-md-timepicker-centre] {
left: 50%;
top: 50%;
width: 8px;
height: 8px;
}
[data-md-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-md-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-md-timepicker-ink] [data-md-timepicker-set] > span[data-md-disabled] {
color: inherit;
}
/* ---- The input variant -------------------------------------------------------------------- */
[data-md-timepicker-inputs] [data-md-timepicker-separator] {
height: 72px;
}
[data-md-timepicker-inputs] [data-md-timepicker-period] {
height: 72px;
}
[data-md-timepicker-column] {
width: 96px;
}
[data-md-timepicker-input] {
display: block;
width: 96px;
height: 72px;
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-md-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-md-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-md-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. The room is two
body-small lines (1rem each), so it stays in rem and grows with the text. */
[data-md-timepicker-support] {
min-height: 2rem;
padding-top: 7px;
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-timepicker-support][data-md-error],
[data-md-timepicker-range-error] {
color: var(--md-sys-color-error);
}
[data-md-timepicker-range-error] {
max-width: 272px;
padding-bottom: var(--md-sys-measurement-space100);
font: var(--md-sys-typescale-body-sm);
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
}
[data-md-timepicker-actions] {
display: flex;
align-items: center;
gap: var(--md-sys-measurement-space100);
min-height: 48px;
}
[data-md-timepicker-actions] > [data-md-timepicker-spacer] {
flex: 1 1 0%;
}
/* ---- Landscape ---------------------------------------------------------------------------- */
@media (orientation: landscape) and (height < 560px) {
[data-md-timepicker-surface][data-md-mode='dial'] {
grid-template-columns: 216px auto;
grid-template-areas:
'display dial'
'actions actions';
column-gap: 36px;
padding: var(--md-sys-measurement-space200) var(--md-sys-measurement-space300) var(--md-sys-measurement-space100);
}
[data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-title] {
grid-area: display;
align-self: start;
margin-top: var(--md-sys-measurement-space100);
padding: 0;
}
[data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-picker] {
display: contents;
}
[data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-display] {
grid-area: display;
flex-direction: column;
align-self: center;
margin: 0;
}
[data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-display][data-md-cycle='24'] [data-md-timepicker-box] {
width: 96px;
}
[data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-display] [data-md-timepicker-period] {
flex-direction: row;
width: 216px;
height: 38px;
margin: var(--md-sys-measurement-space200) 0 0;
}
[data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-dial] {
grid-area: dial;
margin: 0;
}
[data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-actions] {
grid-area: actions;
margin-top: var(--md-sys-measurement-space50);
}
}
@media (orientation: landscape) and (height <= 364px) {
[data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-dial] {
--dial: 238px;
}
}
@media (orientation: landscape) and (height <= 346px) {
[data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-dial] {
--dial: 200px;
}
}
}