Plan step 16. `navigation.js` takes its standard-rail threshold from
`from('expanded')` (840px) instead of the 64rem it had, and `navigation.css`'s
three 64rem queries follow, so the whole expanded class gets the in-layout
collapsible rail M3 asks for rather than one that opens over a scrim (N-06);
its two 40rem queries become 37.5rem, the compact/medium boundary the bar's own
container query already used (N-07). `search.js` and `datepicker.js` swap their
39.99rem media strings for `upTo('medium')`, so the full-screen search view and
the modal date picker end at 600px, not 640px.
The time picker's landscape layout stops keying on width at all: M3 swaps it on
orientation and viewport height, so it is now landscape plus a window too short
for the upright dial (35rem, the dialog's own height), and the two dial-shrink
rules key on height alone.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
469 lines
17 KiB
CSS
469 lines
17 KiB
CSS
/*
|
||
* 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 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] .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 (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-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;
|
||
}
|
||
}
|
||
}
|