Files
livewire-material/resources/css/components/timepicker.css
T
Andreas Reinhold / reiniandClaude Opus 5 b8239dd215 Add the M3 time picker
The dial and input time picker in a modal dialog, with the hour cycle
from the locale, steps, a min and max that may span midnight, and a
landscape layout, ported from Compose.

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

461 lines
16 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
* [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 is Compose's current one (ComposeMaterial3Flags.isUpdatedTimepickerToggleEnabled,
* on by default): two toggle buttons 4px apart, round and surface-container-lowest when off, a 12px
* corner and primary-container with a bold label when on, not the outlined pair of the tokens.
*
* In a landscape window the dial variant lies on its side, as Compose's HorizontalTimePicker does
* whenever the screen is wider than it is tall: 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 short (ClockFaceSizeModifier). The input variant always stands.
*
* 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] .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);
}
[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-pressed="true"] {
border-radius: 0.75rem;
}
[data-timepicker-period] > button[aria-pressed="true"] {
background-color: var(--md-sys-color-primary-container);
color: var(--md-sys-color-on-primary-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) 10%, 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 (min-width: 37rem) {
[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-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 (min-width: 37rem) and (max-height: 22.75rem) {
[data-timepicker-surface][data-mode="dial"] [data-timepicker-dial] {
--dial: 14.875rem;
}
}
@media (orientation: landscape) and (min-width: 37rem) and (max-height: 21.625rem) {
[data-timepicker-surface][data-mode="dial"] [data-timepicker-dial] {
--dial: 12.5rem;
}
}
}