/* * 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: ''; 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); } /* 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; } } }