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