Key breakpoints, scales, inks and states on M3's tokens
Tailwind's sm…2xl are cleared for M3's window size classes (medium 600, expanded 840, large 1200, extra-large 1600; resources/js/breakpoints.js for scripts), and its radius, shadow, text-size, weight, leading, tracking and easing scales are cleared like its palette, so only M3's utilities compile. The semantic inks are roles rather than opacities (M3 reserves 38% for disabled). state.css declares M3's state opacities as tokens, adds the dragged layer and a touch-target utility. Plan: docs/plans/material-3-alignment.md, steps 1, 2, 3 and 8. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
651a513d1e
commit
25ff5a8d71
@@ -1,15 +1,34 @@
|
|||||||
/*
|
/*
|
||||||
* Interaction states, as M3 draws them.
|
* Interaction states, as M3 draws them.
|
||||||
*
|
*
|
||||||
* `state-layer` puts M3's state layer on an element: its content colour laid over it at 8%
|
* The opacities are M3's state tokens (md.sys.state.*): the content colour laid over an
|
||||||
* on hover, 10% on focus and press. Hover only where the pointer can hover, because a touch
|
* element at 8% on hover, 10% on focus and press, 16% while dragged; disabled content at 38%
|
||||||
* screen keeps the last hover after a tap. The layer is a ::before, so the element becomes
|
* and a disabled container at 12%.
|
||||||
* `position: relative` and `isolation: isolate`; never pass `static` or a z-index to it.
|
|
||||||
*
|
*
|
||||||
* `focus-ring` is M3's focus indicator — 3px in secondary, 2px out — drawn only for keyboard
|
* `state-layer` puts M3's state layer on an element. Hover only where the pointer can hover,
|
||||||
* focus. `link` is a link in running text: underlined in the colour of the words around it.
|
* because a touch screen keeps the last hover after a tap; `data-dragged` on the element draws
|
||||||
|
* the dragged layer. The layer is a ::before, so the element becomes `position: relative` and
|
||||||
|
* `isolation: isolate`; never pass `static` or a z-index to it.
|
||||||
|
*
|
||||||
|
* `touch-target` extends an element's pointer target to M3's 48×48px minimum without changing
|
||||||
|
* what is drawn: a ::after centred on it, at least 48px each way. It is `::after`-based, so a
|
||||||
|
* component that already draws with ::after wraps its control in an element carrying it.
|
||||||
|
*
|
||||||
|
* `focus-ring` is M3's focus indicator — 3px in secondary, 2px out, drawn only for keyboard
|
||||||
|
* focus. The site gives no thickness or offset; the values are @material/web's focus ring
|
||||||
|
* tokens (Apache-2.0, © Google LLC). `link` is a link in running text: underlined in the
|
||||||
|
* colour of the words around it.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--md-sys-state-hover-state-layer-opacity: 0.08;
|
||||||
|
--md-sys-state-focus-state-layer-opacity: 0.1;
|
||||||
|
--md-sys-state-pressed-state-layer-opacity: 0.1;
|
||||||
|
--md-sys-state-dragged-state-layer-opacity: 0.16;
|
||||||
|
--md-sys-state-disabled-content-opacity: 0.38;
|
||||||
|
--md-sys-state-disabled-container-opacity: 0.12;
|
||||||
|
}
|
||||||
|
|
||||||
@utility state-layer {
|
@utility state-layer {
|
||||||
position: relative;
|
position: relative;
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
@@ -28,13 +47,20 @@
|
|||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
&:hover::before {
|
&:hover::before {
|
||||||
opacity: 0.08;
|
opacity: var(--md-sys-state-hover-state-layer-opacity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible::before,
|
&:focus-visible::before {
|
||||||
|
opacity: var(--md-sys-state-focus-state-layer-opacity);
|
||||||
|
}
|
||||||
|
|
||||||
&:active::before {
|
&:active::before {
|
||||||
opacity: 0.1;
|
opacity: var(--md-sys-state-pressed-state-layer-opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-dragged]::before {
|
||||||
|
opacity: var(--md-sys-state-dragged-state-layer-opacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled::before,
|
&:disabled::before,
|
||||||
@@ -43,6 +69,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@utility touch-target {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 3rem;
|
||||||
|
min-height: 3rem;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@utility focus-ring {
|
@utility focus-ring {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,40 @@
|
|||||||
--font-sans: var(--md-ref-typeface-brand);
|
--font-sans: var(--md-ref-typeface-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Breakpoints are M3's window size classes, and only those: compact is below `medium`, then
|
||||||
|
* medium 600, expanded 840, large 1200 and extra-large 1600 (`medium:`, `expanded:`, `large:`,
|
||||||
|
* `extra-large:`, and `max-medium:` … for "below"). Tailwind's sm/md/lg/xl/2xl are cleared —
|
||||||
|
* a `sm:` compiles to nothing — because a layout decision keyed on 640px means nothing in M3.
|
||||||
|
* resources/js/breakpoints.js carries the same four numbers for scripts.
|
||||||
|
*/
|
||||||
|
@theme {
|
||||||
|
--breakpoint-*: initial;
|
||||||
|
--breakpoint-medium: 600px;
|
||||||
|
--breakpoint-expanded: 840px;
|
||||||
|
--breakpoint-large: 1200px;
|
||||||
|
--breakpoint-extra-large: 1600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Only M3's scales compile. Tailwind's default radius, shadow, text-size, weight, leading,
|
||||||
|
* tracking and easing utilities are cleared like its palette, so `rounded-lg`, `text-sm`,
|
||||||
|
* `font-medium`, `shadow-md` or `ease-in-out` compile to nothing; the M3 sets are declared
|
||||||
|
* below (`rounded-corner-*`, `shadow-elevation-*`, `type-*`, `ease-spatial-*`…). The 4px
|
||||||
|
* spacing scale stays: it is M3's spacing grid.
|
||||||
|
*/
|
||||||
|
@theme {
|
||||||
|
--radius-*: initial;
|
||||||
|
--shadow-*: initial;
|
||||||
|
--inset-shadow-*: initial;
|
||||||
|
--drop-shadow-*: initial;
|
||||||
|
--text-*: initial;
|
||||||
|
--font-weight-*: initial;
|
||||||
|
--leading-*: initial;
|
||||||
|
--tracking-*: initial;
|
||||||
|
--ease-*: initial;
|
||||||
|
}
|
||||||
|
|
||||||
/* The colour roles, as generated by `php artisan material:scheme`. */
|
/* The colour roles, as generated by `php artisan material:scheme`. */
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--md-sys-color-background);
|
--color-background: var(--md-sys-color-background);
|
||||||
@@ -94,16 +128,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Ink and lines by what they are, rather than by an opacity number that means nothing out of
|
* Ink and lines by what they are: body copy, metadata, decoration; a structural line, a line
|
||||||
* context: body copy, metadata, decoration; a structural line, a line around chrome, a divider.
|
* around chrome, a divider. Each is an M3 role, never an opacity: M3 marks emphasis with
|
||||||
|
* on-surface-variant and outline, and reserves 38% for disabled content, so an ink dimmed by
|
||||||
|
* opacity reads as disabled and loses its contrast guarantee.
|
||||||
*/
|
*/
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-body: var(--md-sys-color-on-surface-variant);
|
--color-body: var(--md-sys-color-on-surface-variant);
|
||||||
--color-meta: color-mix(in oklab, var(--md-sys-color-on-surface) 60%, transparent);
|
--color-meta: var(--md-sys-color-on-surface-variant);
|
||||||
--color-quiet: color-mix(in oklab, var(--md-sys-color-on-surface) 38%, transparent);
|
--color-quiet: var(--md-sys-color-outline);
|
||||||
--color-structure: var(--md-sys-color-outline-variant);
|
--color-structure: var(--md-sys-color-outline-variant);
|
||||||
--color-chrome: color-mix(in oklab, var(--md-sys-color-outline-variant) 60%, transparent);
|
--color-chrome: var(--md-sys-color-outline-variant);
|
||||||
--color-divider: color-mix(in oklab, var(--md-sys-color-outline-variant) 40%, transparent);
|
--color-divider: var(--md-sys-color-outline-variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* M3's window size classes, for scripts that ask the viewport width.
|
||||||
|
*
|
||||||
|
* The same four numbers as the `medium:`, `expanded:`, `large:` and `extra-large:` variants in
|
||||||
|
* tokens/theme.css; compact is everything below `medium`. `from('expanded')` and `upTo('medium')`
|
||||||
|
* return MediaQueryLists on the range syntax Tailwind compiles, so a script and a stylesheet
|
||||||
|
* never disagree at the boundary pixel.
|
||||||
|
*/
|
||||||
|
export const breakpoints = Object.freeze({
|
||||||
|
medium: 600,
|
||||||
|
expanded: 840,
|
||||||
|
large: 1200,
|
||||||
|
'extra-large': 1600,
|
||||||
|
})
|
||||||
|
|
||||||
|
const width = (name) => {
|
||||||
|
if (!(name in breakpoints)) {
|
||||||
|
throw new Error(`Unknown breakpoint "${name}". Use one of: ${Object.keys(breakpoints).join(', ')}.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return breakpoints[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Matches at and above the class's lower edge, like `medium:`. */
|
||||||
|
export const from = (name) => window.matchMedia(`(width >= ${width(name)}px)`)
|
||||||
|
|
||||||
|
/** Matches below the class's lower edge, like `max-medium:`. */
|
||||||
|
export const upTo = (name) => window.matchMedia(`(width < ${width(name)}px)`)
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
@foreach (['primary', 'secondary', 'tertiary'] as $role)
|
@foreach (['primary', 'secondary', 'tertiary'] as $role)
|
||||||
<span
|
<span
|
||||||
style="--swatch-light: {{ $scheme['light'][$role] }}; --swatch-dark: {{ $scheme['dark'][$role] }}"
|
style="--swatch-light: {{ $scheme['light'][$role] }}; --swatch-dark: {{ $scheme['dark'][$role] }}"
|
||||||
class="size-5 rounded-full bg-(--swatch-light) ring-2 ring-surface-container-low in-has-checked:ring-secondary-container dark:bg-(--swatch-dark)"
|
class="size-5 rounded-corner-full bg-(--swatch-light) ring-2 ring-surface-container-low in-has-checked:ring-secondary-container dark:bg-(--swatch-dark)"
|
||||||
></span>
|
></span>
|
||||||
@endforeach
|
@endforeach
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -299,7 +299,7 @@
|
|||||||
data-tick="{{ $format($mark['fraction']) }}"
|
data-tick="{{ $format($mark['fraction']) }}"
|
||||||
@if ($mark['hidden']) hidden @endif
|
@if ($mark['hidden']) hidden @endif
|
||||||
@if ($mark['active']) data-active @endif
|
@if ($mark['active']) data-active @endif
|
||||||
@class(['absolute top-1/2 size-1 -translate-1/2 rounded-full', $tickInk[$color], 'group-has-disabled/slider:bg-on-surface/38 group-has-disabled/slider:data-active:bg-on-surface/12'])
|
@class(['absolute top-1/2 size-1 -translate-1/2 rounded-corner-full', $tickInk[$color], 'group-has-disabled/slider:bg-on-surface/38 group-has-disabled/slider:data-active:bg-on-surface/12'])
|
||||||
style="left: {{ $calc($mark['at']) }}"
|
style="left: {{ $calc($mark['at']) }}"
|
||||||
></span>
|
></span>
|
||||||
@endforeach
|
@endforeach
|
||||||
@@ -308,7 +308,7 @@
|
|||||||
<span
|
<span
|
||||||
data-stop="{{ $part }}"
|
data-stop="{{ $part }}"
|
||||||
@if (! isset($stops[$part])) hidden @endif
|
@if (! isset($stops[$part])) hidden @endif
|
||||||
@class(['absolute top-1/2 size-1 -translate-1/2 rounded-full', $activeInk[$color], 'group-has-disabled/slider:bg-on-surface/38'])
|
@class(['absolute top-1/2 size-1 -translate-1/2 rounded-corner-full', $activeInk[$color], 'group-has-disabled/slider:bg-on-surface/38'])
|
||||||
style="{{ isset($stops[$part]) ? 'left: '.$calc($stops[$part]) : '' }}"
|
style="{{ isset($stops[$part]) ? 'left: '.$calc($stops[$part]) : '' }}"
|
||||||
></span>
|
></span>
|
||||||
@endforeach
|
@endforeach
|
||||||
@@ -329,7 +329,7 @@
|
|||||||
@foreach ($thumbs as $index => $thumb)
|
@foreach ($thumbs as $index => $thumb)
|
||||||
<span data-handle="{{ $thumb }}" class="group/thumb absolute inset-y-0 w-0" style="left: {{ $calc($place($fractions[$index])) }}">
|
<span data-handle="{{ $thumb }}" class="group/thumb absolute inset-y-0 w-0" style="left: {{ $calc($place($fractions[$index])) }}">
|
||||||
<span @class([
|
<span @class([
|
||||||
'absolute top-1/2 left-0 w-1 -translate-1/2 rounded-full',
|
'absolute top-1/2 left-0 w-1 -translate-1/2 rounded-corner-full',
|
||||||
'transition-[width,height] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast',
|
'transition-[width,height] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast',
|
||||||
'group-data-pressed/thumb:w-0.5 group-data-focused/thumb:w-0.5',
|
'group-data-pressed/thumb:w-0.5 group-data-focused/thumb:w-0.5',
|
||||||
'outline-offset-2 outline-secondary group-data-focused/thumb:outline-3',
|
'outline-offset-2 outline-secondary group-data-focused/thumb:outline-3',
|
||||||
|
|||||||
Reference in New Issue
Block a user