Cut duplicated and speculative code across the package
An over-engineering audit of the whole tree, applied in five reviewed batches. Behaviour stays the same except where UPGRADE.md says otherwise. PHP: the showcase and error-page stylesheets are prebuilt into resources/dist by bin/stylesheets.mjs, through Vite's own postcss-import (first occurrence kept, the order an application's build gives), instead of Stylesheets::bundle() inlining imports on every request; only the import walk DesignGuard needs stays. SchemeStylesheet::withProfiles() replaces three copies of the scheme-plus-profiles loop, material:scheme leaves spec and contrast checks to the node script that already made them, and the error page's scheme cache, the hashed view namespace, the translations path with no lang/ folder and DesignGuard's 1.x-name hints are gone. JS: the androidx shape port progress.js and both bin scripts each carried lives once in resources/js/shapes.js (the generated SVGs are unchanged); util.js holds ringIndex(), ms(), reopenGuard() and remember(), which were written out several times; listeners are released through AbortController; tooltip.js's hoverPopover() serves the rich tooltip too. CSS: every rule for an element inside the navigation rail queries `--md-navigation-rail-value` instead of repeating the seven collapsed conditions under five media branches; badge, alert, progress, slider and button read one non-inheriting colour-role table (components/color.css); the dialog chrome, the submenu's popover chrome, the chip's state layer and touch target, and the visually-hidden inputs use the shared rules they copied; foundation/tokens.css is folded into foundation.css. Views: Support\Field and Support\Link replace the error-key, bound-value and link-attribute blocks copied into the fields and link components; the timepicker period group, the menu filter and the showcase head are partials; the datepicker's steppers and entry fields are loops; component docblocks no longer restate SKILL.md. Tests and tooling: one dataset-driven ComponentStylesheetsTest replaces four per-group files, DesignGuardTest and the layout-component tests use datasets, browser tests share one ready() helper, CSS parsing lives in ComponentStylesheet alone. docs/audits and the finding IDs citing it are removed, as are pestphp/pest-plugin-laravel, the unused composer scripts and check:font; the lint job runs in the feature job, which now installs node packages so the prebuilt-stylesheet staleness test runs in CI. Feature suite 1177 passed, Chrome browser suite 299 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
471d927e64
commit
247c596c3a
@@ -5,7 +5,6 @@ This application uses `nonameweb/livewire-material`: Material 3 Expressive compo
|
||||
|
||||
- Components are anonymous Blade components, unprefixed unless `config/livewire-material.php` sets a `prefix`. Before writing or changing a view that uses them, activate the `livewire-material-development` skill for the props, slots and traps of each component.
|
||||
- The CSS entry imports `foundation.css` first, then the stylesheet of each component the views render (or `all.css` for all of them). A component whose stylesheet is not imported renders unstyled; `DesignGuard::missingStylesheets()` names each missing `@import`.
|
||||
- Never write a utility class — Tailwind's, the library's 1.x ones or daisyUI's — or a maryUI tag. Nothing defines them, so they compile to nothing and fail silently. Layout is the layout components (`<x-row>`, `<x-stack>`, `<x-grid>`, `<x-surface>`, `<x-pane>`), text is `md-type-*` and `md-ink-*`, and everything else is the application's own CSS on `--md-sys-*` custom properties.
|
||||
- Every layout includes `<x-theme-script />` in `<head>` before `@vite`. The colour scheme is generated with `php artisan material:scheme` — never edit `resources/css/material-scheme.css` by hand. With colour profiles (`livewire-material.profiles`), run it without a seed after changing them; the active profile comes from `Scheme::resolveProfileUsing()`.
|
||||
- While the application runs locally, every token and component renders in the application's own scheme at `/material` (the showcase).
|
||||
- HTTP error pages and the Markdown mail theme come from the package. Change error wording by publishing `--tag=livewire-material-errors`; select the mail theme with `MAIL_MARKDOWN_THEME=livewire-material::mail.theme`.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
Every view in this application is Material 3 Expressive (m3.material.io), through `nonameweb/livewire-material`. These rules decide what to write; the `material-3-design` skill carries the tables, the numbers and Google's source pages behind each one — activate it before designing a screen.
|
||||
|
||||
The library is plain CSS on M3's tokens, and there are no utility classes: a Tailwind class, or one of the library's 1.x utilities (bg-primary, type-body-md, medium:hidden), compiles to nothing. A view is written three ways:
|
||||
The library is plain CSS on M3's tokens, and there are no utility classes: a Tailwind class, one of the library's 1.x utilities (bg-primary, type-body-md, medium:hidden) or a daisyUI class compiles to nothing, and so does a maryUI tag. A view is written three ways:
|
||||
- Components and their props: `<x-button variant="filled">`, and the layout components `<x-row>`, `<x-stack>`, `<x-grid>`, `<x-feed>`, `<x-surface>` and `<x-pane>`, whose `gap` and `padding` take a spacing token (`space200`) and whose `hide-below`, `hide-from` and `stack-below` take a window size class.
|
||||
- A fixed set of classes for text and interaction on plain elements: `md-type-*`, `md-ink-*`, `md-text-*`, `md-truncate`, `md-tabular`, `md-visually-hidden`, `md-state-layer`, `md-focus-ring`, `md-touch-target` and `md-link`.
|
||||
- The application's own CSS, named by the application, whose values are `--md-sys-*` custom properties.
|
||||
|
||||
@@ -99,14 +99,8 @@ Scheme::resolveProfileUsing(fn (): ?string => Setting::get('color_profile'));
|
||||
- Ink, from the same file: `md-ink` (on-surface), `md-ink-variant` (on-surface-variant: body copy beside a title, metadata), `md-ink-quiet` (outline: decoration only), `md-ink-primary`, `md-ink-error`, `md-ink-success`, `md-ink-warning`, `md-ink-info`, `md-ink-inverse`. On a container the ink is the container's `on-` role, which the component sets.
|
||||
- Text layout: `md-text-start|center|end`, `md-truncate`, `md-line-clamp-2|3`, `md-nowrap`, `md-tabular`, `md-mono`, `md-visually-hidden`. Monospace is for a value read or typed character by character (a token, a recovery code, a file name, a key): put it in `<code>`, `<kbd>` or `<samp>`, which take `--md-ref-typeface-mono` already, or give another element `md-mono` after its `md-type-*` class; `<x-input mono>` for a field. `--md-ref-typeface-mono` is the package's addition, the platform's monospace faces, not an M3 token.
|
||||
- Interaction, from `foundation/interaction.css`: `md-state-layer` (M3's hover, focus, press and dragged overlay in the content colour; makes the element `position: relative` and `isolation: isolate`; `data-md-dragged` draws the dragged layer), `md-focus-ring` (the keyboard focus indicator), `md-touch-target` (a 48px target around a smaller drawing), `md-link` (a link in running text, underlined; add `md-ink-primary` where it should read as primary).
|
||||
- **The application's own stylesheet** for the rest, on the tokens, never on a literal value:
|
||||
- Colour roles, `var(--md-sys-color-…)`: `primary`, `on-primary`, `primary-container`, `on-primary-container`, `primary-dim`, `inverse-primary`, `primary-fixed`, `primary-fixed-dim`, `on-primary-fixed`, `on-primary-fixed-variant`; the same for `secondary` and `tertiary`; `error`, `on-error`, `error-container`, `on-error-container`, `error-dim`; `success`, `warning` and `info` with their `on-`, `-container` and `on-…-container`; `inverse-error|success|warning|info`; `surface`, `surface-dim`, `surface-bright`, `surface-container-lowest|low||high|highest`, `on-surface`, `on-surface-variant`, `inverse-surface`, `inverse-on-surface`, `outline` (a boundary that must be read), `outline-variant` (a divider, a card's edge), `scrim`, `shadow`. There is no white or black.
|
||||
- Type: `font: var(--md-sys-typescale-body-md)` with `letter-spacing: var(--md-sys-typescale-body-md-tracking)`, never a size, weight or line height of your own. The typefaces are `--md-ref-typeface-brand` (Google Sans Flex), `--md-ref-typeface-plain` and `--md-ref-typeface-mono`; the weights `--md-ref-typeface-weight-regular|medium|bold`.
|
||||
- Shape: `var(--md-sys-shape-corner-{none|xs|sm|md|lg|lg-increased|xl|xl-increased|xxl|full})`.
|
||||
- Elevation: `box-shadow: var(--md-sys-elevation-{1…5})` — for what floats over content, not for panels (a panel separates by its container tone).
|
||||
- Motion: `var(--md-sys-motion-spatial-{fast|default|slow})` (position, size, shape; springs that overshoot) and `var(--md-sys-motion-effects-{fast|default|slow})` (colour, opacity, which never overshoot), each always with its own `-duration`: `transition: transform var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast)` — M3's published web durations, spatial 350/500/650 ms and effects 150/200/300 ms. `motion.scheme` in the config picks `expressive` (the default, with the bounce) or `standard` (minimal bounce), which the head script writes to `<html data-motion>` and which swaps the three spatial springs; a rule names a spring, never a scheme. Reduced motion zeroes every duration in both schemes.
|
||||
- Spacing: `var(--md-sys-measurement-space25)` … `var(--md-sys-measurement-space900)`.
|
||||
- States: `--md-sys-state-{hover|focus|pressed|dragged}-state-layer-opacity` and `--md-sys-state-disabled-{content|container}-opacity`, mixed into a role: `color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent)`. A scrim is `color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent)`.
|
||||
- **The application's own stylesheet** for the rest, on the tokens (`var(--md-sys-color-…)`, `-shape-corner-…`, `-elevation-…`, `-motion-spatial-…`/`-motion-effects-…` each with its own `-duration`, `-measurement-space…`, `-state-…-opacity`), never on a literal value — no white, black, hex, pixel radius, shadow, size, weight, line height or duration of your own. Which family names which role, corner, level, spring or space is the `material-3-design` skill's tables, not repeated here. `motion.scheme` in the config picks `expressive` (the default) or `standard`, written to `<html data-motion>`; a rule names a spring, never a scheme.
|
||||
- Names those tables leave out: the weights `--md-ref-typeface-weight-regular|medium|bold`; the state layers `--md-sys-state-{hover|focus|pressed|dragged}-state-layer-opacity` and `--md-sys-state-disabled-{content|container}-opacity`, mixed into a role with `color-mix()`; and the colour roles `inverse-error|success|warning|info` and `shadow`.
|
||||
|
||||
The rules behind these names — which role, surface container, corner, type style, elevation level, motion spring, state and window size class to use, and M3's don'ts — are the `material-3` guideline (always on) and the `material-3-design` skill (the tables and Google's source pages); activate that skill before designing a screen.
|
||||
|
||||
@@ -124,7 +118,12 @@ The rules behind these names — which role, surface container, corner, type sty
|
||||
}
|
||||
```
|
||||
|
||||
- Breakpoints are M3's window size classes, and only those: compact below 600px, medium 600px, expanded 840px, large 1200px, extra-large 1600px. A layout component takes them as props; your own CSS writes them as range media queries (`@media (width >= 840px)`, `@media (width < 600px)`), and the design guard rejects any other width; a script asks `resources/js/breakpoints.js` (`from('expanded')`, `upTo('medium')`), so a stylesheet and a script never disagree at the boundary pixel. A component's *own* width is a container query (`@container (width >= 28rem)`), which is a different thing.
|
||||
- Breakpoints are M3's window size classes, and only those (the guideline has the five numbers). A
|
||||
layout component takes them as props; your own CSS writes them as range media queries, and the
|
||||
design guard rejects any other width; a script asks `resources/js/breakpoints.js`
|
||||
(`from('expanded')`, `upTo('medium')`), so a stylesheet and a script never disagree at the
|
||||
boundary pixel. A component's *own* width is a container query (`@container (width >= 28rem)`),
|
||||
a different thing from a breakpoint.
|
||||
- The roles switch with the theme by themselves. A rule of your own that only applies in dark keys on `[data-theme='dark']` (the page's theme), never on `prefers-color-scheme` (the operating system's).
|
||||
- `x-figure` on an element holding one number counts it up on first appearance and on change.
|
||||
|
||||
@@ -1181,32 +1180,7 @@ it('uses only what compiles', function () {
|
||||
});
|
||||
```
|
||||
|
||||
It reads every path it is given and fails with `path:line` on unknown symbol names, Blade directives written inside a component tag (where they do not compile), and every Tailwind utility or variant — none compiles in a Tailwind-free application — or 1.x utility, each with its 2.0.0 replacement. A class the application's own stylesheets declare is exempt, and so is every `md-*` class:
|
||||
|
||||
| Written | Use |
|
||||
| --- | --- |
|
||||
| `sm:`, `md:`, `lg:`, `xl:`, `2xl:` (and `max-`) | `@media (width >= 600px)` …, or a layout component's `hide-below`/`hide-from`/`stack-below` prop |
|
||||
| `hover:`, `dark:`, `group-hover:`, `rtl:` … | `md-state-layer`/`md-focus-ring`, `[data-theme='dark']`, `:has()`, `:is([dir='rtl'], [dir='rtl'] *)` in your own CSS |
|
||||
| `flex`, `flex-col`, `flex-wrap`, `items-center`, `justify-between` | `<x-row>`, `<x-stack>`, `<x-row wrap>`, `align="center"`, `justify="between"` |
|
||||
| `grid`, `grid-cols-3` | `<x-grid :columns="3">` (or a per-breakpoint map), `<x-feed>` for cards |
|
||||
| `gap-4`, `space-y-4`, `p-4`, `mt-2` | the M3 step at 4px per unit: `gap="space200"` (16px), `<x-stack gap="space200">`, `<x-surface padding="space200">`, `var(--md-sys-measurement-space100)` |
|
||||
| `w-full`, `max-w-5xl`, `size-6` | `<x-pane width>`, `<x-icon size>`, or a length in your own CSS — M3 keeps no size scale |
|
||||
| `hidden` | a layout component's `hide-below`/`hide-from` prop, the `hidden` attribute, or `x-show` |
|
||||
| `block`, `relative`, `inset-0`, `z-10`, `overflow-hidden`, `shrink-0`, `border`, `opacity-50`, `cursor-pointer` … | the rule in your own CSS |
|
||||
| `text-center`, `truncate`, `sr-only`, `whitespace-nowrap`, `line-clamp-2`, `tabular-nums` | `md-text-center`, `md-truncate`, `md-visually-hidden`, `md-nowrap`, `md-line-clamp-2`, `md-tabular` |
|
||||
| `type-body-md`, `focus-ring`, `state-layer`, `link` (1.x) | `md-type-body-md`, `md-focus-ring`, `md-state-layer`, `md-link` |
|
||||
| `text-on-surface-variant`, `text-primary`, `text-meta` … | `md-ink-variant`, `md-ink-primary` … |
|
||||
| `border-outline-variant`, `border-divider` | `<x-divider>` or `<x-surface outlined>` |
|
||||
| `bg-surface-container` | `<x-surface level="surface-container">` |
|
||||
| `bg-primary`, `border-error`, `text-red-500` … | `var(--md-sys-color-*)` |
|
||||
| `bg-brand`, `text-sport-run`, `border-l-zone-4/40` … (a colour your own Tailwind theme named) | your own `var(--…)`, or `color-mix(in srgb, var(--…) 40%, transparent)` |
|
||||
| `bg-white`, `text-black` | a surface or `on-` role — M3 has no white or black |
|
||||
| `text-on-surface/60`, `bg-on-surface/8` | `md-ink-variant`/`md-ink-quiet`, or `md-state-layer` for an overlay |
|
||||
| `rounded-lg`, `rounded-corner-lg` (1.x) | `var(--md-sys-shape-corner-lg)`, or `<x-surface corner="lg">` |
|
||||
| `shadow-md`, `shadow-elevation-2` (1.x) | `var(--md-sys-elevation-2)` |
|
||||
| `text-sm`, `leading-6`, `tracking-wide`, `font-medium` | one of the `md-type-*`/`md-type-emphasized-*` classes |
|
||||
| `ease-in-out`, `ease-spatial-fast` (1.x), `duration-300` | `var(--md-sys-motion-spatial-fast)` with its `-duration`, in your own `transition` |
|
||||
| `bg-[#1d7afc]`, `w-[320px]`, `[--x:1]` | an M3 role, or the value in your own CSS |
|
||||
It reads every path it is given and fails with `path:line` on unknown symbol names, Blade directives written inside a component tag (where they do not compile), and every Tailwind utility or variant — none compiles in a Tailwind-free application — each match printed beside its own 2.0.0 replacement (a layout component and prop, an `md-*` class, a `var(--md-sys-…)` token, or your own CSS), so run the check and read its output rather than a table here. A class the application's own stylesheets declare is exempt, and so is every `md-*` class.
|
||||
|
||||
In the `.css` files it is given (`material-scheme.css` skipped) it fails on a literal colour, radius, shadow, font, font size, weight, line height, letter spacing, easing or duration, and on a media query width off 600/840/1200/1600px; a `var()`, or a `calc()`/`min()`/`max()`/`clamp()` built on one, is fine.
|
||||
|
||||
|
||||
@@ -271,32 +271,22 @@ Sources: https://m3.material.io/styles/icons/overview · https://m3.material.io/
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Native elements before ARIA: `<button>`, `<a href>`, `<dialog>`, `<input>`, `<select>`; a styled `div` that fakes one needs everything re-implemented and tested.
|
||||
- Landmarks: one `main`, one `banner`, one `contentinfo` per page; `nav`, `search`, `complementary`, `form`, `region` labelled when they repeat, never with their own role in the label ("Primary", not "Primary navigation").
|
||||
- Headings: one H1 for the page, then H2–H6 in order without skipping; the level is the document's structure, the `md-type-*` class is the appearance, and they need not match.
|
||||
- Names: an interactive icon, image or ambiguous button ("Save", "Learn more") has a name that says what it does, without the word "button"; decorative images are `alt=""` or `aria-hidden`; text only a screen reader needs is `md-visually-hidden`.
|
||||
- Focus: DOM order is reading order; a dialog moves focus to its first meaningful control and returns it to the opener; grouped controls are one Tab stop with arrows inside; a keyboard shortcut is two keys, or a single key only while its component is focused.
|
||||
- Announcements: an error is tied to its field (`aria-invalid`, `aria-describedby`) and announced; a snackbar is a polite live region that never steals focus and stays while it carries an action; a loading state has a name.
|
||||
- Contrast and states: 4.5:1 text, 3:1 large text and icons and grouped controls, disabled exempt; every state has two indicators; targets 48px with 8px between; text scales to 200%; motion honours reduced motion.
|
||||
The guideline's own Accessibility line has the rule; beyond it: every repeated landmark —
|
||||
`search`, `complementary`, `form`, `region`, not just `nav` — is labelled the same way; an
|
||||
ambiguous button ("Save", "Learn more") needs a name that says what it does, not just what kind
|
||||
of control it is; DOM order is reading order, a dialog returns focus to its opener, and a group of
|
||||
related controls is one Tab stop with the arrows moving inside it; an invalid field also carries
|
||||
`aria-invalid`, and a loading state has a name too.
|
||||
|
||||
Sources: https://m3.material.io/foundations/overview/principles · https://m3.material.io/foundations/designing/structure · https://m3.material.io/foundations/designing/flow · https://m3.material.io/foundations/designing/elements · https://m3.material.io/foundations/overview/assistive-technology
|
||||
|
||||
## Don'ts
|
||||
|
||||
What Google's pages say not to do, and this library follows:
|
||||
|
||||
| Don't | Because | Instead |
|
||||
| --- | --- | --- |
|
||||
| Put an icon in a snackbar | a snackbar is a short message with at most one action | `<x-toast>` types choose the announcement, not a picture |
|
||||
| Disable a FAB | "if the action is unavailable, the FAB shouldn't appear" | hide it |
|
||||
| Lay radio buttons in a row | a row reads as one control | a vertical group; chips or a connected button group for a horizontal choice |
|
||||
| Morph a card's corners on hover | shape morphs mark a press or a selection | the state layer and one level of elevation |
|
||||
| Use `outline` on a divider | dividers carry no contrast requirement and read too heavy | `<x-divider>` (`outline-variant`) |
|
||||
| Use a hex, white, black or an opacity for ink | it ignores theme, contrast level and profile | a role |
|
||||
| Write a utility class, or a breakpoint, radius, shadow, type size or easing of your own | nothing defines utility classes, and other values are not M3's | the layout components' props, `md-type-*` and `md-ink-*`, and `--md-sys-*` tokens at 600/840/1200/1600px in your own CSS |
|
||||
| Use segmented buttons, a navigation drawer or a bottom app bar | deprecated in M3 Expressive | `<x-button-group connected>`, the expanded rail, `<x-toolbar>` |
|
||||
| Truncate without a way to read the rest | an ellipsis alone is not accessible | wrap, grow the container, or a tooltip |
|
||||
| Animate with a literal duration | it ignores reduced motion | the paired tokens |
|
||||
The guideline's Don'ts, Type and Motion bullets name them; where they name no replacement — a vertical
|
||||
group or chips for radios in a row, `<x-divider>` for the outline case, the `md-type-*`/`md-ink-*`
|
||||
classes and `--md-sys-*` tokens for the utility-class case, wrap/grow/a tooltip instead of a bare
|
||||
ellipsis, the paired motion tokens instead of a literal duration — the components and layout
|
||||
sections above have it.
|
||||
|
||||
## Attribution
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Livewire Material for an application that wants everything: the foundation, every layout
|
||||
* stylesheet and every component stylesheet, each imported once. The blocks below are the same
|
||||
* groups `components.css` and `layout.css` used before this file replaced both of them (plan step
|
||||
* 37) — one block per audit stream, so a stylesheet still lands in a predictable place — plus the
|
||||
* 37) — one block per group, so a stylesheet still lands in a predictable place — plus the
|
||||
* Layout block those two files didn't share.
|
||||
*
|
||||
* This is not the recommended way to bring the package's CSS in: importing `all.css` pulls in
|
||||
@@ -38,6 +38,7 @@
|
||||
/* Foundation components */
|
||||
@import './components/icon.css';
|
||||
@import './components/shape.css';
|
||||
@import './components/color.css';
|
||||
|
||||
/* Actions and communication */
|
||||
@import './components/loading.css';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/*
|
||||
* <x-account-menu>: the avatar trigger of an `<x-menu>`, its name-and-email header and its theme
|
||||
* item — an M3 "extra" the audit found already inside the foundations (docs/audits/m3-alignment/
|
||||
* navigation.md): a 40px trigger reaching M3's 48px target through `md-touch-target` (N-01), with
|
||||
* item — not one of M3's own catalogued components, so its states are judged against the general
|
||||
* foundations instead: a 40px trigger reaching M3's 48px target through `md-touch-target`, with
|
||||
* `md-state-layer` for the hover and pressed feedback every other trigger in the library already
|
||||
* has (N-15) and `md-focus-ring` for its keyboard ring. The avatar image sits behind the state
|
||||
* has and `md-focus-ring` for its keyboard ring. The avatar image sits behind the state
|
||||
* layer's `::before` (z-index -1, foundation/interaction.css) at z-index -2, so a hover or a press
|
||||
* still washes over the picture instead of under it; nothing clips the pseudo-target the touch
|
||||
* class draws past the 40px circle.
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
* neutral (surface-container-high, on-surface — it has no state colour of its own). The dismiss
|
||||
* button is drawn at 40px, pulled 8px out into the alert's padding, and renders the foundation's
|
||||
* `md-state-layer`, `md-focus-ring` and `md-touch-target` (foundation/interaction.css) to reach
|
||||
* M3's 48px target (ACT-18); nothing here copies their rules.
|
||||
* M3's 48px target; nothing here copies their rules.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
@import './color.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-alert] {
|
||||
@@ -23,38 +24,8 @@
|
||||
gap: 12px;
|
||||
border-radius: var(--md-sys-shape-corner-md);
|
||||
padding: var(--md-sys-measurement-space200);
|
||||
background-color: var(--md-sys-color-info-container);
|
||||
color: var(--md-sys-color-on-info-container);
|
||||
}
|
||||
|
||||
[data-md-alert][data-md-color='success'] {
|
||||
background-color: var(--md-sys-color-success-container);
|
||||
color: var(--md-sys-color-on-success-container);
|
||||
}
|
||||
|
||||
[data-md-alert][data-md-color='warning'] {
|
||||
background-color: var(--md-sys-color-warning-container);
|
||||
color: var(--md-sys-color-on-warning-container);
|
||||
}
|
||||
|
||||
[data-md-alert][data-md-color='error'] {
|
||||
background-color: var(--md-sys-color-error-container);
|
||||
color: var(--md-sys-color-on-error-container);
|
||||
}
|
||||
|
||||
[data-md-alert][data-md-color='primary'] {
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
[data-md-alert][data-md-color='secondary'] {
|
||||
background-color: var(--md-sys-color-secondary-container);
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
[data-md-alert][data-md-color='tertiary'] {
|
||||
background-color: var(--md-sys-color-tertiary-container);
|
||||
color: var(--md-sys-color-on-tertiary-container);
|
||||
background-color: var(--md-container, var(--md-sys-color-info-container));
|
||||
color: var(--md-on-container, var(--md-sys-color-on-info-container));
|
||||
}
|
||||
|
||||
[data-md-alert][data-md-color='neutral'] {
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
* small 64px, title-large title (label-medium subtitle), 4px from the edges, the title 16px
|
||||
* from the start or straight after the navigation icon
|
||||
* center the same, the title centred — a three-column grid, so it shrinks against whatever is
|
||||
* beside it rather than ellipsising underneath it (N-11: a fixed inset fit exactly one
|
||||
* of M3's two trailing buttons)
|
||||
* beside it rather than ellipsising underneath it: a fixed inset fit exactly one
|
||||
* of M3's two trailing buttons
|
||||
* medium 112px (136px with a subtitle): the 64px row of icons over a headline-medium title
|
||||
* large 120px (152px with a subtitle): a display-small title
|
||||
* search the row holds a search bar, full width to 312px and then half of what is left (N-09)
|
||||
* search the row holds a search bar, full width to 312px and then half of what is left
|
||||
*
|
||||
* The container is the surface, and surface-container once content scrolls under it. A medium or
|
||||
* large bar collapses into the small one without script moving anything: the bar is sticky at a
|
||||
@@ -169,7 +169,7 @@
|
||||
|
||||
/* Centred: a three-column row — leading, headline, trailing — so the headline is centred in the
|
||||
window and shrinks against whatever is actually beside it. M3 allows two trailing icon
|
||||
buttons, which a fixed inset for one would have let a long title ellipsise underneath (N-11). */
|
||||
buttons, which a fixed inset for one would have let a long title ellipsise underneath. */
|
||||
[data-md-app-bar][data-md-variant='center'] [data-md-app-bar-row] {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
@@ -196,7 +196,7 @@
|
||||
}
|
||||
|
||||
/* The search bar fills the space between the leading and trailing elements until it is 312dp
|
||||
wide, and then grows into only half of what is left (N-09). The percentage is of the row
|
||||
wide, and then grows into only half of what is left. The percentage is of the row
|
||||
rather than of that space, which CSS cannot name without measuring it; below 312px the cap
|
||||
never binds, so a phone still gets the whole row. */
|
||||
[data-md-app-bar-search] {
|
||||
|
||||
@@ -20,12 +20,14 @@
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './color.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-badge] {
|
||||
--md-badge-color: var(--md-sys-color-error);
|
||||
--md-badge-on-color: var(--md-sys-color-on-error);
|
||||
--md-badge-container: var(--md-sys-color-error-container);
|
||||
--md-badge-on-container: var(--md-sys-color-on-error-container);
|
||||
--md-badge-color: var(--md-color, var(--md-sys-color-error));
|
||||
--md-badge-on-color: var(--md-on-color, var(--md-sys-color-on-error));
|
||||
--md-badge-container: var(--md-container, var(--md-sys-color-error-container));
|
||||
--md-badge-on-container: var(--md-on-container, var(--md-sys-color-on-error-container));
|
||||
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
@@ -65,48 +67,6 @@
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-color='primary'] {
|
||||
--md-badge-color: var(--md-sys-color-primary);
|
||||
--md-badge-on-color: var(--md-sys-color-on-primary);
|
||||
--md-badge-container: var(--md-sys-color-primary-container);
|
||||
--md-badge-on-container: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-color='secondary'] {
|
||||
--md-badge-color: var(--md-sys-color-secondary);
|
||||
--md-badge-on-color: var(--md-sys-color-on-secondary);
|
||||
--md-badge-container: var(--md-sys-color-secondary-container);
|
||||
--md-badge-on-container: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-color='tertiary'] {
|
||||
--md-badge-color: var(--md-sys-color-tertiary);
|
||||
--md-badge-on-color: var(--md-sys-color-on-tertiary);
|
||||
--md-badge-container: var(--md-sys-color-tertiary-container);
|
||||
--md-badge-on-container: var(--md-sys-color-on-tertiary-container);
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-color='success'] {
|
||||
--md-badge-color: var(--md-sys-color-success);
|
||||
--md-badge-on-color: var(--md-sys-color-on-success);
|
||||
--md-badge-container: var(--md-sys-color-success-container);
|
||||
--md-badge-on-container: var(--md-sys-color-on-success-container);
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-color='warning'] {
|
||||
--md-badge-color: var(--md-sys-color-warning);
|
||||
--md-badge-on-color: var(--md-sys-color-on-warning);
|
||||
--md-badge-container: var(--md-sys-color-warning-container);
|
||||
--md-badge-on-container: var(--md-sys-color-on-warning-container);
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-color='info'] {
|
||||
--md-badge-color: var(--md-sys-color-info);
|
||||
--md-badge-on-color: var(--md-sys-color-on-info);
|
||||
--md-badge-container: var(--md-sys-color-info-container);
|
||||
--md-badge-on-container: var(--md-sys-color-on-info-container);
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-color='neutral'] {
|
||||
--md-badge-color: var(--md-sys-color-on-surface-variant);
|
||||
--md-badge-on-color: var(--md-sys-color-surface);
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
* [data-md-bottom-sheet-probe] hidden, measures a stop's px height for a drag to settle on
|
||||
* [data-md-bottom-sheet-panel] surface-container-low, extra-large top corners, elevation 1;
|
||||
* data-md-preset while `heights`/`snap` gives it stops
|
||||
* [data-md-bottom-sheet-handle] the drag target and M3's 48dp hit area (C-01)
|
||||
* [data-md-bottom-sheet-handle] the drag target and M3's 48dp hit area
|
||||
* [data-md-bottom-sheet-grip] the 32×4px bar, the shared classes' touch target
|
||||
* [data-md-bottom-sheet-announce] a live region, only while stops exist
|
||||
* [data-md-bottom-sheet-body], [data-md-bottom-sheet-title], [data-md-bottom-sheet-actions]
|
||||
*
|
||||
* 640px max width, centred; 28px top corners only (`DockedContainerShape` = CornerExtraLargeTop);
|
||||
* 50dvh default open height under a ceiling of the screen less M3's 72dp top margin (C-19). It
|
||||
* 50dvh default open height under a ceiling of the screen less M3's 72dp top margin. It
|
||||
* rises on emphasized decelerate and only falls on emphasized accelerate — the asymmetric timing a
|
||||
* more specific selector for the open state supplies, since a CSS transition always takes its
|
||||
* duration and easing from the *after-change* style (the technique resources/css/components/
|
||||
|
||||
@@ -143,10 +143,7 @@
|
||||
[data-md-button-group='connected'] > * {
|
||||
--md-button-group-corner: var(--md-button-group-inner);
|
||||
|
||||
border-start-start-radius: var(--md-button-group-corner);
|
||||
border-end-start-radius: var(--md-button-group-corner);
|
||||
border-start-end-radius: var(--md-button-group-corner);
|
||||
border-end-end-radius: var(--md-button-group-corner);
|
||||
border-radius: var(--md-button-group-corner);
|
||||
}
|
||||
|
||||
[data-md-button-group='connected'] > :active {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* lg 96px 48px 12px headline-sm 32 / 32px 2px xl lg
|
||||
* xl 136px 64px 16px headline-lg 40 / 40px 3px xl lg
|
||||
*
|
||||
* (ButtonXSmall…XLargeTokens; the xs padding is the 16px Phase D settled on, ACT-09.) An icon button
|
||||
* (ButtonXSmall…XLargeTokens; the xs padding is the 16px Phase D settled on.) An icon button
|
||||
* (`data-md-icon-button`) is as tall and `narrow`, `default` or `wide` across by
|
||||
* XSmall…XLargeIconButtonTokens: 28/32/40, 32/40/52, 48/56/72, 64/96/128 and 104/136/184px. The
|
||||
* views size the glyphs through `<x-icon size>`; `--md-button-icon` carries the same size for the
|
||||
@@ -30,7 +30,7 @@
|
||||
* plain declaration. Primary's tonal button is secondary-container and its standard icon button
|
||||
* on-surface-variant, as M3 draws them; the text button's label is primary, as Compose draws it,
|
||||
* not the token's on-surface-variant, which its own source marks as wrong; a selected text button
|
||||
* takes the tonal container (the library's deliberate deviations, docs/audits/m3-alignment).
|
||||
* takes the tonal container (a deliberate deviation from the token).
|
||||
*
|
||||
* Motion: corners, padding, margins and `bottom` on the fast spatial spring, colour and shadow on
|
||||
* the fast effects spring beside it — a colour must never overshoot (tokens/motion.css).
|
||||
@@ -54,13 +54,14 @@
|
||||
@import './icon.css';
|
||||
@import './loading.css';
|
||||
@import './tooltip.css';
|
||||
@import './color.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-button] {
|
||||
--md-button-color: var(--md-sys-color-primary);
|
||||
--md-button-on-color: var(--md-sys-color-on-primary);
|
||||
--md-button-tone: var(--md-sys-color-secondary-container);
|
||||
--md-button-on-tone: var(--md-sys-color-on-secondary-container);
|
||||
--md-button-color: var(--md-color, var(--md-sys-color-primary));
|
||||
--md-button-on-color: var(--md-on-color, var(--md-sys-color-on-primary));
|
||||
--md-button-tone: var(--md-container, var(--md-sys-color-secondary-container));
|
||||
--md-button-on-tone: var(--md-on-container, var(--md-sys-color-on-secondary-container));
|
||||
--md-button-tone-selected: var(--md-sys-color-secondary);
|
||||
--md-button-on-tone-selected: var(--md-sys-color-on-secondary);
|
||||
--md-button-quiet: var(--md-sys-color-on-surface-variant);
|
||||
@@ -223,49 +224,14 @@
|
||||
border-radius: var(--md-button-pressed);
|
||||
}
|
||||
|
||||
/* Colour roles. */
|
||||
[data-md-button][data-md-color='secondary'] {
|
||||
--md-button-color: var(--md-sys-color-secondary);
|
||||
--md-button-on-color: var(--md-sys-color-on-secondary);
|
||||
/* Colour roles: every hue but primary comes straight off the shared table (color.css). Its
|
||||
`primary` entry is plain primary-container, so the one hue this file draws differently keeps
|
||||
a small override of its own. */
|
||||
[data-md-button][data-md-color='primary'] {
|
||||
--md-button-tone: var(--md-sys-color-secondary-container);
|
||||
--md-button-on-tone: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
[data-md-button][data-md-color='tertiary'] {
|
||||
--md-button-color: var(--md-sys-color-tertiary);
|
||||
--md-button-on-color: var(--md-sys-color-on-tertiary);
|
||||
--md-button-tone: var(--md-sys-color-tertiary-container);
|
||||
--md-button-on-tone: var(--md-sys-color-on-tertiary-container);
|
||||
}
|
||||
|
||||
[data-md-button][data-md-color='error'] {
|
||||
--md-button-color: var(--md-sys-color-error);
|
||||
--md-button-on-color: var(--md-sys-color-on-error);
|
||||
--md-button-tone: var(--md-sys-color-error-container);
|
||||
--md-button-on-tone: var(--md-sys-color-on-error-container);
|
||||
}
|
||||
|
||||
[data-md-button][data-md-color='success'] {
|
||||
--md-button-color: var(--md-sys-color-success);
|
||||
--md-button-on-color: var(--md-sys-color-on-success);
|
||||
--md-button-tone: var(--md-sys-color-success-container);
|
||||
--md-button-on-tone: var(--md-sys-color-on-success-container);
|
||||
}
|
||||
|
||||
[data-md-button][data-md-color='warning'] {
|
||||
--md-button-color: var(--md-sys-color-warning);
|
||||
--md-button-on-color: var(--md-sys-color-on-warning);
|
||||
--md-button-tone: var(--md-sys-color-warning-container);
|
||||
--md-button-on-tone: var(--md-sys-color-on-warning-container);
|
||||
}
|
||||
|
||||
[data-md-button][data-md-color='info'] {
|
||||
--md-button-color: var(--md-sys-color-info);
|
||||
--md-button-on-color: var(--md-sys-color-on-info);
|
||||
--md-button-tone: var(--md-sys-color-info-container);
|
||||
--md-button-on-tone: var(--md-sys-color-on-info-container);
|
||||
}
|
||||
|
||||
/* Every colour but primary selects into the colour itself and is quiet in it. */
|
||||
[data-md-button]:not([data-md-color='primary']) {
|
||||
--md-button-tone-selected: var(--md-button-color);
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
* OutlinedCardTokens.kt, androidx Compose Material 3, Apache-2.0; docs/reference/m3
|
||||
* § Cards → Specs) — filled (surface-container-highest), elevated (surface-container-low at
|
||||
* elevation 1) and outlined (surface, an outline-variant edge); one 12dp corner for all three,
|
||||
* which never morphs (M3 specifies shape morph for buttons, FABs and list items, never cards —
|
||||
* docs/audits/m3-alignment/containment.md, C-23).
|
||||
* which never morphs (M3 specifies shape morph for buttons, FABs and list items, never cards).
|
||||
*
|
||||
* Every card renders the shared `md-state-layer` class, and a row (`data-md-list-row`,
|
||||
* `resources/js/list-rows.js`) `md-focus-ring` too (foundation/interaction.css), for its tint and
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
/*
|
||||
* <x-carousel-item>: one slide of an <x-carousel>, masked and shifted by the keyline maths
|
||||
* carousel.css's header describes, unless the row is full-screen or multi-aspect, where nothing
|
||||
* masks it at all (docs/reference/m3/components-actions-communication-containment.md § Carousel;
|
||||
* docs/audits/m3-alignment/containment.md, C-11, C-12, C-25).
|
||||
* masks it at all (docs/reference/m3/components-actions-communication-containment.md § Carousel).
|
||||
*
|
||||
* [data-md-carousel-item] the tab stop (C-18), a focusable `group`, `md-focus-ring`
|
||||
* [data-md-carousel-item] the tab stop, a focusable `group`, `md-focus-ring`
|
||||
* [data-md-carousel-surface] the masked, shifted, extra-large-corner (28px) box
|
||||
* [data-md-carousel-content] the slot (an <img>, cropped)
|
||||
* [data-md-carousel-label] the `label` prop's words, over a scrim gradient
|
||||
@@ -26,7 +25,7 @@
|
||||
* The label's ink is a literal `white`, not a role: it sits over `--md-sys-color-scrim`, which is
|
||||
* black in every scheme, theme and contrast level, so white is the one ink that always has the
|
||||
* scrim's contrast — a role (`inverse-on-surface`) is dark ink in a dark scheme and fails there
|
||||
* (C-25, deliberately kept, as the component's original header explained).
|
||||
* (deliberately kept).
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* and sizes as inline custom properties every scroll frame; this file draws everything the script
|
||||
* does not — the row's layout per layout, and the controls — and carousel-item.css draws the
|
||||
* item and its mask (docs/reference/m3/components-actions-communication-containment.md
|
||||
* § Carousel; docs/audits/m3-alignment/containment.md).
|
||||
* § Carousel).
|
||||
*
|
||||
* [data-md-carousel] the row wrapper; its value is the layout
|
||||
* [data-md-carousel-probe] hidden, measures the preferred item width for the script
|
||||
@@ -17,12 +17,12 @@
|
||||
* [data-md-carousel-previous], [data-md-carousel-next]
|
||||
*
|
||||
* Specs table: 16dp leading/trailing padding, 8dp top/bottom and between items, for every layout
|
||||
* but uncontained (leading only) and full-screen (0, edge to edge — C-12); a `>` combinator
|
||||
* but uncontained (leading only) and full-screen (0, edge to edge); a `>` combinator
|
||||
* throughout, never a bare descendant one, because a carousel item can itself hold a nested
|
||||
* carousel (a `multi-aspect` gallery inside a `hero` slide) whose own root would otherwise match
|
||||
* its parent's rules too. Full-screen scrolls vertically, one edge-to-edge item at a time, capped
|
||||
* at the 840px medium window it is meant for (C-11); reduced motion needs no rule here at all —
|
||||
* the script writes zero inset, shift and full opacity itself for every layout (C-05).
|
||||
* at the 840px medium window it is meant for; reduced motion needs no rule here at all —
|
||||
* the script writes zero inset, shift and full opacity itself for every layout.
|
||||
*
|
||||
* The previous/next icons mirror in RTL by scaling the whole button, as the row's arrow keys and
|
||||
* scroll direction already do (`resources/js/carousel.js`'s own `state.rtl`), rather than through
|
||||
@@ -53,10 +53,6 @@
|
||||
[data-md-carousel] > [data-md-carousel-scroller] {
|
||||
display: flex;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-carousel='full-screen'] > [data-md-carousel-scroller] {
|
||||
|
||||
@@ -76,55 +76,28 @@
|
||||
|
||||
/* ---- Assist, filter and suggestion: the chip is the control ------------------------------ */
|
||||
|
||||
/* The state layer and the 48px target are the foundation's `md-state-layer md-touch-target`
|
||||
classes now (chip.blade.php, as group and scheme-picker's own controls render them;
|
||||
foundation/interaction.css), at the same opacities and off the same `currentColor` this
|
||||
chip always painted them from. Three extras stay chip's own: a filter chip's label is never
|
||||
itself `:focus-visible` — the checkbox inside it is, caught with `:has()`, the same way the
|
||||
ring below already had to; `data-md-disabled` marks a chip with no native `disabled` to
|
||||
catch (a link, or a toggle button with no press of its own). */
|
||||
[data-md-chip]:not([data-md-chip='input']) {
|
||||
isolation: isolate;
|
||||
padding-inline: 15px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
|
||||
/* The 48px target, past the chip's 32px. */
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
top: 50%;
|
||||
height: var(--md-sys-measurement-space600);
|
||||
translate: 0 -50%;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
border-radius: inherit;
|
||||
background-color: currentColor;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover::before {
|
||||
opacity: var(--md-sys-state-hover-state-layer-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible::before,
|
||||
&:has(:focus-visible)::before {
|
||||
opacity: var(--md-sys-state-focus-state-layer-opacity);
|
||||
}
|
||||
|
||||
&:active::before {
|
||||
opacity: var(--md-sys-state-pressed-state-layer-opacity);
|
||||
}
|
||||
|
||||
&:is(:focus-visible, :has(:focus-visible)) {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:is(:disabled, [aria-disabled='true'], [data-md-disabled])::before {
|
||||
&[data-md-disabled]::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* <x-collapse>: not an M3 component — M3 has expandable list items and menu expansion but no
|
||||
* standalone disclosure — built on the native `<details>` so it opens without script, keeps its
|
||||
* state through a Livewire morph, and is announced as a disclosure by the browser. Judged against
|
||||
* foundations rather than any M3 component spec (docs/audits/m3-alignment/containment.md, C-24).
|
||||
* foundations rather than any M3 component spec, since M3 has no standalone disclosure.
|
||||
*
|
||||
* The height eases open and shut on the fast spatial spring, the same one that turns the chevron,
|
||||
* from resources/js/collapse.js: it animates the `<details>`' own `block-size`. This file cannot —
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* The colour-role table M3's semantic colours share, so a component that offers the same set of
|
||||
* roles does not repeat it: `data-md-color` picks primary (most components' default, drawn
|
||||
* without the attribute), secondary, tertiary, error, success, warning or info, and this sets the
|
||||
* four generic variables a component's own stylesheet reads — `--md-color`/`--md-on-color` the
|
||||
* role itself, `--md-container`/`--md-on-container` its tonal container.
|
||||
*
|
||||
* A component gives its own prefixed variables (`--md-badge-color`, …) a fallback of its default
|
||||
* role, `var(--md-color, var(--md-sys-color-<default>))`, so the generic ones only take over once
|
||||
* `data-md-color` is actually written on that element. The four are registered as not inheriting,
|
||||
* so a component inside a coloured one (a badge on an error button) still falls back to its own
|
||||
* default rather than taking its ancestor's role. A component whose primary
|
||||
* deliberately reads secondary-container instead of primary-container (button.css, slider.css,
|
||||
* progress.css) keeps a small override of its own for an explicit `data-md-color="primary"` too:
|
||||
* this file's `primary` entry is the plain M3 mapping every other role, and every other component,
|
||||
* wants.
|
||||
*
|
||||
* A component that supports only part of this set (fab.css's primary/secondary/tertiary) does not
|
||||
* import this file: wiring it in would make every role it leaves out suddenly take effect through
|
||||
* the bare `[data-md-color]` selector below, a behaviour change rather than a rewrite.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@layer material.components {
|
||||
@property --md-color {
|
||||
syntax: '*';
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
@property --md-on-color {
|
||||
syntax: '*';
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
@property --md-container {
|
||||
syntax: '*';
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
@property --md-on-container {
|
||||
syntax: '*';
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
[data-md-color='primary'] {
|
||||
--md-color: var(--md-sys-color-primary);
|
||||
--md-on-color: var(--md-sys-color-on-primary);
|
||||
--md-container: var(--md-sys-color-primary-container);
|
||||
--md-on-container: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
[data-md-color='secondary'] {
|
||||
--md-color: var(--md-sys-color-secondary);
|
||||
--md-on-color: var(--md-sys-color-on-secondary);
|
||||
--md-container: var(--md-sys-color-secondary-container);
|
||||
--md-on-container: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
[data-md-color='tertiary'] {
|
||||
--md-color: var(--md-sys-color-tertiary);
|
||||
--md-on-color: var(--md-sys-color-on-tertiary);
|
||||
--md-container: var(--md-sys-color-tertiary-container);
|
||||
--md-on-container: var(--md-sys-color-on-tertiary-container);
|
||||
}
|
||||
|
||||
[data-md-color='error'] {
|
||||
--md-color: var(--md-sys-color-error);
|
||||
--md-on-color: var(--md-sys-color-on-error);
|
||||
--md-container: var(--md-sys-color-error-container);
|
||||
--md-on-container: var(--md-sys-color-on-error-container);
|
||||
}
|
||||
|
||||
[data-md-color='success'] {
|
||||
--md-color: var(--md-sys-color-success);
|
||||
--md-on-color: var(--md-sys-color-on-success);
|
||||
--md-container: var(--md-sys-color-success-container);
|
||||
--md-on-container: var(--md-sys-color-on-success-container);
|
||||
}
|
||||
|
||||
[data-md-color='warning'] {
|
||||
--md-color: var(--md-sys-color-warning);
|
||||
--md-on-color: var(--md-sys-color-on-warning);
|
||||
--md-container: var(--md-sys-color-warning-container);
|
||||
--md-on-container: var(--md-sys-color-on-warning-container);
|
||||
}
|
||||
|
||||
[data-md-color='info'] {
|
||||
--md-color: var(--md-sys-color-info);
|
||||
--md-on-color: var(--md-sys-color-on-info);
|
||||
--md-container: var(--md-sys-color-info-container);
|
||||
--md-on-container: var(--md-sys-color-on-info-container);
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@
|
||||
@import './field.css';
|
||||
@import './icon.css';
|
||||
@import './button.css';
|
||||
@import './modal.css';
|
||||
|
||||
@layer material.components {
|
||||
/* ---- The support line under the field ------------------------------------------------------ */
|
||||
@@ -112,25 +113,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* The entry pop and the scrim are modal.css's shared dialog chrome, imported above. */
|
||||
[data-md-datepicker-picker]:modal {
|
||||
inset: 0;
|
||||
margin: auto;
|
||||
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-datepicker-picker]:modal {
|
||||
opacity: 0;
|
||||
scale: 0.95;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-datepicker-picker]:modal::backdrop {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent);
|
||||
}
|
||||
|
||||
[data-md-datepicker-surface] {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* [data-md-drawer-subtitle], [data-md-drawer-close]
|
||||
* [data-md-drawer-body] a size container, so its contents lay out by the sheet's own
|
||||
* width (`@md:`), never the viewport's
|
||||
* [data-md-drawer-actions] left-aligned, 72dp (C-10 — a dialog's are trailing-aligned)
|
||||
* [data-md-drawer-actions] left-aligned, 72dp — a dialog's are trailing-aligned
|
||||
*
|
||||
* 400px cap, 24dp start/end padding, a large corner on the inner edge only — from the specs
|
||||
* table. It enters on the emphasized-decelerate easing rather than a spring, because a sheet
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
* as the old `font-sans` was; only the fallback, which has no `@font-face`, uses a system stack.
|
||||
*
|
||||
* Colours from the roles (`docs/reference/m3/styles.md` § Typography, "Accessibility
|
||||
* requirements": surface/on-surface); the display code over its primary-container shape is the
|
||||
* pairing `docs/audits/m3-alignment/containment.md`'s "Aligned" section confirms; 24px page
|
||||
* gutters and the 600px growth of the art and headline are the same numbers the audit found
|
||||
* clean. `--md-sys-typescale-emphasized-display-lg` is the code's weight and roundedness; the
|
||||
* shape's 60s linear turn has no motion token (`--md-sys-motion-*` pairs a duration with a spring
|
||||
* for a state change, not an ambient loop) and stays a literal, gated the way the audit's
|
||||
* "Aligned" section already found it: behind `prefers-reduced-motion: no-preference`.
|
||||
* requirements": surface/on-surface); the display code over its primary-container shape is a
|
||||
* correct on-primary-container/primary-container contrast pairing; 24px page gutters and the
|
||||
* 600px growth of the art and headline follow the same spacing rules as every other M3 surface in
|
||||
* the library. `--md-sys-typescale-emphasized-display-lg` is the code's weight and roundedness;
|
||||
* the shape's 60s linear turn has no motion token (`--md-sys-motion-*` pairs a duration with a
|
||||
* spring for a state change, not an ambient loop) and stays a literal, gated behind
|
||||
* `prefers-reduced-motion: no-preference` like every other ambient animation in the library.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
*
|
||||
* The entry is `@starting-style` on the item itself; the exit is the popover's exit copy
|
||||
* (resources/js/popover-exit.js), whose items fab-menu.css sets to the closed values, so the item
|
||||
* has somewhere to animate to (ACT-33).
|
||||
* has somewhere to animate to.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@@ -4,17 +4,17 @@
|
||||
* FabMenuBaselineTokens (androidx Compose Material 3, Apache-2.0): the trigger is a 56px FAB,
|
||||
* `data-md-color`'s container, large corner, elevation 3, rising to elevation 4 under a hovering
|
||||
* pointer; while its list is open (`aria-expanded`) it turns fully round and paints the colour
|
||||
* itself, on the default spatial and effects springs (ACT-19 splits the two). It renders the
|
||||
* itself, on the default spatial and effects springs (split between the two). It renders the
|
||||
* foundation's `md-state-layer` and `md-focus-ring` (foundation/interaction.css); its 56px already
|
||||
* meets M3's target, so it needs no `md-touch-target`. The list sits
|
||||
* `data-md-position`'s corner of the trigger, 8px padding — the room the close button keeps below
|
||||
* the first item — and scrolls past `calc(100dvh - 128px)` on a short window, behind the close
|
||||
* button, which stays fixed (ACT-32).
|
||||
* button, which stays fixed.
|
||||
*
|
||||
* The items (fab-menu-item.css) rise into place as the list opens and sink back as it closes: the
|
||||
* entry is `@starting-style` on the item itself; the exit is the popover's exit copy
|
||||
* (`data-md-popover-exit`, resources/js/popover-exit.js), which stays on screen after the popover
|
||||
* has closed, so the items have somewhere to animate to (ACT-33) in every engine, Firefox included.
|
||||
* has closed, so the items have somewhere to animate to in every engine, Firefox included.
|
||||
* `[data-md-fab-menu-popover]:not(:popover-open) > *` and the copy's `[data-md-popover-closing] > *`
|
||||
* are the closed values; inside the layer they beat fab-menu-item.css's resting `translate: 0 0`
|
||||
* and `opacity: 1` on specificity — two attributes, or an attribute and a pseudo-class, against the
|
||||
|
||||
@@ -126,11 +126,6 @@
|
||||
font-family: var(--md-ref-typeface-mono);
|
||||
}
|
||||
|
||||
[data-md-field-control]::placeholder {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Autofill paints its own background, which on a card is a pale block in the
|
||||
wrong colour. Delaying the transition for a week keeps the field's own. */
|
||||
[data-md-field-control]:-webkit-autofill {
|
||||
|
||||
@@ -97,18 +97,9 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* The radio or checkbox stays in the page, for the form, the keyboard and a screen reader. */
|
||||
& > input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
/* The radio or checkbox stays in the page, for the form, the keyboard and a screen reader
|
||||
— `md-visually-hidden` on the element itself (text.css), which outranks this layer
|
||||
whatever wins the specificity, `material.text` sitting above `material.components`. */
|
||||
}
|
||||
|
||||
[data-md-group][data-md-inline] [data-md-group-segment] {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Material 3, Apache-2.0) — the tallest element sets the height, and a three-line item top-aligns
|
||||
* rather than centring (docs/reference/m3/components-actions-communication-containment.md
|
||||
* § Lists → Specs). Leading/trailing gap and container padding are both `space200` (16px,
|
||||
* `ItemLeadingSpace`/`ItemTrailingSpace`/container padding, C-14).
|
||||
* `ItemLeadingSpace`/`ItemTrailingSpace`/container padding).
|
||||
*
|
||||
* `data-md-list-row` (`resources/js/list-rows.js`, shared with `<x-card>` and an application's own
|
||||
* table rows) and `[data-md-list-open]:focus-visible` answer with the state layer — the row's ink at
|
||||
@@ -12,25 +12,25 @@
|
||||
* `[data-md-card]`, which draws its own tint through the shared `md-state-layer` class instead
|
||||
* (card.css). The layer is mixed over the row's own fill, `--md-list-row-fill` (a segmented tile's
|
||||
* surface, a selected item's secondary-container, table.css's selected row), so a hovered, pressed
|
||||
* or focused row keeps its fill under the tint instead of trading it for a translucent one (C-08's
|
||||
* second fix). `data-md-selected` is M3's selected item, secondary-container filled, which also
|
||||
* or focused row keeps its fill under the tint instead of trading it for a translucent one.
|
||||
* `data-md-selected` is M3's selected item, secondary-container filled, which also
|
||||
* lifts the description/overline/trailing/icon colour rules below since they exclude it — the
|
||||
* container's own colour takes over by inheritance. The same attribute on any other row that is
|
||||
* not a card — an application's own `<li>`, `<div>` or `<tr>` — draws the same fill and ink, as
|
||||
* 1.x's `data-selected` did; a card keeps its own container. A selected option draws a second cue, a
|
||||
* trailing check, so selection is never colour alone (C-03). `aria-disabled` inks the whole item
|
||||
* trailing check, so selection is never colour alone. `aria-disabled` inks the whole item
|
||||
* on-surface at the disabled-content opacity (38%) and blocks the pointer; the view drops the
|
||||
* item's link entirely rather than leaving a focusable, activatable control behind it (C-02).
|
||||
* item's link entirely rather than leaving a focusable, activatable control behind it.
|
||||
*
|
||||
* `data-md-list="segmented"` items (M3 Expressive) are `surface-container` tiles (one tone up from
|
||||
* `ItemSegmentedContainerColor`, so they read against the package's own `surface` page;
|
||||
* C-21), 4px corners that open to 16px at the list's own ends and while hovered (12px), pressed,
|
||||
* focused or selected (16px) — the specs page's interaction-state expressive shapes (C-22). Leading
|
||||
* `ItemSegmentedContainerColor`, so they read against the package's own `surface` page),
|
||||
* 4px corners that open to 16px at the list's own ends and while hovered (12px), pressed,
|
||||
* focused or selected (16px) — the specs page's interaction-state expressive shapes. Leading
|
||||
* icons there are drawn at 20px, not scaled down from 24 — list-item.blade.php passes `size` to
|
||||
* `<x-icon>` from the parent's `@aware(['segmented'])`, so no CSS override is needed here (C-26).
|
||||
* `<x-icon>` from the parent's `@aware(['segmented'])`, so no CSS override is needed here.
|
||||
*
|
||||
* `data-md-dividers` on the list insets its rule 16px from both ends (`DividerLeadingSpace`/
|
||||
* `DividerTrailingSpace`, C-16) rather than full-bleed.
|
||||
* `DividerTrailingSpace`) rather than full-bleed.
|
||||
*
|
||||
* Leading media: `data-md-list-item-avatar` is 40px, full corner, initials in primary-container
|
||||
* when it is a `<span>`, an image otherwise; `data-md-list-item-image` is a 56px small-corner
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*
|
||||
* `selectable`/`selection` switch the container's role between `list` and `listbox`
|
||||
* (list.blade.php; docs/reference/m3/components-actions-communication-containment.md
|
||||
* § Lists → Accessibility, C-03) — a role takes no CSS of its own.
|
||||
* § Lists → Accessibility) — a role takes no CSS of its own.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@@ -3,20 +3,20 @@
|
||||
* the trigger and popover of a nested menu beside it.
|
||||
*
|
||||
* SegmentedMenuTokens (androidx Compose Material 3, Apache-2.0): 48px row (M3's published "List
|
||||
* item height" wins over the token's own 44dp `Item`, docs/audits/m3-alignment/actions.md §
|
||||
* ACT-28), 16px either side (`ItemLeadingSpace`/`ItemTrailingSpace`, § ACT-11), 12px between the
|
||||
* icon, the label and the trailing content (`ItemBetweenSpace`, off the 8dp grid), 4px corners
|
||||
* (`ItemShape`) that open to 12px at either end of the list (`ItemFirstChildShape`/
|
||||
* `ItemLastChildShape` — the library's own list ends, 12px, win over `GroupShape`'s 8dp so a
|
||||
* cluster's ends and a list's ends match, menu-group.css). A selected row
|
||||
* (`role="menuitemcheckbox"`, the tick beside the colour and shape § ACT-27) takes
|
||||
* `ItemSelectedShape`'s 12px corner in tertiary-container; `current` — a menu of places rather
|
||||
* than choices — takes the same 12px corner in secondary-container, the navigation-indicator
|
||||
* role. `description` (`ItemSupportingTextFont`) grows the row by 8px top and bottom; `shortcut`
|
||||
* (`ItemTrailingSupportingTextFont`) sits at the end. Every quiet part of the row — the icon, the
|
||||
* description, the trailing text — inks together as `--md-menu-item-ink`; corner and colour are
|
||||
* two springs, not one (§ ACT-19), and `icon-class` paints only the leading icon, over that ink
|
||||
* but never over disabled. The row renders the foundation's `md-state-layer` and `md-focus-ring`
|
||||
* item height" wins over the token's own 44dp `Item`), 16px either side
|
||||
* (`ItemLeadingSpace`/`ItemTrailingSpace`), 12px between the icon, the label and the trailing
|
||||
* content (`ItemBetweenSpace`, off the 8dp grid), 4px corners (`ItemShape`) that open to 12px at
|
||||
* either end of the list (`ItemFirstChildShape`/`ItemLastChildShape` — the library's own list
|
||||
* ends, 12px, win over `GroupShape`'s 8dp so a cluster's ends and a list's ends match,
|
||||
* menu-group.css). A selected row (`role="menuitemcheckbox"`, the tick beside the colour and
|
||||
* shape) takes `ItemSelectedShape`'s 12px corner in tertiary-container; `current` — a menu of
|
||||
* places rather than choices — takes the same 12px corner in secondary-container, the
|
||||
* navigation-indicator role. `description` (`ItemSupportingTextFont`) grows the row by 8px top
|
||||
* and bottom; `shortcut` (`ItemTrailingSupportingTextFont`) sits at the end. Every quiet part of
|
||||
* the row — the icon, the description, the trailing text — inks together as
|
||||
* `--md-menu-item-ink`; corner and colour are two springs, not one — a colour must never
|
||||
* overshoot — and `icon-class` paints only the leading icon, over that ink but never over
|
||||
* disabled. The row renders the foundation's `md-state-layer` and `md-focus-ring`
|
||||
* (foundation/interaction.css) — no `md-touch-target`, since the row is already 48px tall — and
|
||||
* keeps only one refinement: the ring reads inward (-3px), because a row sits edge to edge in the
|
||||
* list and an outward one would run past it. A disabled item stays focusable and keeps its ring,
|
||||
@@ -36,6 +36,7 @@
|
||||
|
||||
@import './icon.css';
|
||||
@import './badge.css';
|
||||
@import './menu.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-menu-item] {
|
||||
@@ -145,40 +146,13 @@
|
||||
}
|
||||
|
||||
/* The submenu popover: a menu of its own, sharing the container colour of the list it opens
|
||||
from. The fallbacks are the standard menu's, for a submenu inside some other list. */
|
||||
from. Its size, radius, padding, shadow and pop-and-fade entry/exit are menu.css's shared
|
||||
popover chrome (imported above); only its axis and its position are its own — inline,
|
||||
beside the item, rather than the standard menu's block axis under its trigger. */
|
||||
[data-md-submenu] {
|
||||
inset: auto;
|
||||
margin: 0;
|
||||
margin-inline: var(--md-sys-measurement-space50);
|
||||
min-inline-size: 112px;
|
||||
max-inline-size: 280px;
|
||||
max-block-size: min(288px, calc(100dvh - 32px));
|
||||
overflow-y: auto;
|
||||
border-width: 0;
|
||||
border-radius: var(--md-sys-shape-corner-lg);
|
||||
padding: var(--md-sys-measurement-space50);
|
||||
background-color: var(--material-menu-surface, var(--md-sys-color-surface-container-low));
|
||||
color: var(--material-menu-ink, var(--md-sys-color-on-surface));
|
||||
box-shadow: var(--md-sys-elevation-2);
|
||||
margin: 0 var(--md-sys-measurement-space50);
|
||||
transform-origin: top;
|
||||
position-area: inline-end span-block-end;
|
||||
position-try-fallbacks: flip-inline;
|
||||
opacity: 0;
|
||||
scale: 0.95;
|
||||
transition-property: opacity, scale;
|
||||
transition-duration: var(--md-sys-motion-effects-fast-duration), var(--md-sys-motion-spatial-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-effects-fast), var(--md-sys-motion-spatial-fast);
|
||||
|
||||
&:popover-open:not([data-md-popover-closing]) {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
&:popover-open {
|
||||
opacity: 0;
|
||||
scale: 0.95;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
*
|
||||
* `[data-md-menu]` is `<x-menu>`'s root, holding the trigger and the popover; the popover itself
|
||||
* is `[data-md-menu-popover]`, capped at 288px so a long menu scrolls instead of running off the
|
||||
* top layer's edge (ACT-04), and transitions in by growing from its trigger's corner while it
|
||||
* fades, on the spatial and effects springs respectively (ACT-26). `data-md-vibrant` swaps its
|
||||
* top layer's edge, and transitions in by growing from its trigger's corner while it
|
||||
* fades, on the spatial and effects springs respectively. `data-md-vibrant` swaps its
|
||||
* container for tertiary-container; a nested submenu inherits the colour through
|
||||
* `--material-menu-surface`/`--material-menu-ink`, which the sheet's own copy of the list falls
|
||||
* back to instead (it is not a descendant of the popover).
|
||||
@@ -50,13 +50,14 @@
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
[data-md-menu-popover] {
|
||||
--material-menu-surface: var(--md-sys-color-surface-container-low);
|
||||
--material-menu-ink: var(--md-sys-color-on-surface);
|
||||
|
||||
/* The popover chrome a submenu shares (menu-item.css, which imports this file for it): size
|
||||
limits, the surface's own colour (a submenu with no ancestor popover falls back to the
|
||||
standard menu's own), radius, padding, shadow, and the pop-and-fade entry/exit — M3's
|
||||
effects-fast fade behind the spatial-fast scale. Each writes its own whole `margin`, so no
|
||||
rule here depends on which file a bundler puts first. */
|
||||
[data-md-menu-popover],
|
||||
[data-md-submenu] {
|
||||
inset: auto;
|
||||
margin: 0;
|
||||
margin-block: var(--md-sys-measurement-space50);
|
||||
min-inline-size: 112px;
|
||||
max-inline-size: 280px;
|
||||
max-block-size: min(288px, calc(100dvh - 32px));
|
||||
@@ -64,10 +65,9 @@
|
||||
border-width: 0;
|
||||
border-radius: var(--md-sys-shape-corner-lg);
|
||||
padding: var(--md-sys-measurement-space50);
|
||||
background-color: var(--material-menu-surface);
|
||||
color: var(--material-menu-ink);
|
||||
background-color: var(--material-menu-surface, var(--md-sys-color-surface-container-low));
|
||||
color: var(--material-menu-ink, var(--md-sys-color-on-surface));
|
||||
box-shadow: var(--md-sys-elevation-2);
|
||||
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
|
||||
opacity: 0;
|
||||
scale: 0.95;
|
||||
transition-property: opacity, scale;
|
||||
@@ -87,6 +87,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-menu-popover] {
|
||||
--material-menu-surface: var(--md-sys-color-surface-container-low);
|
||||
--material-menu-ink: var(--md-sys-color-on-surface);
|
||||
|
||||
margin: var(--md-sys-measurement-space50) 0;
|
||||
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
|
||||
}
|
||||
|
||||
[data-md-menu-popover]:has(> [data-md-menu-filter]) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
*
|
||||
* 560/280px width (max/min), 48px scrim margin, 28px corner and 24dp padding all match the specs
|
||||
* page; a compact window's full-screen dialog swaps the box for the whole screen with a 56px bar
|
||||
* (C-17: this used to be 64px) instead of the headline block. It opens on the fast spatial spring
|
||||
* (this used to be 64px) instead of the headline block. It opens on the fast spatial spring
|
||||
* and closes at once — a native `<dialog>` makes an exit transition awkward, since
|
||||
* `@starting-style` only ever supplies an entry.
|
||||
*
|
||||
* The dividers (§ Dialogs → Anatomy, C-06): a 1px outline-variant rule under the pinned head and
|
||||
* The dividers (§ Dialogs → Anatomy): a 1px outline-variant rule under the pinned head and
|
||||
* over the pinned actions, each only while the body has more content hidden on its side.
|
||||
* resources/js/dialog.js marks the `<dialog>` with data-md-overflow-top/-bottom as the body
|
||||
* scrolls; `data-md-modal-divider` says an element takes part at all — the head and the actions
|
||||
@@ -53,6 +53,19 @@
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
/* Dialog chrome shared with the time picker's dialog and a modal date picker (timepicker.css,
|
||||
* datepicker.css, both import this file for it): M3's spatial-fast pop and a 32%-scrim
|
||||
* backdrop, entering only — a native `<dialog>` makes an exit transition awkward, since
|
||||
* `@starting-style` only ever supplies an entry. A plain selector list, not `:is()`: `:is()`
|
||||
* would flatten every branch to the specificity of `[data-md-datepicker-picker]:modal`, the
|
||||
* one with a pseudo-class, and outrank whatever else the modal or the time picker's dialog is
|
||||
* layered against.
|
||||
*/
|
||||
[data-md-modal],
|
||||
[data-md-timepicker-dialog],
|
||||
[data-md-datepicker-picker]:modal {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
transition-property: opacity, scale;
|
||||
@@ -61,13 +74,17 @@
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
[data-md-modal] {
|
||||
[data-md-modal],
|
||||
[data-md-timepicker-dialog],
|
||||
[data-md-datepicker-picker]:modal {
|
||||
opacity: 0;
|
||||
scale: 0.95;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-modal]::backdrop {
|
||||
[data-md-modal]::backdrop,
|
||||
[data-md-timepicker-dialog]::backdrop,
|
||||
[data-md-datepicker-picker]:modal::backdrop {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent);
|
||||
}
|
||||
|
||||
@@ -100,7 +117,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* The phone-only header bar: M3's full-screen dialog header, 56dp (C-17). */
|
||||
/* The phone-only header bar: M3's full-screen dialog header, 56dp. */
|
||||
[data-md-modal-bar] {
|
||||
display: flex;
|
||||
height: var(--md-sys-measurement-space700);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* wide. From 600px icon and label share a 40px horizontal indicator with 16px leading and
|
||||
* trailing space; the label stays label-medium — `NavigationBarTokens.LabelTextFont` is the bar's
|
||||
* only label token, and Compose's `ShortNavigationBarItem` passes it for both icon positions (the
|
||||
* *rail's* horizontal item is label-large, a different component's token; N-08). An active item is
|
||||
* *rail's* horizontal item is label-large, a different component's token). An active item is
|
||||
* secondary-container behind on-secondary-container (icon and, from 600px, label); its indicator's
|
||||
* fill is a background image so it can grow from its centre (navigation-item.css) rather than
|
||||
* stretch the icon.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* `NavigationRailColorTokens.kt` gives one colour for all six states, active or not:
|
||||
* `ItemActiveHoveredStateLayer = … = ItemInactivePressedStateLayer = OnSecondaryContainer`.
|
||||
* `NavigationBarTokens.kt` states no state-layer tokens of its own, so the bar's item takes the
|
||||
* rail's (N-19). Hover only where a pointer can hover, so a touch screen does not keep the last
|
||||
* rail's. Hover only where a pointer can hover, so a touch screen does not keep the last
|
||||
* hover after a tap; state.css's own opacities (8% hover, 10% focus and press) rather than the
|
||||
* package's `md-state-layer` class, because the element focused and pressed is the whole item
|
||||
* while the layer is drawn only on the smaller indicator or pill inside it — `md-state-layer`
|
||||
|
||||
@@ -108,14 +108,7 @@
|
||||
/* ---- Collapsed: icon over label, the 56×32 indicator, badge back on the icon. ---- */
|
||||
|
||||
[data-md-navigation-rail-item] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
@@ -127,136 +120,13 @@
|
||||
font: var(--md-sys-typescale-label-md);
|
||||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
height: auto;
|
||||
min-height: 64px;
|
||||
margin-inline: 0;
|
||||
padding: 6px 4px;
|
||||
border-radius: 0;
|
||||
font: var(--md-sys-typescale-label-md);
|
||||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
height: auto;
|
||||
min-height: 64px;
|
||||
margin-inline: 0;
|
||||
padding: 6px 4px;
|
||||
border-radius: 0;
|
||||
font: var(--md-sys-typescale-label-md);
|
||||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
height: auto;
|
||||
min-height: 64px;
|
||||
margin-inline: 0;
|
||||
padding: 6px 4px;
|
||||
border-radius: 0;
|
||||
font: var(--md-sys-typescale-label-md);
|
||||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
height: auto;
|
||||
min-height: 64px;
|
||||
margin-inline: 0;
|
||||
padding: 6px 4px;
|
||||
border-radius: 0;
|
||||
font: var(--md-sys-typescale-label-md);
|
||||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
--navigation-item-layer: 0;
|
||||
--navigation-indicator-layer: var(--navigation-layer);
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
--navigation-item-layer: 0;
|
||||
--navigation-indicator-layer: var(--navigation-layer);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
--navigation-item-layer: 0;
|
||||
--navigation-indicator-layer: var(--navigation-layer);
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
--navigation-item-layer: 0;
|
||||
--navigation-indicator-layer: var(--navigation-layer);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
--navigation-item-layer: 0;
|
||||
--navigation-indicator-layer: var(--navigation-layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item] [data-md-navigation-label] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
flex: none;
|
||||
display: -webkit-box;
|
||||
max-width: 100%;
|
||||
@@ -265,381 +135,52 @@
|
||||
text-align: center;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
flex: none;
|
||||
display: -webkit-box;
|
||||
max-width: 100%;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
text-align: center;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
flex: none;
|
||||
display: -webkit-box;
|
||||
max-width: 100%;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
text-align: center;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
flex: none;
|
||||
display: -webkit-box;
|
||||
max-width: 100%;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
text-align: center;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
flex: none;
|
||||
display: -webkit-box;
|
||||
max-width: 100%;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
text-align: center;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item][data-md-active] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
background-image: none;
|
||||
color: var(--md-sys-color-secondary);
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
background-image: none;
|
||||
color: var(--md-sys-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
background-image: none;
|
||||
color: var(--md-sys-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
background-image: none;
|
||||
color: var(--md-sys-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
background-image: none;
|
||||
color: var(--md-sys-color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item] [data-md-navigation-indicator] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
width: 56px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
width: 56px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
width: 56px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
width: 56px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
width: 56px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item][data-md-active] [data-md-navigation-indicator] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item]:focus-visible {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item]:focus-visible [data-md-navigation-indicator] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-badge-end],
|
||||
[data-md-navigation-badge-icon] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-badge-icon] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,46 +14,9 @@
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-section]:not(:first-child) {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-heading] {
|
||||
@@ -67,48 +30,9 @@
|
||||
letter-spacing: var(--md-sys-typescale-title-sm-tracking);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-heading] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
* [data-md-navigation-rail-item] navigation-rail-item.css
|
||||
* [data-md-navigation-rail-footer] never scrolls
|
||||
*
|
||||
* **Collapsed**: a rail (and anything in it) is drawn collapsed while any of these hold, each its
|
||||
* own rule below wherever a property differs by shape — a fixed `collapsed` mode; a `collapsible` rail the visitor collapsed
|
||||
* **Collapsed**: a rail (and anything in it) is drawn collapsed while any of these hold, which the
|
||||
* rail's own two rules below publish as `--md-navigation-rail-value` for everything inside it — a fixed `collapsed` mode; a `collapsible` rail the visitor collapsed
|
||||
* (`<html data-rail="collapsed">`, set before the first paint by `<x-theme-script>`) and not
|
||||
* currently open over the page; a `modal` rail not open; below `medium` (600px) a `collapsible`
|
||||
* rail regardless of choice, M3's floor ("compact → use a navigation bar, not a standard rail");
|
||||
@@ -56,14 +56,14 @@
|
||||
*
|
||||
* `data-md-divider` and `data-md-fill="false"` are both Missing until this rewrite: an optional
|
||||
* vertical divider on the page's edge, M3's own answer to a page scrolling under a fixed rail, and
|
||||
* a transparent container as long as the items keep 3:1 contrast (N-22). `data-md-align="center"`
|
||||
* a transparent container as long as the items keep 3:1 contrast. `data-md-align="center"`
|
||||
* is M3's other alignment, preferred on a tablet for reach — only the destinations move; the
|
||||
* header, footer and menu button stay at their ends, and `safe` gives the top back once there are
|
||||
* more destinations than fit.
|
||||
*
|
||||
* The rail-header FAB rules at the end draw M3's nested-FAB elevation (level 0, not a standalone
|
||||
* FAB's 3 — N-03) and morph it into an extended FAB as the rail opens, rather than swapping two by
|
||||
* `display` (N-23). Both sit in this file's own `material.components` layer and win by specificity
|
||||
* FAB's 3) and morph it into an extended FAB as the rail opens, rather than swapping two by
|
||||
* `display`. Both sit in this file's own `material.components` layer and win by specificity
|
||||
* alone, whichever copy of fab.css a build places last: the flat shadow outranks fab.css's resting
|
||||
* and hover shadows, and the collapsed FAB's `gap` and minimum width outrank fab.css's extended-size
|
||||
* and collapse-on-scroll rules, which would otherwise tie with them.
|
||||
@@ -187,14 +187,7 @@
|
||||
alone; the label is taken out of the drawing, not out of the page (navigation-rail-item.css
|
||||
renders it always — it is what names the destination). */
|
||||
[data-md-navigation-rail][data-md-width='narrow'] [data-md-navigation-rail-item] [data-md-navigation-label] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
@@ -204,64 +197,6 @@
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* A modal rail keeps its collapsed width in the layout while it is open over it, as Compose's
|
||||
@@ -284,7 +219,7 @@
|
||||
|
||||
/* "Compact → don't use a standard rail, use a navigation bar." A `collapsible` rail takes
|
||||
the width the visitor chose, which on a 360px screen would be two-thirds of it, so it is
|
||||
floored at its collapsed width (N-24). `collapsed` and `expanded` mean what they say and
|
||||
floored at its collapsed width. `collapsed` and `expanded` mean what they say and
|
||||
are left alone: wrap one in an application's own `medium:` element if it must not show
|
||||
on a phone. */
|
||||
[data-md-navigation-rail='collapsible'] {
|
||||
@@ -315,7 +250,7 @@
|
||||
/* M3's two optional container treatments: "optional vertical divider separates rail from
|
||||
content, placed on the content-adjacent edge" — also its answer to a page scrolling under a
|
||||
fixed rail — and "container fill can be turned off (transparent) as long as items keep ≥3:1
|
||||
contrast" (N-22). Neither applies to a rail open over a scrim, a surface over the page. */
|
||||
contrast". Neither applies to a rail open over a scrim, a surface over the page. */
|
||||
[data-md-navigation-rail][data-md-divider]:not([data-md-open], [data-md-closing='sheet']) > [data-md-navigation-rail-panel] {
|
||||
border-inline-end: 1px solid var(--md-sys-color-outline-variant);
|
||||
}
|
||||
@@ -393,14 +328,7 @@
|
||||
the collapsed branches, which stop the moment the rail is open — that is what makes it the
|
||||
expanded rail that comes back, on the unconditional rule below. */
|
||||
[data-md-navigation-rail][data-md-hide-when-collapsed] > [data-md-navigation-rail-panel] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
display: none;
|
||||
translate: -100% 0;
|
||||
transition:
|
||||
@@ -410,64 +338,6 @@
|
||||
translate: 100% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
display: none;
|
||||
translate: -100% 0;
|
||||
transition:
|
||||
translate var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-easing-emphasized-accelerate);
|
||||
|
||||
&:is([dir='rtl'], [dir='rtl'] *) {
|
||||
translate: 100% 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
display: none;
|
||||
translate: -100% 0;
|
||||
transition:
|
||||
translate var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-easing-emphasized-accelerate);
|
||||
|
||||
&:is([dir='rtl'], [dir='rtl'] *) {
|
||||
translate: 100% 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
display: none;
|
||||
translate: -100% 0;
|
||||
transition:
|
||||
translate var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-easing-emphasized-accelerate);
|
||||
|
||||
&:is([dir='rtl'], [dir='rtl'] *) {
|
||||
translate: 100% 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
display: none;
|
||||
translate: -100% 0;
|
||||
transition:
|
||||
translate var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-easing-emphasized-accelerate);
|
||||
|
||||
&:is([dir='rtl'], [dir='rtl'] *) {
|
||||
translate: 100% 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail][data-md-hide-when-collapsed][data-md-open] > [data-md-navigation-rail-panel] {
|
||||
@@ -591,46 +461,9 @@
|
||||
}
|
||||
|
||||
[data-md-navigation-rail][data-md-width='narrow'] [data-md-navigation-rail-header]:has([data-md-navigation-rail-menu]) [data-md-navigation-rail-menu-row] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-menu] {
|
||||
@@ -658,137 +491,24 @@
|
||||
|
||||
[data-md-navigation-rail-menu-collapse-icon],
|
||||
[data-md-navigation-rail-menu-expand-icon] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-menu-expand-icon] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-brand] {
|
||||
min-width: 0;
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-brand] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* The FAB row: 20px either side in the default 96px rail, 12px in a narrow 80 while collapsed
|
||||
@@ -803,46 +523,9 @@
|
||||
}
|
||||
|
||||
[data-md-navigation-rail][data-md-width='narrow'] [data-md-navigation-rail-fab-row] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
padding-inline: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
padding-inline: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
padding-inline: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
padding-inline: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-destinations] {
|
||||
@@ -858,50 +541,10 @@
|
||||
[data-md-navigation-rail-footer] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-destinations],
|
||||
[data-md-navigation-rail-footer] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-footer] {
|
||||
@@ -921,7 +564,7 @@
|
||||
/*
|
||||
* A FAB in the rail's header rests at elevation 0, not the 3 a standalone FAB has
|
||||
* (§ Navigation Rail/Behaviour: "when nested within another component, such as the navigation
|
||||
* rail, the FAB's resting elevation should be level 0" — N-03). The doubled `[data-md-fab]`
|
||||
* rail, the FAB's resting elevation should be level 0"). The doubled `[data-md-fab]`
|
||||
* attribute only adds specificity, over fab.css's `[data-md-fab]:hover` too, as
|
||||
* `[data-md-navigation-rail-item][data-md-navigation-rail-item]::before`
|
||||
* (navigation-rail-item.css) does for the same reason.
|
||||
@@ -933,7 +576,7 @@
|
||||
|
||||
/*
|
||||
* "Rail transitions from collapsed to expanded should animate the FAB into and out of an
|
||||
* extended FAB" (N-23) — so the header takes one `<x-fab label icon>` and morphs it, rather
|
||||
* extended FAB" — so the header takes one `<x-fab label icon>` and morphs it, rather
|
||||
* than swapping two by `display`, which showed the collapsed one at once while the rail's
|
||||
* width was still springing. The label's width springs on the rail's own spatial spring; with
|
||||
* the gap and the extended FAB's own minimum width both zeroed, `aspect-ratio` squares what is
|
||||
@@ -949,109 +592,21 @@
|
||||
transition:
|
||||
max-width var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default),
|
||||
opacity var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-effects-default);
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-header] [data-md-fab] > span {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* `[data-md-extended]` doubled for the same reason as the shadow's `[data-md-fab]` above: it
|
||||
outranks fab.css's `[data-md-fab][data-md-extended][data-md-size]` gap and its collapsed
|
||||
collapse-on-scroll FAB's minimum width, three attributes each. */
|
||||
[data-md-navigation-rail-header] [data-md-fab][data-md-extended][data-md-extended] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
min-inline-size: 0;
|
||||
aspect-ratio: 1;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
min-inline-size: 0;
|
||||
aspect-ratio: 1;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
min-inline-size: 0;
|
||||
aspect-ratio: 1;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
min-inline-size: 0;
|
||||
aspect-ratio: 1;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
min-inline-size: 0;
|
||||
aspect-ratio: 1;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,12 @@
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './color.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-progress] {
|
||||
color: var(--md-sys-color-primary);
|
||||
stroke: var(--md-sys-color-secondary-container);
|
||||
color: var(--md-color, var(--md-sys-color-primary));
|
||||
stroke: var(--md-container, var(--md-sys-color-secondary-container));
|
||||
|
||||
& > svg {
|
||||
display: block;
|
||||
@@ -80,33 +82,9 @@
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
[data-md-progress][data-md-color='secondary'] {
|
||||
color: var(--md-sys-color-secondary);
|
||||
/* Every other hue comes from the shared colour-role table (color.css): its own `primary` entry
|
||||
is plain primary-container, so the one hue this file draws differently keeps its override. */
|
||||
[data-md-progress][data-md-color='primary'] {
|
||||
stroke: var(--md-sys-color-secondary-container);
|
||||
}
|
||||
|
||||
[data-md-progress][data-md-color='tertiary'] {
|
||||
color: var(--md-sys-color-tertiary);
|
||||
stroke: var(--md-sys-color-tertiary-container);
|
||||
}
|
||||
|
||||
[data-md-progress][data-md-color='error'] {
|
||||
color: var(--md-sys-color-error);
|
||||
stroke: var(--md-sys-color-error-container);
|
||||
}
|
||||
|
||||
[data-md-progress][data-md-color='success'] {
|
||||
color: var(--md-sys-color-success);
|
||||
stroke: var(--md-sys-color-success-container);
|
||||
}
|
||||
|
||||
[data-md-progress][data-md-color='warning'] {
|
||||
color: var(--md-sys-color-warning);
|
||||
stroke: var(--md-sys-color-warning-container);
|
||||
}
|
||||
|
||||
[data-md-progress][data-md-color='info'] {
|
||||
color: var(--md-sys-color-info);
|
||||
stroke: var(--md-sys-color-info-container);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* back to the closed opacity.
|
||||
*
|
||||
* resources/js/rich-tooltip.js shows and hides the bubble and keeps the trigger described with
|
||||
* `aria-describedby` — and, while `persistent`, `aria-haspopup`/`aria-expanded` (ACT-22) — none of
|
||||
* `aria-describedby` — and, while `persistent`, `aria-haspopup`/`aria-expanded` — none of
|
||||
* which this stylesheet draws.
|
||||
*/
|
||||
|
||||
|
||||
@@ -84,17 +84,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
& > input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
/* The radio stays in the page, for the form, the keyboard and a screen reader —
|
||||
`md-visually-hidden` on the element itself (text.css), which outranks this layer
|
||||
whatever wins the specificity, `material.text` sitting above `material.components`. */
|
||||
}
|
||||
|
||||
[data-md-scheme-picker-swatches] {
|
||||
|
||||
@@ -168,11 +168,6 @@
|
||||
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
||||
}
|
||||
|
||||
[data-md-search-input]::placeholder {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-md-search-input]::-webkit-search-cancel-button,
|
||||
[data-md-search-input]::-webkit-search-decoration {
|
||||
appearance: none;
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* <x-section-nav>: the navigation inside one area of an application — M3's secondary tabs from
|
||||
* `medium` (600px), five or more of them the scrollable tab bar M3's own accessibility page
|
||||
* blesses rather than a grid that would break the tab bar's divider and strand the upper rows'
|
||||
* indicators against nothing (docs/audits/m3-alignment/navigation.md N-16); below `medium`, where a
|
||||
* row of them never fits, a button naming the current section opens the same list as a menu.
|
||||
* indicators against nothing; below `medium`, where a row of them never fits, a button naming the
|
||||
* current section opens the same list as a menu.
|
||||
*
|
||||
* [data-md-section-nav] min-inline-size: 0
|
||||
* [data-md-section-nav-picker] hidden from medium
|
||||
@@ -14,7 +14,7 @@
|
||||
*
|
||||
* Reuses `<x-tabs>`'s own hooks and stylesheet rather than a tab bar of its own: a link marked
|
||||
* `aria-current="page"` takes the same active indicator and colour tabs.css already draws for
|
||||
* `aria-selected="true"` (N-21), so nothing here repeats a rule tabs.css owns; the only addition is
|
||||
* `aria-selected="true"`, so nothing here repeats a rule tabs.css owns; the only addition is
|
||||
* this bar's own list items — flex rows, so tabs.css's `li > [data-md-tab]` share of the width
|
||||
* applies, and `min-inline-size: 0`, so a long title in a fixed row wraps to its second line and
|
||||
* truncates there (tabs.css, M3's "may wrap to a max second line if needed with truncation")
|
||||
|
||||
@@ -45,13 +45,14 @@
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
@import './color.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-slider] {
|
||||
--slider-active: var(--md-sys-color-primary);
|
||||
--slider-inactive: var(--md-sys-color-secondary-container);
|
||||
--slider-on-active: var(--md-sys-color-on-primary);
|
||||
--slider-on-inactive: var(--md-sys-color-on-secondary-container);
|
||||
--slider-active: var(--md-color, var(--md-sys-color-primary));
|
||||
--slider-inactive: var(--md-container, var(--md-sys-color-secondary-container));
|
||||
--slider-on-active: var(--md-on-color, var(--md-sys-color-on-primary));
|
||||
--slider-on-inactive: var(--md-on-container, var(--md-sys-color-on-secondary-container));
|
||||
--slider-disabled-active: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
--slider-disabled-inactive: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-container-opacity) * 100%), transparent);
|
||||
--slider-across: 48px;
|
||||
@@ -84,44 +85,11 @@
|
||||
--slider-handle: 108px;
|
||||
}
|
||||
|
||||
[data-md-slider][data-md-color='secondary'] {
|
||||
--slider-active: var(--md-sys-color-secondary);
|
||||
--slider-on-active: var(--md-sys-color-on-secondary);
|
||||
}
|
||||
|
||||
[data-md-slider][data-md-color='tertiary'] {
|
||||
--slider-active: var(--md-sys-color-tertiary);
|
||||
--slider-inactive: var(--md-sys-color-tertiary-container);
|
||||
--slider-on-active: var(--md-sys-color-on-tertiary);
|
||||
--slider-on-inactive: var(--md-sys-color-on-tertiary-container);
|
||||
}
|
||||
|
||||
[data-md-slider][data-md-color='error'] {
|
||||
--slider-active: var(--md-sys-color-error);
|
||||
--slider-inactive: var(--md-sys-color-error-container);
|
||||
--slider-on-active: var(--md-sys-color-on-error);
|
||||
--slider-on-inactive: var(--md-sys-color-on-error-container);
|
||||
}
|
||||
|
||||
[data-md-slider][data-md-color='success'] {
|
||||
--slider-active: var(--md-sys-color-success);
|
||||
--slider-inactive: var(--md-sys-color-success-container);
|
||||
--slider-on-active: var(--md-sys-color-on-success);
|
||||
--slider-on-inactive: var(--md-sys-color-on-success-container);
|
||||
}
|
||||
|
||||
[data-md-slider][data-md-color='warning'] {
|
||||
--slider-active: var(--md-sys-color-warning);
|
||||
--slider-inactive: var(--md-sys-color-warning-container);
|
||||
--slider-on-active: var(--md-sys-color-on-warning);
|
||||
--slider-on-inactive: var(--md-sys-color-on-warning-container);
|
||||
}
|
||||
|
||||
[data-md-slider][data-md-color='info'] {
|
||||
--slider-active: var(--md-sys-color-info);
|
||||
--slider-inactive: var(--md-sys-color-info-container);
|
||||
--slider-on-active: var(--md-sys-color-on-info);
|
||||
--slider-on-inactive: var(--md-sys-color-on-info-container);
|
||||
/* Every other hue comes from the shared colour-role table (color.css): its own `primary` entry
|
||||
is plain primary-container, so the one hue this file draws differently keeps its override. */
|
||||
[data-md-slider][data-md-color='primary'] {
|
||||
--slider-inactive: var(--md-sys-color-secondary-container);
|
||||
--slider-on-inactive: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
/* Disabled: the colours give way to on-surface at M3's disabled opacities. */
|
||||
|
||||
@@ -66,10 +66,7 @@
|
||||
[data-md-split-button] [data-md-split] {
|
||||
--md-split-button-corner: var(--md-split-button-inner);
|
||||
|
||||
border-start-start-radius: var(--md-split-button-corner);
|
||||
border-end-start-radius: var(--md-split-button-corner);
|
||||
border-start-end-radius: var(--md-split-button-corner);
|
||||
border-end-end-radius: var(--md-split-button-corner);
|
||||
border-radius: var(--md-split-button-corner);
|
||||
}
|
||||
|
||||
[data-md-split-button] [data-md-split]:active {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* [data-md-tab-content] [data-md-icon] (optional), [data-md-tab-label],
|
||||
* [data-md-tab-indicator]
|
||||
* [data-md-tab-label] [data-md-tab-text], [data-md-badge] (optional)
|
||||
* [data-md-tab-panel] role="tabpanel", hidden server-side until chosen (N-05)
|
||||
* [data-md-tab-panel] role="tabpanel", hidden server-side until chosen
|
||||
*
|
||||
* "Labels: single row by default; may wrap to a max second line if needed with truncation, or use
|
||||
* scrollable tabs to give longer titles more room" (§ Tabs, "Behaviour and guidelines"). A fixed
|
||||
@@ -25,7 +25,7 @@
|
||||
*
|
||||
* The active indicator is drawn in every tab and shown under the chosen one, so it is right before
|
||||
* Alpine starts and after a morph: 3px with rounded top corners under the content of a primary tab
|
||||
* (at least 24px wide, inset 2px at each side — N-18), 2px across the whole of a secondary one.
|
||||
* (at least 24px wide, inset 2px at each side), 2px across the whole of a secondary one.
|
||||
* When the choice changes, a view transition moves it from the old tab to the new (resources/js/tabs.js),
|
||||
* as M3's slides.
|
||||
*
|
||||
@@ -33,10 +33,10 @@
|
||||
* rest of the package draws 3px thick 2px outside the element (tokens/state.css). `--tabs-ring` is
|
||||
* the room that takes: the bar carries it as padding on every side, and the state layer and the
|
||||
* indicator reach back out into it, so the divider still sits against the tabs and the indicator
|
||||
* still sits on the divider (N-20). That makes the bar 10px taller than M3's 48px tab. A scrollable
|
||||
* set's first tab is offset 52dp from the leading edge, so it reads as scrollable (N-10). A link
|
||||
* still sits on the divider. That makes the bar 10px taller than M3's 48px tab. A scrollable
|
||||
* set's first tab is offset 52dp from the leading edge, so it reads as scrollable. A link
|
||||
* marked `aria-current="page"` (`<x-section-nav>`'s tabs) takes the active indicator and colour
|
||||
* exactly as `aria-selected="true"` does (N-21).
|
||||
* exactly as `aria-selected="true"` does.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
@@ -63,7 +63,7 @@
|
||||
}
|
||||
|
||||
/* "Offset the first scrollable tab 52dp from the leading edge so it's clear that more content
|
||||
is available" (N-10). Logical, so a right-to-left page mirrors it. */
|
||||
is available". Logical, so a right-to-left page mirrors it. */
|
||||
[data-md-tabs-bar][data-md-scrollable] {
|
||||
padding-inline-start: 52px;
|
||||
scroll-padding-inline-start: 52px;
|
||||
@@ -141,7 +141,7 @@
|
||||
}
|
||||
|
||||
/* A link-based bar marks the page with aria-current, not aria-selected; both are the chosen
|
||||
tab, and both take the variant's active colour (N-21). */
|
||||
tab, and both take the variant's active colour. */
|
||||
[data-md-tab]:is([aria-selected='true'], [aria-current='page']) {
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
@@ -196,7 +196,7 @@
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* Primary indicators are inset 2dp at each side (N-18); both variants sit on the divider, which
|
||||
/* Primary indicators are inset 2dp at each side; both variants sit on the divider, which
|
||||
the ring's room has pushed below the tab. */
|
||||
[data-md-tab-indicator] {
|
||||
position: absolute;
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
/*
|
||||
* <x-theme-toggle>: `mode="toggle"|"cycle"` an icon button between light, dark (and system for
|
||||
* cycle); `mode="picker"|"contrast"` a connected group over native radios (docs/audits/m3-alignment/
|
||||
* navigation.md N-04, N-17 — the Expressive-deprecated segmented button's successor,
|
||||
* `<x-group>`, already fixes both there). This file only draws the icon button; the group's own
|
||||
* shape, colour and 48px minimum below `medium` are group.css's.
|
||||
* cycle); `mode="picker"|"contrast"` a connected group over native radios — the Expressive-
|
||||
* deprecated segmented button's successor, `<x-group>`, already gives it the keyboard handling and
|
||||
* shape a hand-rolled control would have to reimplement. This file only draws the icon button; the
|
||||
* group's own shape, colour and 48px minimum below `medium` are group.css's.
|
||||
*
|
||||
* [data-md-theme-toggle="toggle"|"cycle"] the icon button: 40px reaching 48px through
|
||||
* md-touch-target (N-01), morphing corner
|
||||
* md-touch-target, morphing corner
|
||||
* [data-md-theme-toggle="picker"|"contrast"] the group's own wrapper (`role="group"`)
|
||||
*
|
||||
* The icon button also carries `data-md-icon-button`, the hook `resources/css/components/
|
||||
* toolbar.css` matches generically (not `[data-md-button][data-md-icon-button]`, as button.css
|
||||
* requires): a theme toggle placed in a toolbar's `actions` takes the toolbar's own icon colour
|
||||
* (N-13) the same way one of its `<x-button icon>` children would.
|
||||
* the same way one of its `<x-button icon>` children would.
|
||||
*
|
||||
* The corner morphs from full to `sm` while pressed on the fast spatial spring — M3 Expressive's
|
||||
* pressed-state shape change, the same motion `<x-button>`'s own icon buttons use
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
@import './field.css';
|
||||
@import './icon.css';
|
||||
@import './button.css';
|
||||
@import './modal.css';
|
||||
|
||||
@layer material.components {
|
||||
@property --timepicker-angle {
|
||||
@@ -77,7 +78,8 @@
|
||||
caret-color: transparent;
|
||||
}
|
||||
|
||||
/* The dialog: M3's spatial-fast pop, a 32%-scrim backdrop, no default border or padding. */
|
||||
/* The dialog's size, no default border or padding; the entry pop and the scrim are
|
||||
modal.css's shared dialog chrome, imported above. */
|
||||
[data-md-timepicker-dialog] {
|
||||
max-width: calc(100vw - var(--md-sys-measurement-space400));
|
||||
max-height: calc(100dvh - var(--md-sys-measurement-space400));
|
||||
@@ -87,22 +89,6 @@
|
||||
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] {
|
||||
|
||||
@@ -15,19 +15,19 @@
|
||||
* as M3 nudges a snackbar clear of a docked toolbar and never in front of navigation;
|
||||
* `data-md-position="bottom-start"` moves it to the start edge, with M3's margin growing from `medium`. resources/js/snackbar.js
|
||||
* publishes the snackbar's own height as `--material-snackbar-height`, so a FAB (fab.css) can
|
||||
* lift clear of it — M3 puts a snackbar above a FAB, never in front of or behind one (ACT-17).
|
||||
* lift clear of it — M3 puts a snackbar above a FAB, never in front of or behind one.
|
||||
* Below `medium` it is full width; from there it hugs its line length, which an application's own
|
||||
* class can still bound.
|
||||
*
|
||||
* The action and the close button are both drawn at 40px and render the foundation's
|
||||
* `md-state-layer`, `md-focus-ring` and `md-touch-target` (foundation/interaction.css) to reach
|
||||
* M3's 48px target (ACT-18); nothing here copies their rules.
|
||||
* M3's 48px target; nothing here copies their rules.
|
||||
*
|
||||
* It enters on a translate and a fade on the fast spatial spring, `@starting-style` giving both a
|
||||
* start. No state icon: M3 says to avoid one in a snackbar (ACT-20). The live region is the host
|
||||
* itself, in the page before any message is, never the snackbar that comes and goes (ACT-02);
|
||||
* start. No state icon: M3 says to avoid one in a snackbar. The live region is the host
|
||||
* itself, in the page before any message is, never the snackbar that comes and goes;
|
||||
* resources/js/snackbar.js queues, times and dismisses a toast, including on Escape while it
|
||||
* holds the focus and on Alt+G, which moves the focus to one carrying an action (ACT-34).
|
||||
* holds the focus and on Alt+G, which moves the focus to one carrying an action.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* 8px at its ends and 4px between controls; `vertical` stands it on end
|
||||
*
|
||||
* Placed over the page, a horizontal toolbar keeps 16dp from the window's edge and a vertical one
|
||||
* 24dp, which is M3's minimum for each (N-12).
|
||||
* 24dp, which is M3's minimum for each.
|
||||
*
|
||||
* On web and large screens a docked toolbar "can be rounded", and dividers can organise its
|
||||
* controls into groups: `rounded` does the first from `expanded` (840px), and a divider in the
|
||||
@@ -23,7 +23,7 @@
|
||||
*
|
||||
* `<x-button>` and `<x-fab>` draw their own ink from `data-md-color`/`data-md-variant`, not a
|
||||
* utility, so a toolbar's recolouring rules — vibrant's icon buttons, a standard toolbar's primary
|
||||
* ones (N-13), a docked FAB's flattened shadow — sit in this file's own `material.components`
|
||||
* ones, a docked FAB's flattened shadow — sit in this file's own `material.components`
|
||||
* layer like everything else, each more specific than the button.css or fab.css rule it overrides,
|
||||
* whichever file an application imports first. The recolouring leaves a disabled button alone, so
|
||||
* button.css's disabled colours still apply inside a toolbar.
|
||||
@@ -82,7 +82,7 @@
|
||||
}
|
||||
|
||||
/* M3's colour list for a standard toolbar ends "Standard button (Primary)", as the vibrant list
|
||||
ends "Standard button (On primary container)" — the row above (N-13). Both rows name the
|
||||
ends "Standard button (On primary container)" — the row above. Both rows name the
|
||||
*standard* button, so a button that fills its own container is left alone: it carries the
|
||||
matching label colour (`filled` on-primary, `tonal` on-tone, either selected), and painting
|
||||
that label primary put a primary icon on a primary fill — the current page's button in a
|
||||
@@ -140,7 +140,7 @@
|
||||
* A toolbar at the bottom shares the screen region with a navigation bar, so it clears the one
|
||||
* <x-scaffold> publishes as --material-bottom-bar, exactly as the FAB and the snackbar do —
|
||||
* `max()`, not a sum, because that height already swallows the bottom safe area; without a bar
|
||||
* the safe area alone applies (N-02). A *docked* toolbar and a navigation bar must never be on
|
||||
* the safe area alone applies. A *docked* toolbar and a navigation bar must never be on
|
||||
* screen together at all; the offset is the backstop, not a licence.
|
||||
*/
|
||||
[data-md-toolbar-place='bottom'] {
|
||||
@@ -160,7 +160,7 @@
|
||||
}
|
||||
|
||||
/* "Horizontal toolbars should have a minimum 16dp margin from the edge of the window. Vertical
|
||||
toolbars should have a minimum 24dp margin" (N-12). */
|
||||
toolbars should have a minimum 24dp margin". */
|
||||
[data-md-toolbar][data-md-vertical][data-md-toolbar-place='end'],
|
||||
[data-md-toolbar-group][data-md-vertical][data-md-toolbar-place='end'] {
|
||||
inset-inline-end: calc(var(--md-sys-measurement-space300) + var(--material-safe-right, env(safe-area-inset-right)));
|
||||
|
||||
Vendored
+9
-2
@@ -10,7 +10,8 @@
|
||||
* Every rule of the package sits in a sub-layer of `material`, in this order, lowest first:
|
||||
*
|
||||
* material.reset foundation/reset.css — the browser's defaults taken back
|
||||
* material.tokens foundation/tokens.css — every `--md-sys-*` and `--md-ref-*` property
|
||||
* material.tokens tokens/*.css, imported here directly — every `--md-sys-*` and
|
||||
* `--md-ref-*` property
|
||||
* material.base foundation/base.css, foundation/interaction.css — the page, the font
|
||||
* face, and md-state-layer, md-focus-ring, md-touch-target, md-link
|
||||
* material.layout the layout components: scaffold, panes, the canonical layouts
|
||||
@@ -33,7 +34,13 @@
|
||||
|
||||
@import './foundation/reset.css';
|
||||
@import './foundation/hidden.css';
|
||||
@import './foundation/tokens.css';
|
||||
@import './tokens/scheme.css';
|
||||
@import './tokens/shape.css';
|
||||
@import './tokens/elevation.css';
|
||||
@import './tokens/motion.css';
|
||||
@import './tokens/type.css';
|
||||
@import './tokens/state.css';
|
||||
@import './tokens/spacing.css';
|
||||
@import './foundation/base.css';
|
||||
@import './foundation/interaction.css';
|
||||
@import './text.css';
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* The tokens: every `--md-sys-*` and `--md-ref-*` custom property the package reads.
|
||||
*
|
||||
* Each token file wraps its own declarations in `material.tokens`, because an `@import` cannot sit
|
||||
* inside a layer block and `layer()` on the import would nest the layer a second time. Being the
|
||||
* lowest layer but one, a token here loses to the same property declared anywhere an application
|
||||
* writes it: its generated `material-scheme.css`, unlayered, replaces the default scheme below
|
||||
* whatever its selectors, and a typeface or a spacing step is overridden the same way.
|
||||
*
|
||||
* scheme the colour roles of M3's baseline scheme, light and dark, three contrast levels
|
||||
* shape the corner scale
|
||||
* elevation the five shadow levels
|
||||
* motion the springs of both motion schemes and M3's legacy easings
|
||||
* type the 30 type styles and the brand typeface
|
||||
* state the state-layer and disabled opacities
|
||||
* spacing the measurement scale, space25 … space900
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import '../tokens/scheme.css';
|
||||
@import '../tokens/shape.css';
|
||||
@import '../tokens/elevation.css';
|
||||
@import '../tokens/motion.css';
|
||||
@import '../tokens/type.css';
|
||||
@import '../tokens/state.css';
|
||||
@import '../tokens/spacing.css';
|
||||
@@ -24,12 +24,10 @@
|
||||
* bar, the FAB wrapper and `<main>` stack regardless of the row's own display.
|
||||
*
|
||||
* `[data-md-scaffold-actions]`'s column layout is `rail-collapsed:flex-col`, the one Tailwind
|
||||
* variant scaffold.blade.php still used (`resources/css/tailwind.css`, removed with this rule):
|
||||
* written out branch for branch as the navigation rail's own rewrite did for its internal parts
|
||||
* (`resources/css/components/navigation-rail.css`) — the three width-independent conditions in one
|
||||
* `:where()` group, the four width-gated ones each in their own `@media` block, since CSS cannot
|
||||
* merge different queries. Only the "descendant of a collapsed rail" half of each pair applies:
|
||||
* the actions row can never be the rail element itself.
|
||||
* variant scaffold.blade.php still used (`resources/css/tailwind.css`, removed with this rule): the
|
||||
* actions row is always inside a rail, never the rail element itself, so it reads
|
||||
* `--md-navigation-rail-value` with `@container style()` like any other application content does
|
||||
* (`resources/css/components/navigation-rail.css`), rather than copying the rail's own conditions.
|
||||
*
|
||||
* `[data-md-scaffold-bar]` pins the navigation bar to the window's bottom edge below `medium`, at
|
||||
* z-index 30: over the page, under the rail's scrim (40) and the snackbar (50, toast.css). While a
|
||||
@@ -137,39 +135,9 @@
|
||||
padding-block-start: var(--md-sys-measurement-space100);
|
||||
padding-inline: 20px;
|
||||
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 840px) {
|
||||
@media (width < 1200px) {
|
||||
&:where(:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where([data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-scaffold-content] > main {
|
||||
|
||||
@@ -144,53 +144,17 @@
|
||||
}
|
||||
|
||||
/* Shape: a corner swatch, filled with a tonal colour that is decoration only (never meaning,
|
||||
same as the corner it demonstrates), one per corner token. */
|
||||
same as the corner it demonstrates), one per corner token. The swatch is a `<x-surface>`
|
||||
hook (`data-md-surface`, layout/surface.css) for its `data-md-corner` table alone — this
|
||||
rule's own background outranks surface.css's, `material.components` over `material.layout`
|
||||
whatever either's specificity, so the swatch keeps its tonal fill rather than surface.css's
|
||||
default surface-container. */
|
||||
[data-md-showcase-shape-swatch] {
|
||||
display: block;
|
||||
height: 80px;
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
}
|
||||
|
||||
[data-md-showcase-shape-swatch][data-md-corner='none'] {
|
||||
border-radius: var(--md-sys-shape-corner-none);
|
||||
}
|
||||
|
||||
[data-md-showcase-shape-swatch][data-md-corner='xs'] {
|
||||
border-radius: var(--md-sys-shape-corner-xs);
|
||||
}
|
||||
|
||||
[data-md-showcase-shape-swatch][data-md-corner='sm'] {
|
||||
border-radius: var(--md-sys-shape-corner-sm);
|
||||
}
|
||||
|
||||
[data-md-showcase-shape-swatch][data-md-corner='md'] {
|
||||
border-radius: var(--md-sys-shape-corner-md);
|
||||
}
|
||||
|
||||
[data-md-showcase-shape-swatch][data-md-corner='lg'] {
|
||||
border-radius: var(--md-sys-shape-corner-lg);
|
||||
}
|
||||
|
||||
[data-md-showcase-shape-swatch][data-md-corner='lg-increased'] {
|
||||
border-radius: var(--md-sys-shape-corner-lg-increased);
|
||||
}
|
||||
|
||||
[data-md-showcase-shape-swatch][data-md-corner='xl'] {
|
||||
border-radius: var(--md-sys-shape-corner-xl);
|
||||
}
|
||||
|
||||
[data-md-showcase-shape-swatch][data-md-corner='xl-increased'] {
|
||||
border-radius: var(--md-sys-shape-corner-xl-increased);
|
||||
}
|
||||
|
||||
[data-md-showcase-shape-swatch][data-md-corner='xxl'] {
|
||||
border-radius: var(--md-sys-shape-corner-xxl);
|
||||
}
|
||||
|
||||
[data-md-showcase-shape-swatch][data-md-corner='full'] {
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
}
|
||||
|
||||
/* Elevation: a tile at one shadow level, on the surface-container-high M3 pairs a shadow with. */
|
||||
[data-md-showcase-elevation-tile] {
|
||||
display: flex;
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
*
|
||||
* The brand typeface is Google Sans Flex (font.css). Emphasized styles are one weight step
|
||||
* heavier and fully rounded ("ROND" 100) — the axis that typeface exists for, verified by
|
||||
* `npm run check:font` — and they carry their own tracking, because M3 widens four of them and
|
||||
* takes Display Large's negative tracking back to zero.
|
||||
* tests/Feature/FontTest.php — and they carry their own tracking, because M3 widens four of them
|
||||
* and takes Display Large's negative tracking back to zero.
|
||||
*
|
||||
* `--md-ref-typeface-mono` is the package's, not M3's (M3 names a brand and a plain typeface and
|
||||
* nothing for code): the platform's own monospace faces, nothing downloaded, for what a person
|
||||
|
||||
+1802
File diff suppressed because it is too large
Load Diff
Vendored
+592
@@ -0,0 +1,592 @@
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
@layer material.components {
|
||||
[data-md-icon] {
|
||||
flex-shrink: 0;
|
||||
inline-size: var(--md-icon-size, 24px);
|
||||
block-size: var(--md-icon-size, 24px);
|
||||
}
|
||||
|
||||
[data-md-icon][data-md-mirror-rtl]:is([dir='rtl'], [dir='rtl'] *) {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
@layer material.components {
|
||||
[data-md-loading] {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
inline-size: var(--md-loading-size, 48px);
|
||||
block-size: var(--md-loading-size, 48px);
|
||||
color: var(--md-sys-color-primary);
|
||||
|
||||
& > svg {
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
}
|
||||
|
||||
& > [data-md-loading-still] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
& > [data-md-loading-animated] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& > [data-md-loading-still] {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-loading][data-md-contained] {
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
}
|
||||
@layer material.components {
|
||||
[data-md-tooltip-anchor] {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
[data-md-tooltip] {
|
||||
inset: auto;
|
||||
margin: 0;
|
||||
max-inline-size: 200px;
|
||||
overflow: visible;
|
||||
border-width: 0;
|
||||
border-radius: var(--md-sys-shape-corner-xs);
|
||||
padding-block: var(--md-sys-measurement-space50);
|
||||
padding-inline: var(--md-sys-measurement-space100);
|
||||
background-color: var(--md-sys-color-inverse-surface);
|
||||
color: var(--md-sys-color-inverse-on-surface);
|
||||
font: var(--md-sys-typescale-body-sm);
|
||||
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
||||
font-variation-settings: normal;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition-property: opacity;
|
||||
transition-duration: var(--md-sys-motion-effects-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-effects-fast);
|
||||
|
||||
&:popover-open:not([data-md-popover-closing]) {
|
||||
opacity: 1;
|
||||
|
||||
@starting-style {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-tooltip]:is([data-md-side='top'], [data-md-side='bottom']) {
|
||||
margin-block: var(--md-sys-measurement-space50);
|
||||
position-try-fallbacks: flip-block;
|
||||
}
|
||||
|
||||
[data-md-tooltip]:is([data-md-side='left'], [data-md-side='right']) {
|
||||
margin-inline: var(--md-sys-measurement-space50);
|
||||
position-try-fallbacks: flip-inline;
|
||||
}
|
||||
|
||||
[data-md-tooltip][data-md-side='top'] {
|
||||
position-area: top;
|
||||
}
|
||||
|
||||
[data-md-tooltip][data-md-side='bottom'] {
|
||||
position-area: bottom;
|
||||
}
|
||||
|
||||
[data-md-tooltip][data-md-side='left'] {
|
||||
position-area: left;
|
||||
}
|
||||
|
||||
[data-md-tooltip][data-md-side='right'] {
|
||||
position-area: right;
|
||||
}
|
||||
}
|
||||
@layer material.components {
|
||||
@property --md-color {
|
||||
syntax: '*';
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
@property --md-on-color {
|
||||
syntax: '*';
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
@property --md-container {
|
||||
syntax: '*';
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
@property --md-on-container {
|
||||
syntax: '*';
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
[data-md-color='primary'] {
|
||||
--md-color: var(--md-sys-color-primary);
|
||||
--md-on-color: var(--md-sys-color-on-primary);
|
||||
--md-container: var(--md-sys-color-primary-container);
|
||||
--md-on-container: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
[data-md-color='secondary'] {
|
||||
--md-color: var(--md-sys-color-secondary);
|
||||
--md-on-color: var(--md-sys-color-on-secondary);
|
||||
--md-container: var(--md-sys-color-secondary-container);
|
||||
--md-on-container: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
[data-md-color='tertiary'] {
|
||||
--md-color: var(--md-sys-color-tertiary);
|
||||
--md-on-color: var(--md-sys-color-on-tertiary);
|
||||
--md-container: var(--md-sys-color-tertiary-container);
|
||||
--md-on-container: var(--md-sys-color-on-tertiary-container);
|
||||
}
|
||||
|
||||
[data-md-color='error'] {
|
||||
--md-color: var(--md-sys-color-error);
|
||||
--md-on-color: var(--md-sys-color-on-error);
|
||||
--md-container: var(--md-sys-color-error-container);
|
||||
--md-on-container: var(--md-sys-color-on-error-container);
|
||||
}
|
||||
|
||||
[data-md-color='success'] {
|
||||
--md-color: var(--md-sys-color-success);
|
||||
--md-on-color: var(--md-sys-color-on-success);
|
||||
--md-container: var(--md-sys-color-success-container);
|
||||
--md-on-container: var(--md-sys-color-on-success-container);
|
||||
}
|
||||
|
||||
[data-md-color='warning'] {
|
||||
--md-color: var(--md-sys-color-warning);
|
||||
--md-on-color: var(--md-sys-color-on-warning);
|
||||
--md-container: var(--md-sys-color-warning-container);
|
||||
--md-on-container: var(--md-sys-color-on-warning-container);
|
||||
}
|
||||
|
||||
[data-md-color='info'] {
|
||||
--md-color: var(--md-sys-color-info);
|
||||
--md-on-color: var(--md-sys-color-on-info);
|
||||
--md-container: var(--md-sys-color-info-container);
|
||||
--md-on-container: var(--md-sys-color-on-info-container);
|
||||
}
|
||||
}
|
||||
@layer material.components {
|
||||
[data-md-button] {
|
||||
--md-button-color: var(--md-color, var(--md-sys-color-primary));
|
||||
--md-button-on-color: var(--md-on-color, var(--md-sys-color-on-primary));
|
||||
--md-button-tone: var(--md-container, var(--md-sys-color-secondary-container));
|
||||
--md-button-on-tone: var(--md-on-container, var(--md-sys-color-on-secondary-container));
|
||||
--md-button-tone-selected: var(--md-sys-color-secondary);
|
||||
--md-button-on-tone-selected: var(--md-sys-color-on-secondary);
|
||||
--md-button-quiet: var(--md-sys-color-on-surface-variant);
|
||||
|
||||
--md-button-container: transparent;
|
||||
--md-button-label: var(--md-button-color);
|
||||
--md-button-outline: transparent;
|
||||
--md-button-elevation: none;
|
||||
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
block-size: var(--md-button-height);
|
||||
background-color: var(--md-button-container);
|
||||
color: var(--md-button-label);
|
||||
box-shadow: var(--md-button-elevation);
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
transition-property: border-radius, padding, margin, bottom, background-color, color, box-shadow;
|
||||
transition-duration:
|
||||
var(--md-sys-motion-spatial-fast-duration), var(--md-sys-motion-spatial-fast-duration),
|
||||
var(--md-sys-motion-spatial-fast-duration), var(--md-sys-motion-spatial-fast-duration),
|
||||
var(--md-sys-motion-effects-fast-duration), var(--md-sys-motion-effects-fast-duration), var(--md-sys-motion-effects-fast-duration);
|
||||
transition-timing-function:
|
||||
var(--md-sys-motion-spatial-fast), var(--md-sys-motion-spatial-fast),
|
||||
var(--md-sys-motion-spatial-fast), var(--md-sys-motion-spatial-fast),
|
||||
var(--md-sys-motion-effects-fast), var(--md-sys-motion-effects-fast), var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
[data-md-button][data-md-size='xs'] {
|
||||
--md-button-height: 32px;
|
||||
--md-button-padding: var(--md-sys-measurement-space200);
|
||||
--md-button-gap: var(--md-sys-measurement-space100);
|
||||
--md-button-icon: 20px;
|
||||
--md-button-outline-width: 1px;
|
||||
--md-button-square: var(--md-sys-shape-corner-md);
|
||||
--md-button-pressed: var(--md-sys-shape-corner-sm);
|
||||
--md-button-narrow: 28px;
|
||||
--md-button-default: 32px;
|
||||
--md-button-wide: 40px;
|
||||
}
|
||||
|
||||
[data-md-button][data-md-size='sm'] {
|
||||
--md-button-height: 40px;
|
||||
--md-button-padding: var(--md-sys-measurement-space200);
|
||||
--md-button-gap: var(--md-sys-measurement-space100);
|
||||
--md-button-icon: 20px;
|
||||
--md-button-outline-width: 1px;
|
||||
--md-button-square: var(--md-sys-shape-corner-md);
|
||||
--md-button-pressed: var(--md-sys-shape-corner-sm);
|
||||
--md-button-narrow: 32px;
|
||||
--md-button-default: 40px;
|
||||
--md-button-wide: 52px;
|
||||
}
|
||||
|
||||
[data-md-button][data-md-size='sm'][data-md-icon-button] {
|
||||
--md-button-icon: 24px;
|
||||
}
|
||||
|
||||
[data-md-button][data-md-size='md'] {
|
||||
--md-button-height: 56px;
|
||||
--md-button-padding: var(--md-sys-measurement-space300);
|
||||
--md-button-gap: var(--md-sys-measurement-space100);
|
||||
--md-button-icon: 24px;
|
||||
--md-button-outline-width: 1px;
|
||||
--md-button-square: var(--md-sys-shape-corner-lg);
|
||||
--md-button-pressed: var(--md-sys-shape-corner-md);
|
||||
--md-button-narrow: 48px;
|
||||
--md-button-default: 56px;
|
||||
--md-button-wide: 72px;
|
||||
}
|
||||
|
||||
[data-md-button][data-md-size='lg'] {
|
||||
--md-button-height: 96px;
|
||||
--md-button-padding: var(--md-sys-measurement-space600);
|
||||
--md-button-gap: 12px;
|
||||
--md-button-icon: 32px;
|
||||
--md-button-outline-width: 2px;
|
||||
--md-button-square: var(--md-sys-shape-corner-xl);
|
||||
--md-button-pressed: var(--md-sys-shape-corner-lg);
|
||||
--md-button-narrow: 64px;
|
||||
--md-button-default: 96px;
|
||||
--md-button-wide: 128px;
|
||||
}
|
||||
|
||||
[data-md-button][data-md-size='xl'] {
|
||||
--md-button-height: 136px;
|
||||
--md-button-padding: var(--md-sys-measurement-space800);
|
||||
--md-button-gap: var(--md-sys-measurement-space200);
|
||||
--md-button-icon: 40px;
|
||||
--md-button-outline-width: 3px;
|
||||
--md-button-square: var(--md-sys-shape-corner-xl);
|
||||
--md-button-pressed: var(--md-sys-shape-corner-lg);
|
||||
--md-button-narrow: 104px;
|
||||
--md-button-default: 136px;
|
||||
--md-button-wide: 184px;
|
||||
}
|
||||
|
||||
[data-md-button]:not([data-md-icon-button]) {
|
||||
gap: var(--md-button-gap);
|
||||
padding-inline: var(--md-button-padding);
|
||||
}
|
||||
|
||||
[data-md-button]:not([data-md-icon-button]):is([data-md-size='xs'], [data-md-size='sm']) {
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-button]:not([data-md-icon-button])[data-md-size='md'] {
|
||||
font: var(--md-sys-typescale-title-md);
|
||||
letter-spacing: var(--md-sys-typescale-title-md-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-button]:not([data-md-icon-button])[data-md-size='lg'] {
|
||||
font: var(--md-sys-typescale-headline-sm);
|
||||
letter-spacing: var(--md-sys-typescale-headline-sm-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-button]:not([data-md-icon-button])[data-md-size='xl'] {
|
||||
font: var(--md-sys-typescale-headline-lg);
|
||||
letter-spacing: var(--md-sys-typescale-headline-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-button][data-md-icon-button] {
|
||||
--md-button-width: var(--md-button-default);
|
||||
|
||||
inline-size: var(--md-button-width);
|
||||
|
||||
&[data-md-width='narrow'] {
|
||||
--md-button-width: var(--md-button-narrow);
|
||||
}
|
||||
|
||||
&[data-md-width='wide'] {
|
||||
--md-button-width: var(--md-button-wide);
|
||||
}
|
||||
}
|
||||
:where([data-md-button]) {
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
}
|
||||
|
||||
:where([data-md-button]):where([data-md-shape='square'], [data-md-selected='true']) {
|
||||
border-radius: var(--md-button-square);
|
||||
}
|
||||
|
||||
:where([data-md-button]):where([data-md-icon-button][data-md-shape='square'][data-md-selected='true']) {
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
}
|
||||
|
||||
:where([data-md-button]):where(:active) {
|
||||
border-radius: var(--md-button-pressed);
|
||||
}
|
||||
[data-md-button][data-md-color='primary'] {
|
||||
--md-button-tone: var(--md-sys-color-secondary-container);
|
||||
--md-button-on-tone: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
[data-md-button]:not([data-md-color='primary']) {
|
||||
--md-button-tone-selected: var(--md-button-color);
|
||||
--md-button-on-tone-selected: var(--md-button-on-color);
|
||||
--md-button-quiet: var(--md-button-color);
|
||||
}
|
||||
[data-md-button][data-md-variant='filled'] {
|
||||
--md-button-container: var(--md-button-color);
|
||||
--md-button-label: var(--md-button-on-color);
|
||||
|
||||
&[data-md-selected='false'] {
|
||||
--md-button-container: var(--md-sys-color-surface-container);
|
||||
--md-button-label: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-button][data-md-variant='tonal'] {
|
||||
--md-button-container: var(--md-button-tone);
|
||||
--md-button-label: var(--md-button-on-tone);
|
||||
|
||||
&[data-md-selected='true'] {
|
||||
--md-button-container: var(--md-button-tone-selected);
|
||||
--md-button-label: var(--md-button-on-tone-selected);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-button][data-md-variant='outlined'] {
|
||||
--md-button-label: var(--md-button-quiet);
|
||||
--md-button-outline: var(--md-sys-color-outline-variant);
|
||||
|
||||
border: var(--md-button-outline-width) solid var(--md-button-outline);
|
||||
|
||||
&[data-md-selected='true'] {
|
||||
--md-button-container: var(--md-sys-color-inverse-surface);
|
||||
--md-button-label: var(--md-sys-color-inverse-on-surface);
|
||||
--md-button-outline: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-button][data-md-variant='elevated'] {
|
||||
--md-button-container: var(--md-sys-color-surface-container-low);
|
||||
--md-button-elevation: var(--md-sys-elevation-1);
|
||||
|
||||
&[data-md-selected='true'] {
|
||||
--md-button-container: var(--md-button-color);
|
||||
--md-button-label: var(--md-button-on-color);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-button][data-md-variant='text'][data-md-icon-button]:not([data-md-selected='true']) {
|
||||
--md-button-label: var(--md-button-quiet);
|
||||
}
|
||||
|
||||
[data-md-button][data-md-variant='text']:not([data-md-icon-button])[data-md-selected='true'] {
|
||||
--md-button-container: var(--md-button-tone);
|
||||
--md-button-label: var(--md-button-on-tone);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-md-button]:is([data-md-variant='filled'], [data-md-variant='tonal']):hover {
|
||||
--md-button-elevation: var(--md-sys-elevation-1);
|
||||
}
|
||||
|
||||
[data-md-button][data-md-variant='elevated']:hover {
|
||||
--md-button-elevation: var(--md-sys-elevation-2);
|
||||
}
|
||||
}
|
||||
[data-md-button]:is(:disabled, [aria-disabled='true']) {
|
||||
box-shadow: none;
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
}
|
||||
|
||||
[data-md-button]:is([data-md-variant='filled'], [data-md-variant='tonal'], [data-md-variant='elevated'], [data-md-variant='outlined'][data-md-selected='true']):is(:disabled, [aria-disabled='true']) {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
|
||||
}
|
||||
|
||||
[data-md-button]:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
[data-md-button][aria-disabled='true'] {
|
||||
pointer-events: none;
|
||||
}
|
||||
[data-md-button-icon] {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
[data-md-button-spinner] {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
[data-md-button-spinner] > [data-md-loading] {
|
||||
inline-size: var(--md-button-icon);
|
||||
block-size: var(--md-button-icon);
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
[data-md-button] > [data-md-responsive] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
[data-md-button][data-md-compact-fab] {
|
||||
--md-button-height: 56px;
|
||||
--md-button-padding: var(--md-sys-measurement-space200);
|
||||
--md-button-gap: var(--md-sys-measurement-space100);
|
||||
|
||||
position: fixed;
|
||||
inset-inline-end: var(--md-sys-measurement-space200);
|
||||
bottom: calc(var(--material-bottom-bar, 0px) + var(--material-snackbar-height, 0px) + var(--md-sys-measurement-space200));
|
||||
z-index: 30;
|
||||
border-radius: var(--md-sys-shape-corner-lg);
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
box-shadow: var(--md-sys-elevation-3);
|
||||
font: var(--md-sys-typescale-title-md);
|
||||
letter-spacing: var(--md-sys-typescale-title-md-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-button][data-md-compact-fab] [data-md-icon] {
|
||||
inline-size: 24px;
|
||||
block-size: 24px;
|
||||
}
|
||||
[data-md-button][data-md-compact-fab][data-md-unavailable] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@layer material.components {
|
||||
[data-md-shape][style*='--md-shape-size'] {
|
||||
inline-size: var(--md-shape-size);
|
||||
block-size: var(--md-shape-size);
|
||||
}
|
||||
}
|
||||
@layer material.components {
|
||||
body:has(> [data-md-error-page]) {
|
||||
margin: 0;
|
||||
min-height: 100dvh;
|
||||
background-color: var(--md-sys-color-surface);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
[data-md-error-page] {
|
||||
display: flex;
|
||||
min-height: 100dvh;
|
||||
max-width: 576px;
|
||||
margin-inline: auto;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-inline: var(--md-sys-measurement-space300);
|
||||
padding-block: var(--md-sys-measurement-space600);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-md-error-art] {
|
||||
position: relative;
|
||||
display: grid;
|
||||
flex-shrink: 0;
|
||||
place-items: center;
|
||||
width: 192px;
|
||||
height: 192px;
|
||||
}
|
||||
|
||||
@media (width >= 600px) {
|
||||
[data-md-error-art] {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-error-shape] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
[data-md-error-shape] svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--md-sys-color-primary-container);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
[data-md-error-shape] {
|
||||
animation: material-error-turn 60s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes material-error-turn {
|
||||
to {
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-error-code] {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
font: var(--md-sys-typescale-emphasized-display-lg);
|
||||
letter-spacing: var(--md-sys-typescale-emphasized-display-lg-tracking);
|
||||
font-variation-settings: 'ROND' 100;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
[data-md-error-headline] {
|
||||
margin: var(--md-sys-measurement-space500) 0 0;
|
||||
font: var(--md-sys-typescale-headline-md);
|
||||
letter-spacing: var(--md-sys-typescale-headline-md-tracking);
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
@media (width >= 600px) {
|
||||
[data-md-error-headline] {
|
||||
font: var(--md-sys-typescale-headline-lg);
|
||||
letter-spacing: var(--md-sys-typescale-headline-lg-tracking);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-error-message] {
|
||||
margin: 12px 0 0;
|
||||
max-width: 448px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-lg);
|
||||
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
[data-md-error-actions] {
|
||||
display: flex;
|
||||
margin-top: var(--md-sys-measurement-space500);
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
Vendored
+10252
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* `materialBottomSheet(standard, presets)`: the behaviour of `<x-bottom-sheet>`, spread into its
|
||||
* x-data alongside `open` (entangled with Livewire, or the surrounding Alpine scope's).
|
||||
* `materialBottomSheet(presets)`: the behaviour of `<x-bottom-sheet>`, spread into its x-data
|
||||
* alongside `open` (entangled with Livewire, or the surrounding Alpine scope's).
|
||||
*
|
||||
* Without preset heights the sheet is as tall as its content allows and a downward drag follows the
|
||||
* pointer: released past a quarter of the sheet's height or flicked down, it closes; otherwise it
|
||||
@@ -26,8 +26,7 @@ const TOP_MARGIN = 72
|
||||
|
||||
const clamp = (value, min, max) => Math.min(Math.max(value, min), max)
|
||||
|
||||
window.materialBottomSheet = (standard = false, presets = {}) => ({
|
||||
standard,
|
||||
window.materialBottomSheet = (presets = {}) => ({
|
||||
stops: presets.stops ?? [],
|
||||
labels: presets.labels ?? {},
|
||||
start: presets.start ?? 0,
|
||||
@@ -142,10 +141,10 @@ window.materialBottomSheet = (standard = false, presets = {}) => ({
|
||||
}
|
||||
}
|
||||
|
||||
const controller = new AbortController()
|
||||
|
||||
const end = () => {
|
||||
window.removeEventListener('pointermove', move)
|
||||
window.removeEventListener('pointerup', end)
|
||||
window.removeEventListener('pointercancel', end)
|
||||
controller.abort()
|
||||
|
||||
this.dragging = false
|
||||
|
||||
@@ -158,9 +157,9 @@ window.materialBottomSheet = (standard = false, presets = {}) => ({
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('pointermove', move)
|
||||
window.addEventListener('pointerup', end)
|
||||
window.addEventListener('pointercancel', end)
|
||||
window.addEventListener('pointermove', move, { signal: controller.signal })
|
||||
window.addEventListener('pointerup', end, { signal: controller.signal })
|
||||
window.addEventListener('pointercancel', end, { signal: controller.signal })
|
||||
},
|
||||
|
||||
/** A preset-height drag let go: the nearest stop, or out past the smallest one. */
|
||||
|
||||
+16
-41
@@ -226,34 +226,18 @@ const isLastFocalItemAtEndOfContainer = (list, space) => {
|
||||
}
|
||||
|
||||
const firstIndexAfterFocalRangeWithSize = (list, size) => {
|
||||
for (let index = list.lastFocalIndex; index < list.keylines.length; index++) {
|
||||
if (list.keylines[index].size === size) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
const index = list.keylines.findIndex((keyline, i) => i >= list.lastFocalIndex && keyline.size === size)
|
||||
|
||||
return list.keylines.length - 1
|
||||
return index === -1 ? list.keylines.length - 1 : index
|
||||
}
|
||||
|
||||
const lastIndexBeforeFocalRangeWithSize = (list, size) => {
|
||||
for (let index = list.firstFocalIndex - 1; index >= 0; index--) {
|
||||
if (list.keylines[index].size === size) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
const index = list.keylines.findLastIndex((keyline, i) => i < list.firstFocalIndex && keyline.size === size)
|
||||
|
||||
return 0
|
||||
return index === -1 ? 0 : index
|
||||
}
|
||||
|
||||
const keylineBefore = (list, unadjustedOffset) => {
|
||||
for (let index = list.keylines.length - 1; index >= 0; index--) {
|
||||
if (list.keylines[index].unadjustedOffset < unadjustedOffset) {
|
||||
return list.keylines[index]
|
||||
}
|
||||
}
|
||||
|
||||
return list.keylines[0]
|
||||
}
|
||||
const keylineBefore = (list, unadjustedOffset) => list.keylines.findLast((keyline) => keyline.unadjustedOffset < unadjustedOffset) ?? list.keylines[0]
|
||||
|
||||
const keylineAfter = (list, unadjustedOffset) => list.keylines.find((keyline) => keyline.unadjustedOffset >= unadjustedOffset) ?? list.keylines.at(-1)
|
||||
|
||||
@@ -817,7 +801,7 @@ document.addEventListener('alpine:init', () => {
|
||||
target: null,
|
||||
targetAt: 0,
|
||||
settle: null,
|
||||
listeners: [],
|
||||
controller: null,
|
||||
mutations: null,
|
||||
resizes: null,
|
||||
reducedMotion: null,
|
||||
@@ -828,17 +812,20 @@ document.addEventListener('alpine:init', () => {
|
||||
const scroller = this.$refs.scroller
|
||||
|
||||
state.reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)')
|
||||
state.controller = new AbortController()
|
||||
|
||||
this.listen(scroller, 'scroll', () => this.scrolled(), { passive: true })
|
||||
const signal = state.controller.signal
|
||||
|
||||
scroller.addEventListener('scroll', () => this.scrolled(), { passive: true, signal })
|
||||
|
||||
// A scroll the person makes themselves is theirs to end wherever it ends.
|
||||
for (const type of ['pointerdown', 'wheel', 'touchstart']) {
|
||||
this.listen(scroller, type, () => (state.target = null), { passive: true })
|
||||
scroller.addEventListener(type, () => (state.target = null), { passive: true, signal })
|
||||
}
|
||||
this.listen(scroller, 'keydown', (event) => this.navigate(event))
|
||||
this.listen(scroller, 'focusin', (event) => this.reveal(event))
|
||||
this.listen(scroller, 'click', (event) => this.open(event))
|
||||
this.listen(state.reducedMotion, 'change', () => this.schedule())
|
||||
scroller.addEventListener('keydown', (event) => this.navigate(event), { signal })
|
||||
scroller.addEventListener('focusin', (event) => this.reveal(event), { signal })
|
||||
scroller.addEventListener('click', (event) => this.open(event), { signal })
|
||||
state.reducedMotion.addEventListener('change', () => this.schedule(), { signal })
|
||||
|
||||
state.resizes = new ResizeObserver(() => this.refresh())
|
||||
state.resizes.observe(scroller)
|
||||
@@ -859,11 +846,6 @@ document.addEventListener('alpine:init', () => {
|
||||
return element === this.$root || state.items.some((item) => item.element === element || item.surface === element)
|
||||
},
|
||||
|
||||
listen(target, type, handler, options) {
|
||||
target.addEventListener(type, handler, options)
|
||||
state.listeners.push(() => target.removeEventListener(type, handler, options))
|
||||
},
|
||||
|
||||
/** Measures the container and items, and builds the strategy for this width. */
|
||||
refresh() {
|
||||
const root = this.$root
|
||||
@@ -1098,13 +1080,6 @@ document.addEventListener('alpine:init', () => {
|
||||
})
|
||||
},
|
||||
|
||||
/** The item nearest the current scroll position. */
|
||||
current() {
|
||||
const scroll = this.scrollOffset()
|
||||
|
||||
return state.snaps.reduce((best, snap, index) => (Math.abs(snap - scroll) < Math.abs(state.snaps[best] - scroll) ? index : best), 0)
|
||||
},
|
||||
|
||||
/** Where the carousel is headed: an unfinished scroll's target, or where it is. */
|
||||
heading() {
|
||||
const moving = state.target !== null && performance.now() - state.targetAt < TARGET_MS
|
||||
@@ -1235,7 +1210,7 @@ document.addEventListener('alpine:init', () => {
|
||||
destroy() {
|
||||
cancelAnimationFrame(state.frame)
|
||||
clearTimeout(state.settle)
|
||||
state.listeners.forEach((remove) => remove())
|
||||
state.controller?.abort()
|
||||
state.resizes?.disconnect()
|
||||
state.mutations?.disconnect()
|
||||
},
|
||||
|
||||
+5
-14
@@ -19,6 +19,8 @@
|
||||
* and Home and End go to its ends. The roving mark is re-applied whenever the set changes, because
|
||||
* a Livewire morph rewrites the chips underneath it.
|
||||
*/
|
||||
import { ringIndex } from './util.js'
|
||||
|
||||
const CONTROLS = 'button:not(:disabled), a[href]:not([tabindex="-1"]), input:not(:disabled):not([type="hidden"])'
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
@@ -216,20 +218,9 @@ document.addEventListener('alpine:init', () => {
|
||||
return
|
||||
}
|
||||
|
||||
const forwards = getComputedStyle(this.row).direction === 'rtl' ? 'ArrowLeft' : 'ArrowRight'
|
||||
const backwards = forwards === 'ArrowRight' ? 'ArrowLeft' : 'ArrowRight'
|
||||
|
||||
let next = null
|
||||
|
||||
if (event.key === forwards || event.key === 'ArrowDown') {
|
||||
next = (index + 1) % controls.length
|
||||
} else if (event.key === backwards || event.key === 'ArrowUp') {
|
||||
next = (index - 1 + controls.length) % controls.length
|
||||
} else if (event.key === 'Home') {
|
||||
next = 0
|
||||
} else if (event.key === 'End') {
|
||||
next = controls.length - 1
|
||||
}
|
||||
const rtl = getComputedStyle(this.row).direction === 'rtl'
|
||||
// Up and Down move the row too, unmirrored, beside whichever of Left and Right does.
|
||||
const next = ringIndex(event.key, index, controls.length, { rtl }) ?? ringIndex(event.key, index, controls.length, { vertical: true })
|
||||
|
||||
if (next === null) {
|
||||
return
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
* opens or closes it at once, as the browser does, and this follows along. Under reduced motion the
|
||||
* duration token is zero and every route is the browser's own, instant.
|
||||
*/
|
||||
import { ms } from './util.js'
|
||||
|
||||
const COLLAPSE = 'details[data-md-collapse]'
|
||||
const CLOSING = 'data-md-collapse-closing'
|
||||
|
||||
@@ -33,8 +35,6 @@ const targets = new WeakMap()
|
||||
const animations = new WeakMap()
|
||||
const lifted = new WeakMap()
|
||||
|
||||
const milliseconds = (value) => parseFloat(value) * (value.trim().endsWith('ms') ? 1 : 1000) || 0
|
||||
|
||||
/** The height the section is drawn at, closed: its summary, and its own padding and border. */
|
||||
const closedHeight = (details) => {
|
||||
const style = getComputedStyle(details)
|
||||
@@ -59,7 +59,7 @@ const restoreName = (details) => {
|
||||
/** Moves the height from where it is drawn now to `to`, then `done`. Returns false under reduced motion. */
|
||||
const animate = (details, from, to, done) => {
|
||||
const style = getComputedStyle(details)
|
||||
const duration = milliseconds(style.getPropertyValue('--md-sys-motion-spatial-fast-duration'))
|
||||
const duration = ms(details, '--md-sys-motion-spatial-fast-duration') ?? 0
|
||||
|
||||
if (duration === 0) {
|
||||
return false
|
||||
@@ -185,7 +185,7 @@ document.addEventListener('click', (event) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (milliseconds(getComputedStyle(details).getPropertyValue('--md-sys-motion-spatial-fast-duration')) === 0) {
|
||||
if ((ms(details, '--md-sys-motion-spatial-fast-duration') ?? 0) === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+13
-23
@@ -23,29 +23,28 @@ document.addEventListener('alpine:init', () => {
|
||||
window.Alpine.data('materialFab', () => ({
|
||||
collapsed: false,
|
||||
lastY: 0,
|
||||
ticking: false,
|
||||
listeners: [],
|
||||
frame: null,
|
||||
// Set in init(). Declared here, or Alpine writes them to the outermost x-data scope,
|
||||
// where a second FAB in the same page scope would take the first one's listener.
|
||||
schedule: null,
|
||||
|
||||
init() {
|
||||
this.lastY = Math.max(window.scrollY, 0)
|
||||
this.listen(window, 'scroll', () => this.queue(), { passive: true })
|
||||
},
|
||||
|
||||
/** One reading a frame: `scroll` fires far more often than anything can be drawn. */
|
||||
queue() {
|
||||
if (this.ticking) {
|
||||
return
|
||||
this.schedule = () => {
|
||||
this.frame ??= requestAnimationFrame(() => this.measure())
|
||||
}
|
||||
|
||||
this.ticking = true
|
||||
window.addEventListener('scroll', this.schedule, { passive: true })
|
||||
},
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.ticking = false
|
||||
this.measure()
|
||||
})
|
||||
destroy() {
|
||||
window.removeEventListener('scroll', this.schedule)
|
||||
cancelAnimationFrame(this.frame)
|
||||
},
|
||||
|
||||
measure() {
|
||||
this.frame = null
|
||||
|
||||
const y = Math.max(window.scrollY, 0)
|
||||
const moved = y - this.lastY
|
||||
|
||||
@@ -61,14 +60,5 @@ document.addEventListener('alpine:init', () => {
|
||||
this.lastY = y
|
||||
}
|
||||
},
|
||||
|
||||
listen(target, type, handler, options) {
|
||||
target.addEventListener(type, handler, options)
|
||||
this.listeners.push(() => target.removeEventListener(type, handler, options))
|
||||
},
|
||||
|
||||
destroy() {
|
||||
this.listeners.forEach((remove) => remove())
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
+11
-9
@@ -23,14 +23,21 @@ function grow(textarea) {
|
||||
textarea.style.height = `${textarea.scrollHeight + textarea.offsetHeight - textarea.clientHeight}px`
|
||||
}
|
||||
|
||||
/** `root` itself, if it matches `selector`, together with its matching descendants. */
|
||||
function matching(root, selector) {
|
||||
const descendants = [...root.querySelectorAll(selector)]
|
||||
|
||||
return root instanceof Element && root.matches(selector) ? [root, ...descendants] : descendants
|
||||
}
|
||||
|
||||
function growAll(root) {
|
||||
if (! growsByItself) {
|
||||
root.querySelectorAll(GROWING).forEach(grow)
|
||||
matching(root, GROWING).forEach(grow)
|
||||
}
|
||||
}
|
||||
|
||||
function markAll(root) {
|
||||
root.querySelectorAll(MIXED).forEach((input) => (input.indeterminate = true))
|
||||
matching(root, MIXED).forEach((input) => (input.indeterminate = true))
|
||||
}
|
||||
|
||||
if (! growsByItself) {
|
||||
@@ -63,13 +70,8 @@ new MutationObserver((records) => {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const input of [node, ...node.querySelectorAll(MIXED)].filter((element) => element.matches(MIXED))) {
|
||||
input.indeterminate = true
|
||||
}
|
||||
|
||||
if (! growsByItself) {
|
||||
;[node, ...node.querySelectorAll(GROWING)].filter((element) => element.matches(GROWING)).forEach(grow)
|
||||
}
|
||||
markAll(node)
|
||||
growAll(node)
|
||||
}
|
||||
}
|
||||
}).observe(document.documentElement, {
|
||||
|
||||
+3
-10
@@ -17,6 +17,8 @@
|
||||
* mid-count. The observer ignores the directive's own writes by comparing against the last
|
||||
* thing it wrote.
|
||||
*/
|
||||
import { ms } from './util.js'
|
||||
|
||||
const FIGURE = /[-−]?\d{1,3}(?:[,'’]\d{3})+(?:\.\d+)?|[-−]?\d+(?:\.\d+)?/g
|
||||
|
||||
const readFigure = (text) => {
|
||||
@@ -54,15 +56,6 @@ document.addEventListener('alpine:init', () => {
|
||||
window.Alpine.directive('figure', (el, _, { cleanup }) => {
|
||||
const text = () => (el.childNodes.length === 1 && el.firstChild.nodeType === Node.TEXT_NODE ? el.firstChild : null)
|
||||
|
||||
// In milliseconds, whichever unit arrives: a minifier turns `600ms` into `.6s`, and a
|
||||
// bare parseFloat would read that as 0.6ms — an instant count.
|
||||
const duration = () => {
|
||||
const token = getComputedStyle(document.documentElement).getPropertyValue('--md-sys-motion-spatial-slow-duration').trim()
|
||||
const value = parseFloat(token) || 0
|
||||
|
||||
return token.endsWith('ms') ? value : value * 1000
|
||||
}
|
||||
|
||||
let written = text()?.data ?? null
|
||||
let frame = null
|
||||
|
||||
@@ -70,7 +63,7 @@ document.addEventListener('alpine:init', () => {
|
||||
cancelAnimationFrame(frame)
|
||||
|
||||
const node = text()
|
||||
const length = duration()
|
||||
const length = ms(document.documentElement, '--md-sys-motion-spatial-slow-duration') ?? 0
|
||||
|
||||
if (!node || !target || length === 0 || from === target.value) {
|
||||
return
|
||||
|
||||
+13
-10
@@ -85,6 +85,17 @@ document.addEventListener('keydown', (event) => {
|
||||
}
|
||||
}, true)
|
||||
|
||||
/** The nearest layer matching `selector` around `target` that is open, walking out past shut ones. */
|
||||
const nearestOpen = (target, selector) => {
|
||||
let layer = target instanceof Element ? target.closest(selector) : null
|
||||
|
||||
while (layer !== null && !isOpen(layer)) {
|
||||
layer = layer.parentElement?.closest(selector) ?? null
|
||||
}
|
||||
|
||||
return layer
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a `focusin` is an open modal layer moving the focus into itself (see the header): the
|
||||
* focus arrives from outside the nearest open layer around its target, or from nowhere, and no Tab
|
||||
@@ -96,11 +107,7 @@ export const openingFocus = (event) => {
|
||||
return false
|
||||
}
|
||||
|
||||
let layer = event.target.closest(MODAL_LAYERS)
|
||||
|
||||
while (layer !== null && !isOpen(layer)) {
|
||||
layer = layer.parentElement?.closest(MODAL_LAYERS) ?? null
|
||||
}
|
||||
const layer = nearestOpen(event.target, MODAL_LAYERS)
|
||||
|
||||
return layer !== null && !(event.relatedTarget instanceof Node && layer.contains(event.relatedTarget))
|
||||
}
|
||||
@@ -111,11 +118,7 @@ const owns = (panel, event) => {
|
||||
return false
|
||||
}
|
||||
|
||||
let layer = event.target instanceof Element ? event.target.closest(LAYERS) : null
|
||||
|
||||
while (layer !== null && !isOpen(layer)) {
|
||||
layer = layer.parentElement?.closest(LAYERS) ?? null
|
||||
}
|
||||
const layer = nearestOpen(event.target, LAYERS)
|
||||
|
||||
return layer === null ? panels.at(-1) === panel : layer === panel
|
||||
}
|
||||
|
||||
+512
-535
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,7 @@
|
||||
* `materialNavigationBar` is `<x-navigation-bar hide-on-scroll>` — see the same file's sibling.
|
||||
*/
|
||||
import { from, upTo } from './breakpoints.js'
|
||||
import { remember } from './util.js'
|
||||
|
||||
/** The root of every interactive rail on the page once its view is initialised: what `toggle()` asks. */
|
||||
const rails = new Set()
|
||||
@@ -102,11 +103,7 @@ document.addEventListener('alpine:init', () => {
|
||||
root.dataset.rail = collapsed ? 'collapsed' : 'expanded'
|
||||
root.removeAttribute('data-rail-auto')
|
||||
|
||||
try {
|
||||
localStorage.setItem(root.dataset.railKey || 'material-rail', root.dataset.rail)
|
||||
} catch {
|
||||
// Blocked storage: the rail still toggles, it just will not remember.
|
||||
}
|
||||
remember(root.dataset.railKey || 'material-rail', root.dataset.rail)
|
||||
},
|
||||
|
||||
show() {
|
||||
@@ -125,8 +122,7 @@ document.addEventListener('alpine:init', () => {
|
||||
wide: mode === 'adaptive' ? from('expanded').matches : false,
|
||||
roomy: mode === 'adaptive' ? from('large').matches : false,
|
||||
tight: mode === 'collapsible' ? upTo('medium').matches : false,
|
||||
queries: [],
|
||||
listeners: [],
|
||||
controller: null,
|
||||
|
||||
/**
|
||||
* `data-md-closing` while a rail that was open over the page leaves it: `sheet` when the
|
||||
@@ -144,6 +140,7 @@ document.addEventListener('alpine:init', () => {
|
||||
|
||||
init() {
|
||||
rails.add(this.$root)
|
||||
this.controller = new AbortController()
|
||||
|
||||
if (mode !== 'adaptive') {
|
||||
// Below `medium` a collapsible rail is held at its collapsed width whatever the
|
||||
@@ -181,16 +178,12 @@ document.addEventListener('alpine:init', () => {
|
||||
},
|
||||
|
||||
watch(query, onChange) {
|
||||
const listener = (event) => onChange(event.matches)
|
||||
|
||||
query.addEventListener('change', listener)
|
||||
this.queries.push(query)
|
||||
this.listeners.push(listener)
|
||||
query.addEventListener('change', (event) => onChange(event.matches), { signal: this.controller.signal })
|
||||
},
|
||||
|
||||
destroy() {
|
||||
rails.delete(this.$root)
|
||||
this.queries.forEach((query, index) => query.removeEventListener('change', this.listeners[index]))
|
||||
this.controller.abort()
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,9 +36,6 @@ const longestTransition = (element) => {
|
||||
return Math.max(0, ...durations.map((duration, index) => duration + (delays[index % delays.length] || 0)))
|
||||
}
|
||||
|
||||
/** Out of the DOM, which takes it off the top layer without a `beforetoggle` or `toggle` of its own. */
|
||||
const remove = (ghost) => ghost.remove()
|
||||
|
||||
/**
|
||||
* A still copy of the popover, pinned where it is drawn. Colours come from custom properties an
|
||||
* ancestor may set (a vibrant menu's `--material-menu-surface`), which the copy loses at the end of
|
||||
@@ -97,7 +94,8 @@ const exit = ({ ghost, scrollTop }, popover) => {
|
||||
|
||||
ghosts.set(popover, ghost)
|
||||
setTimeout(() => {
|
||||
remove(ghost)
|
||||
// Out of the DOM, which takes it off the top layer without a `beforetoggle` or `toggle` of its own.
|
||||
ghost.remove()
|
||||
|
||||
if (ghosts.get(popover) === ghost) {
|
||||
ghosts.delete(popover)
|
||||
@@ -118,7 +116,7 @@ document.addEventListener(
|
||||
|
||||
if (previous) {
|
||||
ghosts.delete(popover)
|
||||
remove(previous)
|
||||
previous.remove()
|
||||
}
|
||||
|
||||
if (event.newState !== 'closed') {
|
||||
|
||||
+10
-636
@@ -33,9 +33,8 @@
|
||||
* compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/ShapeUtil.kt
|
||||
* compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/*ProgressIndicatorTokens.kt
|
||||
* compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt (keyframes)
|
||||
* graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/*.kt (via bin/shapes.mjs
|
||||
* and bin/loading-indicator.mjs: RoundedPolygon, CornerRounding, Cubic, Morph, FeatureMapping,
|
||||
* PolygonMeasure)
|
||||
* graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/*.kt (via shapes.js:
|
||||
* RoundedPolygon, CornerRounding, Cubic, Morph, FeatureMapping, PolygonMeasure)
|
||||
*
|
||||
* Copyright 2022-2025 The Android Open Source Project
|
||||
*
|
||||
@@ -53,6 +52,9 @@
|
||||
* ---------------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import { asCubics, circlePolygon, match, point, rounding, star as starPolygon } from './shapes.js'
|
||||
import { ms } from './util.js'
|
||||
|
||||
const SVG = 'http://www.w3.org/2000/svg'
|
||||
const WATCHED = ['data-md-value', 'data-md-max', 'data-md-circular', 'data-md-wavy', 'data-md-thick']
|
||||
|
||||
@@ -289,624 +291,6 @@ function waveSegment(from, to, halfWavelength, height, shift, middle, amplitude)
|
||||
return d
|
||||
}
|
||||
|
||||
// Shapes: bin/shapes.mjs (RoundedPolygon) --------------------------------------------------
|
||||
|
||||
const DISTANCE_EPSILON = 1e-4
|
||||
const ANGLE_EPSILON = 1e-6
|
||||
|
||||
const point = (x, y) => ({ x, y })
|
||||
const plus = (a, b) => point(a.x + b.x, a.y + b.y)
|
||||
const minus = (a, b) => point(a.x - b.x, a.y - b.y)
|
||||
const times = (a, k) => point(a.x * k, a.y * k)
|
||||
const div = (a, k) => point(a.x / k, a.y / k)
|
||||
const dot = (a, b) => a.x * b.x + a.y * b.y
|
||||
const length = (a) => Math.sqrt(a.x * a.x + a.y * a.y)
|
||||
const rotate90 = (a) => point(-a.y, a.x)
|
||||
const lerp = (a, b, f) => (1 - f) * a + f * b
|
||||
const lerpPoint = (a, b, f) => point(lerp(a.x, b.x, f), lerp(a.y, b.y, f))
|
||||
const direction = (a) => div(a, length(a))
|
||||
const radialToCartesian = (radius, angle) => point(Math.cos(angle) * radius, Math.sin(angle) * radius)
|
||||
const convex = (previous, current, next) => {
|
||||
const [a, b] = [minus(current, previous), minus(next, current)]
|
||||
|
||||
return a.x * b.y - a.y * b.x > 0
|
||||
}
|
||||
|
||||
/** A cubic is [anchor0X, anchor0Y, control0X, control0Y, control1X, control1Y, anchor1X, anchor1Y]. */
|
||||
const cubic = (a0, c0, c1, a1) => [a0.x, a0.y, c0.x, c0.y, c1.x, c1.y, a1.x, a1.y]
|
||||
|
||||
const straightLine = (x0, y0, x1, y1) => [x0, y0, lerp(x0, x1, 1 / 3), lerp(y0, y1, 1 / 3), lerp(x0, x1, 2 / 3), lerp(y0, y1, 2 / 3), x1, y1]
|
||||
|
||||
function circularArc(centerX, centerY, x0, y0, x1, y1) {
|
||||
const p0d = direction(point(x0 - centerX, y0 - centerY))
|
||||
const p1d = direction(point(x1 - centerX, y1 - centerY))
|
||||
const rotatedP0 = rotate90(p0d)
|
||||
const rotatedP1 = rotate90(p1d)
|
||||
const clockwise = dot(rotatedP0, point(x1 - centerX, y1 - centerY)) >= 0
|
||||
const cosa = dot(p0d, p1d)
|
||||
|
||||
if (cosa > 0.999) {
|
||||
return straightLine(x0, y0, x1, y1)
|
||||
}
|
||||
|
||||
const k =
|
||||
(((length(point(x0 - centerX, y0 - centerY)) * 4) / 3) * (Math.sqrt(2 * (1 - cosa)) - Math.sqrt(1 - cosa * cosa))) /
|
||||
(1 - cosa) *
|
||||
(clockwise ? 1 : -1)
|
||||
|
||||
return [x0, y0, x0 + rotatedP0.x * k, y0 + rotatedP0.y * k, x1 - rotatedP1.x * k, y1 - rotatedP1.y * k, x1, y1]
|
||||
}
|
||||
|
||||
function pointOnCurve(c, t) {
|
||||
const u = 1 - t
|
||||
|
||||
return point(
|
||||
c[0] * (u * u * u) + c[2] * (3 * t * u * u) + c[4] * (3 * t * t * u) + c[6] * (t * t * t),
|
||||
c[1] * (u * u * u) + c[3] * (3 * t * u * u) + c[5] * (3 * t * t * u) + c[7] * (t * t * t),
|
||||
)
|
||||
}
|
||||
|
||||
function split(c, t) {
|
||||
const u = 1 - t
|
||||
const p = pointOnCurve(c, t)
|
||||
|
||||
return [
|
||||
[c[0], c[1], c[0] * u + c[2] * t, c[1] * u + c[3] * t, c[0] * (u * u) + c[2] * (2 * u * t) + c[4] * (t * t), c[1] * (u * u) + c[3] * (2 * u * t) + c[5] * (t * t), p.x, p.y],
|
||||
[p.x, p.y, c[2] * (u * u) + c[4] * (2 * u * t) + c[6] * (t * t), c[3] * (u * u) + c[5] * (2 * u * t) + c[7] * (t * t), c[4] * u + c[6] * t, c[5] * u + c[7] * t, c[6], c[7]],
|
||||
]
|
||||
}
|
||||
|
||||
const reverse = (c) => [c[6], c[7], c[4], c[5], c[2], c[3], c[0], c[1]]
|
||||
const zeroLength = (c) => Math.abs(c[0] - c[6]) < DISTANCE_EPSILON && Math.abs(c[1] - c[7]) < DISTANCE_EPSILON
|
||||
|
||||
const rounding = (radius = 0, smoothing = 0) => ({ radius, smoothing })
|
||||
|
||||
class RoundedCorner {
|
||||
constructor(p0, p1, p2, cornerRounding) {
|
||||
this.p0 = p0
|
||||
this.p1 = p1
|
||||
this.p2 = p2
|
||||
|
||||
const v01 = minus(p0, p1)
|
||||
const v21 = minus(p2, p1)
|
||||
const d01 = length(v01)
|
||||
const d21 = length(v21)
|
||||
|
||||
if (d01 > 0 && d21 > 0) {
|
||||
this.d1 = div(v01, d01)
|
||||
this.d2 = div(v21, d21)
|
||||
this.cornerRadius = cornerRounding?.radius ?? 0
|
||||
this.smoothing = cornerRounding?.smoothing ?? 0
|
||||
this.cosAngle = dot(this.d1, this.d2)
|
||||
this.sinAngle = Math.sqrt(1 - this.cosAngle * this.cosAngle)
|
||||
this.expectedRoundCut = this.sinAngle > 1e-3 ? (this.cornerRadius * (this.cosAngle + 1)) / this.sinAngle : 0
|
||||
} else {
|
||||
this.d1 = point(0, 0)
|
||||
this.d2 = point(0, 0)
|
||||
this.cornerRadius = 0
|
||||
this.smoothing = 0
|
||||
this.cosAngle = 0
|
||||
this.sinAngle = 0
|
||||
this.expectedRoundCut = 0
|
||||
}
|
||||
}
|
||||
|
||||
get expectedCut() {
|
||||
return (1 + this.smoothing) * this.expectedRoundCut
|
||||
}
|
||||
|
||||
getCubics(allowedCut0, allowedCut1 = allowedCut0) {
|
||||
const allowedCut = Math.min(allowedCut0, allowedCut1)
|
||||
|
||||
if (this.expectedRoundCut < DISTANCE_EPSILON || allowedCut < DISTANCE_EPSILON || this.cornerRadius < DISTANCE_EPSILON) {
|
||||
return [straightLine(this.p1.x, this.p1.y, this.p1.x, this.p1.y)]
|
||||
}
|
||||
|
||||
const actualRoundCut = Math.min(allowedCut, this.expectedRoundCut)
|
||||
const actualSmoothing0 = this.actualSmoothing(allowedCut0)
|
||||
const actualSmoothing1 = this.actualSmoothing(allowedCut1)
|
||||
const actualR = (this.cornerRadius * actualRoundCut) / this.expectedRoundCut
|
||||
const centerDistance = Math.sqrt(actualR * actualR + actualRoundCut * actualRoundCut)
|
||||
const center = plus(this.p1, times(direction(div(plus(this.d1, this.d2), 2)), centerDistance))
|
||||
const circleIntersection0 = plus(this.p1, times(this.d1, actualRoundCut))
|
||||
const circleIntersection2 = plus(this.p1, times(this.d2, actualRoundCut))
|
||||
const flanking0 = this.flankingCurve(actualRoundCut, actualSmoothing0, this.p1, this.p0, circleIntersection0, circleIntersection2, center, actualR)
|
||||
const flanking2 = reverse(this.flankingCurve(actualRoundCut, actualSmoothing1, this.p1, this.p2, circleIntersection2, circleIntersection0, center, actualR))
|
||||
|
||||
return [flanking0, circularArc(center.x, center.y, flanking0[6], flanking0[7], flanking2[0], flanking2[1]), flanking2]
|
||||
}
|
||||
|
||||
actualSmoothing(allowedCut) {
|
||||
if (allowedCut > this.expectedCut) {
|
||||
return this.smoothing
|
||||
}
|
||||
|
||||
if (allowedCut > this.expectedRoundCut) {
|
||||
return (this.smoothing * (allowedCut - this.expectedRoundCut)) / (this.expectedCut - this.expectedRoundCut)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
flankingCurve(actualRoundCut, smoothing, corner, sideStart, intersection, otherIntersection, circleCenter, actualR) {
|
||||
const sideDirection = direction(minus(sideStart, corner))
|
||||
const curveStart = plus(corner, times(sideDirection, actualRoundCut * (1 + smoothing)))
|
||||
const p = lerpPoint(intersection, div(plus(intersection, otherIntersection), 2), smoothing)
|
||||
const curveEnd = plus(circleCenter, times(direction(minus(p, circleCenter)), actualR))
|
||||
const circleTangent = rotate90(minus(curveEnd, circleCenter))
|
||||
const anchorEnd = lineIntersection(sideStart, sideDirection, curveEnd, circleTangent) ?? intersection
|
||||
const anchorStart = div(plus(curveStart, times(anchorEnd, 2)), 3)
|
||||
|
||||
return cubic(curveStart, anchorStart, anchorEnd, curveEnd)
|
||||
}
|
||||
}
|
||||
|
||||
function lineIntersection(p0, d0, p1, d1) {
|
||||
const rotatedD1 = rotate90(d1)
|
||||
const den = dot(d0, rotatedD1)
|
||||
|
||||
if (Math.abs(den) < DISTANCE_EPSILON) {
|
||||
return null
|
||||
}
|
||||
|
||||
const num = dot(minus(p1, p0), rotatedD1)
|
||||
|
||||
if (Math.abs(den) < DISTANCE_EPSILON * Math.abs(num)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return plus(p0, times(d0, num / den))
|
||||
}
|
||||
|
||||
class RoundedPolygon {
|
||||
constructor(features, center) {
|
||||
this.features = features
|
||||
this.center = center
|
||||
this.cubics = flatten(features, center)
|
||||
}
|
||||
|
||||
transformed(f) {
|
||||
const move = (c) => {
|
||||
const out = []
|
||||
|
||||
for (let i = 0; i < 8; i += 2) {
|
||||
const p = f(c[i], c[i + 1])
|
||||
out.push(p.x, p.y)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
return new RoundedPolygon(
|
||||
this.features.map((feature) => ({ ...feature, cubics: feature.cubics.map(move) })),
|
||||
f(this.center.x, this.center.y),
|
||||
)
|
||||
}
|
||||
|
||||
/** RoundedPolygon.normalized: into the unit square, by the approximate (control point) bounds. */
|
||||
normalized() {
|
||||
let [left, top, right, bottom] = [Infinity, Infinity, -Infinity, -Infinity]
|
||||
|
||||
for (const c of this.cubics) {
|
||||
const count = zeroLength(c) ? 2 : 8
|
||||
|
||||
for (let i = 0; i < count; i += 2) {
|
||||
left = Math.min(left, c[i])
|
||||
right = Math.max(right, c[i])
|
||||
top = Math.min(top, c[i + 1])
|
||||
bottom = Math.max(bottom, c[i + 1])
|
||||
}
|
||||
}
|
||||
|
||||
const [width, height] = [right - left, bottom - top]
|
||||
const side = Math.max(width, height)
|
||||
const offsetX = (side - width) / 2 - left
|
||||
const offsetY = (side - height) / 2 - top
|
||||
|
||||
return this.transformed((x, y) => point((x + offsetX) / side, (y + offsetY) / side))
|
||||
}
|
||||
}
|
||||
|
||||
function flatten(features, center) {
|
||||
const out = []
|
||||
let firstCubic = null
|
||||
let lastCubic = null
|
||||
let firstFeatureSplitStart = null
|
||||
let firstFeatureSplitEnd = null
|
||||
|
||||
if (features.length > 0 && features[0].cubics.length === 3) {
|
||||
const [start, end] = split(features[0].cubics[1], 0.5)
|
||||
firstFeatureSplitStart = [features[0].cubics[0], start]
|
||||
firstFeatureSplitEnd = [end, features[0].cubics[2]]
|
||||
}
|
||||
|
||||
for (let i = 0; i <= features.length; i++) {
|
||||
let featureCubics
|
||||
|
||||
if (i === 0 && firstFeatureSplitEnd !== null) {
|
||||
featureCubics = firstFeatureSplitEnd
|
||||
} else if (i === features.length) {
|
||||
if (firstFeatureSplitStart === null) {
|
||||
break
|
||||
}
|
||||
|
||||
featureCubics = firstFeatureSplitStart
|
||||
} else {
|
||||
featureCubics = features[i].cubics
|
||||
}
|
||||
|
||||
for (const c of featureCubics) {
|
||||
if (!zeroLength(c)) {
|
||||
if (lastCubic !== null) {
|
||||
out.push(lastCubic)
|
||||
}
|
||||
|
||||
lastCubic = c
|
||||
firstCubic ??= c
|
||||
} else if (lastCubic !== null) {
|
||||
lastCubic = [...lastCubic]
|
||||
lastCubic[6] = c[0]
|
||||
lastCubic[7] = c[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lastCubic !== null && firstCubic !== null) {
|
||||
out.push([...lastCubic.slice(0, 6), firstCubic[0], firstCubic[1]])
|
||||
} else {
|
||||
out.push([center.x, center.y, center.x, center.y, center.x, center.y, center.x, center.y])
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
/** RoundedPolygon(vertices, rounding, perVertexRounding, centerX, centerY). */
|
||||
function polygonFromVertices(vertices, perVertexRounding, center) {
|
||||
const n = vertices.length
|
||||
const roundedCorners = vertices.map((vertex, i) => new RoundedCorner(vertices[(i + n - 1) % n], vertex, vertices[(i + 1) % n], perVertexRounding[i]))
|
||||
|
||||
const cutAdjusts = vertices.map((vertex, i) => {
|
||||
const next = (i + 1) % n
|
||||
const expectedRoundCut = roundedCorners[i].expectedRoundCut + roundedCorners[next].expectedRoundCut
|
||||
const expectedCut = roundedCorners[i].expectedCut + roundedCorners[next].expectedCut
|
||||
const sideSize = length(minus(vertex, vertices[next]))
|
||||
|
||||
if (expectedRoundCut > sideSize) {
|
||||
return [sideSize / expectedRoundCut, 0]
|
||||
}
|
||||
|
||||
if (expectedCut > sideSize) {
|
||||
return [1, (sideSize - expectedRoundCut) / (expectedCut - expectedRoundCut)]
|
||||
}
|
||||
|
||||
return [1, 1]
|
||||
})
|
||||
|
||||
const corners = roundedCorners.map((corner, i) => {
|
||||
const allowedCuts = [0, 1].map((delta) => {
|
||||
const [roundCutRatio, cutRatio] = cutAdjusts[(i + n - 1 + delta) % n]
|
||||
|
||||
return corner.expectedRoundCut * roundCutRatio + (corner.expectedCut - corner.expectedRoundCut) * cutRatio
|
||||
})
|
||||
|
||||
return corner.getCubics(allowedCuts[0], allowedCuts[1])
|
||||
})
|
||||
|
||||
const features = []
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const end = corners[i].at(-1)
|
||||
const start = corners[(i + 1) % n][0]
|
||||
|
||||
features.push({ type: 'corner', convex: convex(vertices[(i + n - 1) % n], vertices[i], vertices[(i + 1) % n]), cubics: corners[i] })
|
||||
features.push({ type: 'edge', cubics: [straightLine(end[6], end[7], start[0], start[1])] })
|
||||
}
|
||||
|
||||
return new RoundedPolygon(features, center)
|
||||
}
|
||||
|
||||
/** RoundedPolygon.circle(numVertices): a regular polygon rounded all the way round. */
|
||||
function circlePolygon(vertexCount) {
|
||||
const radius = 1 / Math.cos(Math.PI / vertexCount)
|
||||
const vertices = Array.from({ length: vertexCount }, (_, i) => radialToCartesian(radius, ((Math.PI / vertexCount) * 2 * i)))
|
||||
|
||||
return polygonFromVertices(vertices, vertices.map(() => rounding(1)), point(0, 0))
|
||||
}
|
||||
|
||||
/** RoundedPolygon.star(numVerticesPerRadius, innerRadius, rounding, innerRounding). */
|
||||
function starPolygon(vertexCount, innerRadius, outerRounding, innerRounding) {
|
||||
const vertices = []
|
||||
const roundings = []
|
||||
|
||||
for (let i = 0; i < vertexCount; i++) {
|
||||
vertices.push(radialToCartesian(1, (Math.PI / vertexCount) * 2 * i), radialToCartesian(innerRadius, (Math.PI / vertexCount) * (2 * i + 1)))
|
||||
roundings.push(outerRounding, innerRounding)
|
||||
}
|
||||
|
||||
return polygonFromVertices(vertices, roundings, point(0, 0))
|
||||
}
|
||||
|
||||
// Morph: bin/loading-indicator.mjs (FloatMapping, PolygonMeasure, FeatureMapping, Morph) ---
|
||||
|
||||
const positiveModulo = (num, mod) => ((num % mod) + mod) % mod
|
||||
const progressInRange = (progress, from, to) => (to >= from ? progress >= from && progress <= to : progress >= from || progress <= to)
|
||||
const progressDistance = (a, b) => Math.min(Math.abs(a - b), 1 - Math.abs(a - b))
|
||||
|
||||
function linearMap(xValues, yValues, x) {
|
||||
const n = xValues.length
|
||||
const start = xValues.findIndex((_, i) => progressInRange(x, xValues[i], xValues[(i + 1) % n]))
|
||||
const end = (start + 1) % n
|
||||
const sizeX = positiveModulo(xValues[end] - xValues[start], 1)
|
||||
const sizeY = positiveModulo(yValues[end] - yValues[start], 1)
|
||||
const position = sizeX < 0.001 ? 0.5 : positiveModulo(x - xValues[start], 1) / sizeX
|
||||
|
||||
return positiveModulo(yValues[start] + sizeY * position, 1)
|
||||
}
|
||||
|
||||
const MEASURE_SEGMENTS = 3
|
||||
|
||||
function closestProgressTo(c, threshold) {
|
||||
let total = 0
|
||||
let remainder = threshold
|
||||
let previous = point(c[0], c[1])
|
||||
|
||||
for (let i = 1; i <= MEASURE_SEGMENTS; i++) {
|
||||
const progress = i / MEASURE_SEGMENTS
|
||||
const p = pointOnCurve(c, progress)
|
||||
const segment = Math.hypot(p.x - previous.x, p.y - previous.y)
|
||||
|
||||
if (segment >= remainder) {
|
||||
return [progress - (1 - remainder / segment) / MEASURE_SEGMENTS, threshold]
|
||||
}
|
||||
|
||||
remainder -= segment
|
||||
total += segment
|
||||
previous = p
|
||||
}
|
||||
|
||||
return [1, total]
|
||||
}
|
||||
|
||||
class MeasuredCubic {
|
||||
constructor(c, startOutlineProgress, endOutlineProgress) {
|
||||
this.cubic = c
|
||||
this.startOutlineProgress = startOutlineProgress
|
||||
this.endOutlineProgress = endOutlineProgress
|
||||
this.measuredSize = closestProgressTo(c, Infinity)[1]
|
||||
}
|
||||
|
||||
cutAtProgress(cutOutlineProgress) {
|
||||
const bounded = clamp(cutOutlineProgress, this.startOutlineProgress, this.endOutlineProgress)
|
||||
const relativeProgress = (bounded - this.startOutlineProgress) / (this.endOutlineProgress - this.startOutlineProgress)
|
||||
const t = closestProgressTo(this.cubic, relativeProgress * this.measuredSize)[0]
|
||||
const [c1, c2] = split(this.cubic, t)
|
||||
|
||||
return [new MeasuredCubic(c1, this.startOutlineProgress, bounded), new MeasuredCubic(c2, bounded, this.endOutlineProgress)]
|
||||
}
|
||||
}
|
||||
|
||||
class MeasuredPolygon {
|
||||
constructor(features, cubics, outlineProgress) {
|
||||
this.features = features
|
||||
this.cubics = []
|
||||
|
||||
let startOutlineProgress = 0
|
||||
|
||||
for (let i = 0; i < cubics.length; i++) {
|
||||
if (outlineProgress[i + 1] - outlineProgress[i] > DISTANCE_EPSILON) {
|
||||
this.cubics.push(new MeasuredCubic(cubics[i], startOutlineProgress, outlineProgress[i + 1]))
|
||||
startOutlineProgress = outlineProgress[i + 1]
|
||||
}
|
||||
}
|
||||
|
||||
this.cubics.at(-1).endOutlineProgress = 1
|
||||
}
|
||||
|
||||
static measure(polygon) {
|
||||
const cubics = []
|
||||
const featureToCubic = []
|
||||
|
||||
for (const feature of polygon.features) {
|
||||
feature.cubics.forEach((c, i) => {
|
||||
if (feature.type === 'corner' && i === Math.floor(feature.cubics.length / 2)) {
|
||||
featureToCubic.push([feature, cubics.length])
|
||||
}
|
||||
|
||||
cubics.push(c)
|
||||
})
|
||||
}
|
||||
|
||||
const measures = [0]
|
||||
|
||||
for (const c of cubics) {
|
||||
measures.push(measures.at(-1) + closestProgressTo(c, Infinity)[1])
|
||||
}
|
||||
|
||||
const outlineProgress = measures.map((measure) => measure / measures.at(-1))
|
||||
const features = featureToCubic.map(([feature, ix]) => ({
|
||||
progress: positiveModulo((outlineProgress[ix] + outlineProgress[ix + 1]) / 2, 1),
|
||||
feature,
|
||||
}))
|
||||
|
||||
return new MeasuredPolygon(features, cubics, outlineProgress)
|
||||
}
|
||||
|
||||
cutAndShift(cuttingPoint) {
|
||||
if (cuttingPoint < DISTANCE_EPSILON) {
|
||||
return this
|
||||
}
|
||||
|
||||
const n = this.cubics.length
|
||||
const targetIndex = this.cubics.findIndex((c) => cuttingPoint >= c.startOutlineProgress && cuttingPoint <= c.endOutlineProgress)
|
||||
const [b1, b2] = this.cubics[targetIndex].cutAtProgress(cuttingPoint)
|
||||
const cubics = [b2.cubic]
|
||||
|
||||
for (let i = 1; i < n; i++) {
|
||||
cubics.push(this.cubics[(i + targetIndex) % n].cubic)
|
||||
}
|
||||
|
||||
cubics.push(b1.cubic)
|
||||
|
||||
const outlineProgress = Array.from({ length: n + 2 }, (_, index) => {
|
||||
if (index === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
if (index === n + 1) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return positiveModulo(this.cubics[(targetIndex + index - 1) % n].endOutlineProgress - cuttingPoint, 1)
|
||||
})
|
||||
|
||||
const features = this.features.map(({ progress, feature }) => ({ progress: positiveModulo(progress - cuttingPoint, 1), feature }))
|
||||
|
||||
return new MeasuredPolygon(features, cubics, outlineProgress)
|
||||
}
|
||||
}
|
||||
|
||||
function featureDistSquared(f1, f2) {
|
||||
if (f1.type === 'corner' && f2.type === 'corner' && f1.convex !== f2.convex) {
|
||||
return Infinity
|
||||
}
|
||||
|
||||
const representative = (feature) => {
|
||||
const [first, last] = [feature.cubics[0], feature.cubics.at(-1)]
|
||||
|
||||
return point((first[0] + last[6]) / 2, (first[1] + last[7]) / 2)
|
||||
}
|
||||
|
||||
const [p1, p2] = [representative(f1), representative(f2)]
|
||||
|
||||
return (p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2
|
||||
}
|
||||
|
||||
function doMapping(features1, features2) {
|
||||
const distanceVertexList = []
|
||||
|
||||
for (const f1 of features1) {
|
||||
for (const f2 of features2) {
|
||||
const distance = featureDistSquared(f1.feature, f2.feature)
|
||||
|
||||
if (distance !== Infinity) {
|
||||
distanceVertexList.push({ distance, f1, f2 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
distanceVertexList.sort((a, b) => a.distance - b.distance)
|
||||
|
||||
if (distanceVertexList.length === 0) {
|
||||
return [
|
||||
[0, 0],
|
||||
[0.5, 0.5],
|
||||
]
|
||||
}
|
||||
|
||||
if (distanceVertexList.length === 1) {
|
||||
const { f1, f2 } = distanceVertexList[0]
|
||||
|
||||
return [
|
||||
[f1.progress, f2.progress],
|
||||
[(f1.progress + 0.5) % 1, (f2.progress + 0.5) % 1],
|
||||
]
|
||||
}
|
||||
|
||||
const mapping = []
|
||||
const usedF1 = new Set()
|
||||
const usedF2 = new Set()
|
||||
|
||||
for (const { f1, f2 } of distanceVertexList) {
|
||||
if (usedF1.has(f1) || usedF2.has(f2)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const insertionIndex = mapping.findIndex((m) => m[0] >= f1.progress)
|
||||
const index = insertionIndex === -1 ? mapping.length : insertionIndex
|
||||
const n = mapping.length
|
||||
|
||||
if (n >= 1) {
|
||||
const [before1, before2] = mapping[(index + n - 1) % n]
|
||||
const [after1, after2] = mapping[index % n]
|
||||
|
||||
if (
|
||||
progressDistance(f1.progress, before1) < DISTANCE_EPSILON ||
|
||||
progressDistance(f1.progress, after1) < DISTANCE_EPSILON ||
|
||||
progressDistance(f2.progress, before2) < DISTANCE_EPSILON ||
|
||||
progressDistance(f2.progress, after2) < DISTANCE_EPSILON
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (n > 1 && !progressInRange(f2.progress, before2, after2)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
mapping.splice(index, 0, [f1.progress, f2.progress])
|
||||
usedF1.add(f1)
|
||||
usedF2.add(f2)
|
||||
}
|
||||
|
||||
return mapping
|
||||
}
|
||||
|
||||
/** Morph.match: both shapes cut into pairs of matching cubics. */
|
||||
function match(p1, p2) {
|
||||
const measuredPolygon1 = MeasuredPolygon.measure(p1)
|
||||
const measuredPolygon2 = MeasuredPolygon.measure(p2)
|
||||
const corners = (features) => features.filter(({ feature }) => feature.type === 'corner')
|
||||
const mappings = doMapping(corners(measuredPolygon1.features), corners(measuredPolygon2.features))
|
||||
const [sources, targets] = [mappings.map((m) => m[0]), mappings.map((m) => m[1])]
|
||||
const map = (x) => linearMap(sources, targets, x)
|
||||
const mapBack = (x) => linearMap(targets, sources, x)
|
||||
const polygon2CutPoint = map(0)
|
||||
const bs1 = measuredPolygon1.cubics
|
||||
const bs2 = measuredPolygon2.cutAndShift(polygon2CutPoint).cubics
|
||||
const pairs = []
|
||||
|
||||
let i1 = 0
|
||||
let i2 = 0
|
||||
let b1 = bs1[i1++]
|
||||
let b2 = bs2[i2++]
|
||||
|
||||
while (b1 !== undefined && b2 !== undefined) {
|
||||
const b1a = i1 === bs1.length ? 1 : b1.endOutlineProgress
|
||||
const b2a = i2 === bs2.length ? 1 : mapBack(positiveModulo(b2.endOutlineProgress + polygon2CutPoint, 1))
|
||||
const minb = Math.min(b1a, b2a)
|
||||
let seg1
|
||||
let seg2
|
||||
|
||||
if (b1a > minb + ANGLE_EPSILON) {
|
||||
;[seg1, b1] = b1.cutAtProgress(minb)
|
||||
} else {
|
||||
seg1 = b1
|
||||
b1 = bs1[i1++]
|
||||
}
|
||||
|
||||
if (b2a > minb + ANGLE_EPSILON) {
|
||||
;[seg2, b2] = b2.cutAtProgress(positiveModulo(map(minb) - polygon2CutPoint, 1))
|
||||
} else {
|
||||
seg2 = b2
|
||||
b2 = bs2[i2++]
|
||||
}
|
||||
|
||||
pairs.push([seg1.cubic, seg2.cubic])
|
||||
}
|
||||
|
||||
return pairs
|
||||
}
|
||||
|
||||
/** Morph.asCubics: every matched pair interpolated at `progress`, closed on its first anchor. */
|
||||
function asCubics(pairs, progress) {
|
||||
const cubics = pairs.map(([start, end]) => start.map((value, i) => value + (end[i] - value) * progress))
|
||||
|
||||
cubics.at(-1)[6] = cubics[0][0]
|
||||
cubics.at(-1)[7] = cubics[0][1]
|
||||
|
||||
return cubics
|
||||
}
|
||||
|
||||
// The circular wave ---------------------------------------------------------------------
|
||||
|
||||
const circularShapes = new Map()
|
||||
@@ -915,7 +299,7 @@ const circularShapes = new Map()
|
||||
function shapesFor(vertexCount) {
|
||||
if (!circularShapes.has(vertexCount)) {
|
||||
const circle = circlePolygon(vertexCount).normalized()
|
||||
const star = starPolygon(vertexCount, 0.75, rounding(0.35, 0.4), rounding(0.5)).normalized()
|
||||
const star = starPolygon(vertexCount, { innerRadius: 0.75, cornerRounding: rounding(0.35, 0.4), innerCornerRounding: rounding(0.5) }).normalized()
|
||||
let pairs = null
|
||||
|
||||
circularShapes.set(vertexCount, {
|
||||
@@ -935,6 +319,8 @@ const GAUSS_LEGENDRE = [
|
||||
[0.9602898564975363, 0.1012285362903763],
|
||||
]
|
||||
|
||||
// Pure arithmetic rather than getTotalLength(): WebKit lays the whole page out on that call, even
|
||||
// for a detached path, and this runs every frame while the amplitude animates.
|
||||
function cubicLength(c) {
|
||||
let sum = 0
|
||||
|
||||
@@ -988,18 +374,6 @@ function circularPath(cubics, pivot, repeat, scale, cx, cy) {
|
||||
|
||||
// The component -------------------------------------------------------------------------
|
||||
|
||||
/** A duration token in milliseconds, whichever unit a minifier left it in; null when unset. */
|
||||
function tokenDuration(element, token) {
|
||||
const value = getComputedStyle(element).getPropertyValue(token).trim()
|
||||
const number = parseFloat(value)
|
||||
|
||||
if (Number.isNaN(number)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return value.endsWith('ms') ? number : number * 1000
|
||||
}
|
||||
|
||||
function svgElement(name, attributes) {
|
||||
const element = document.createElementNS(SVG, name)
|
||||
|
||||
@@ -1077,7 +451,7 @@ class Indicator {
|
||||
// Compose draws determinate and indeterminate indicators as two different composables.
|
||||
this.reset(target)
|
||||
} else if (target !== null && target !== this.target) {
|
||||
const duration = tokenDuration(element, '--md-sys-motion-effects-slow-duration') ?? VALUE_SETTLE
|
||||
const duration = ms(element, '--md-sys-motion-effects-slow-duration') ?? VALUE_SETTLE
|
||||
this.timeScale = duration > 0 && !this.motion.matches ? VALUE_SETTLE / duration : 0
|
||||
}
|
||||
|
||||
@@ -1202,7 +576,7 @@ class Indicator {
|
||||
const goal = this.target === null ? 1 : amplitudeFor(clamp(this.progress, 0, 1))
|
||||
|
||||
if (!this.amplitudeAnimation && goal !== this.amplitudeGoal) {
|
||||
const duration = reduced ? 0 : (tokenDuration(this.element, '--md-sys-motion-duration-long') ?? AMPLITUDE_DURATION)
|
||||
const duration = reduced ? 0 : (ms(this.element, '--md-sys-motion-duration-long') ?? AMPLITUDE_DURATION)
|
||||
|
||||
this.amplitudeGoal = goal
|
||||
this.morphed = true
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* that can hover, at once on keyboard focus (not the focus a sheet or dialog moves to it as it
|
||||
* opens, layers.js), and standing for 1.5s once the pointer or the focus
|
||||
* leaves, which M3 gives plain and rich tooltips alike (docs/reference/m3/
|
||||
* components-actions-communication-containment.md § Tooltips, ACT-25). The bubble is inside the
|
||||
* components-actions-communication-containment.md § Tooltips). The bubble is inside the
|
||||
* wrapper, so moving onto it to reach its actions, by pointer or by Tab, is no leave at all; Escape
|
||||
* hides it at once. Persistent: a press on the trigger opens it as a light-dismiss popover.
|
||||
*
|
||||
@@ -16,20 +16,12 @@
|
||||
* A Livewire morph strips attributes the server did not render and gives the bubble a new id, so
|
||||
* they are written again whenever the trigger is reached.
|
||||
*/
|
||||
import { openingFocus } from './layers.js'
|
||||
|
||||
const HOVER_DELAY_MS = 500
|
||||
const LEAVE_GRACE_MS = 1500
|
||||
|
||||
// A persistent bubble is a `popover="auto"`, and its trigger is outside it: the press on the
|
||||
// trigger light-dismisses the open bubble, and the click that follows would open it again, so a
|
||||
// second press never closed it. A close the browser made this recently is taken as that press, as
|
||||
// menu.js's REOPEN_GUARD_MS does; `beforetoggle` times it, because `toggle` is queued past the click.
|
||||
const REOPEN_GUARD_MS = 250
|
||||
import { hoverPopover, LEAVE_DELAY_MS } from './tooltip.js'
|
||||
import { reopenGuard } from './util.js'
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
window.Alpine.data('materialRichTooltip', (persistent = false) => ({
|
||||
timer: null,
|
||||
stop: null,
|
||||
|
||||
init() {
|
||||
const bubble = this.$refs.bubble
|
||||
@@ -59,16 +51,12 @@ document.addEventListener('alpine:init', () => {
|
||||
wrapper.addEventListener('pointerenter', describe)
|
||||
|
||||
if (persistent) {
|
||||
let dismissedAt = -Infinity
|
||||
let closingExplicitly = false
|
||||
// A persistent bubble is a `popover="auto"`, and its trigger is outside it: the
|
||||
// press on the trigger light-dismisses the open bubble, and the click that
|
||||
// follows would open it again, so a second press never closed it.
|
||||
const guard = reopenGuard()
|
||||
|
||||
bubble.addEventListener('beforetoggle', (event) => {
|
||||
if (event.newState === 'closed' && !closingExplicitly) {
|
||||
dismissedAt = performance.now()
|
||||
}
|
||||
|
||||
closingExplicitly = false
|
||||
})
|
||||
bubble.addEventListener('beforetoggle', (event) => guard.beforeToggle(event))
|
||||
|
||||
wrapper.addEventListener('click', (event) => {
|
||||
if (bubble.contains(event.target)) {
|
||||
@@ -76,9 +64,9 @@ document.addEventListener('alpine:init', () => {
|
||||
}
|
||||
|
||||
if (open()) {
|
||||
closingExplicitly = true
|
||||
guard.closing()
|
||||
bubble.hidePopover()
|
||||
} else if (performance.now() - dismissedAt > REOPEN_GUARD_MS) {
|
||||
} else if (guard.canOpen()) {
|
||||
bubble.showPopover()
|
||||
}
|
||||
})
|
||||
@@ -86,25 +74,20 @@ document.addEventListener('alpine:init', () => {
|
||||
return
|
||||
}
|
||||
|
||||
const show = (delay) => {
|
||||
clearTimeout(this.timer)
|
||||
this.timer = setTimeout(() => !open() && bubble.showPopover(), delay)
|
||||
}
|
||||
const popover = hoverPopover(wrapper, {
|
||||
open,
|
||||
show: () => bubble.showPopover(),
|
||||
hide: () => bubble.hidePopover(),
|
||||
focusable: (event) => event.target.matches(':focus-visible'),
|
||||
})
|
||||
|
||||
const hide = (delay = 0) => {
|
||||
clearTimeout(this.timer)
|
||||
this.timer = setTimeout(() => open() && bubble.hidePopover(), delay)
|
||||
}
|
||||
wrapper.addEventListener('focusout', (event) => !wrapper.contains(event.relatedTarget) && popover.hide(LEAVE_DELAY_MS), { signal: popover.signal })
|
||||
|
||||
wrapper.addEventListener('pointerenter', (event) => event.pointerType === 'mouse' && show(HOVER_DELAY_MS))
|
||||
wrapper.addEventListener('pointerleave', () => hide(LEAVE_GRACE_MS))
|
||||
wrapper.addEventListener('focusin', (event) => !openingFocus(event) && event.target.matches(':focus-visible') && show(0))
|
||||
wrapper.addEventListener('focusout', (event) => !wrapper.contains(event.relatedTarget) && hide(LEAVE_GRACE_MS))
|
||||
document.addEventListener('keydown', (event) => event.key === 'Escape' && hide())
|
||||
this.stop = popover.destroy
|
||||
},
|
||||
|
||||
destroy() {
|
||||
clearTimeout(this.timer)
|
||||
this.stop?.()
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
+6
-15
@@ -41,15 +41,6 @@ const RETURN_GUARD_MS = 250
|
||||
// counting, so the live region speaks once.
|
||||
const SETTLE_MS = 120
|
||||
|
||||
/** The longest `transition-duration` + `transition-delay` on an element, in ms: zero under reduced motion. */
|
||||
const longestTransition = (element) => {
|
||||
const style = getComputedStyle(element)
|
||||
const ms = (value) => parseFloat(value) * (value.trim().endsWith('ms') ? 1 : 1000)
|
||||
const delays = style.transitionDelay.split(',').map(ms)
|
||||
|
||||
return Math.max(0, ...style.transitionDuration.split(',').map((duration, index) => ms(duration) + (delays[index % delays.length] || 0)))
|
||||
}
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
window.Alpine.data('materialSearch', (docked = false, announce = {}, trigger = 'bar') => ({
|
||||
open: false,
|
||||
@@ -162,9 +153,9 @@ document.addEventListener('alpine:init', () => {
|
||||
},
|
||||
|
||||
/**
|
||||
* Keeps the full-screen layout for the length of the view's exit. The closed state's
|
||||
* durations are read a frame on, once they are the ones computed; reopening, which counts
|
||||
* `leavings` up, lets a pending end go by.
|
||||
* Keeps the full-screen layout for the length of the view's exit: until the animations the
|
||||
* closed state starts, a frame on, have finished (at once when none run); reopening, which
|
||||
* counts `leavings` up, lets a pending end go by.
|
||||
*/
|
||||
hold() {
|
||||
const leaving = ++this.leavings
|
||||
@@ -172,13 +163,13 @@ document.addEventListener('alpine:init', () => {
|
||||
this.leaving = true
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const duration = this.$refs.view ? longestTransition(this.$refs.view) : 0
|
||||
const animations = this.$refs.view?.getAnimations() ?? []
|
||||
|
||||
setTimeout(() => {
|
||||
Promise.allSettled(animations.map((animation) => animation.finished)).then(() => {
|
||||
if (leaving === this.leavings) {
|
||||
this.leaving = false
|
||||
}
|
||||
}, duration)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,772 @@
|
||||
/**
|
||||
* Shared, DOM-free geometry: androidx's graphics-shapes (RoundedPolygon — a shape built from
|
||||
* vertices with per-corner rounding, turned into cubic Béziers) and its Morph (matching two
|
||||
* shapes' cubics so they can be interpolated). Used by bin/shapes.mjs and bin/loading-indicator.mjs
|
||||
* to build the shape and loading-indicator SVGs at commit time, and by progress.js to morph the
|
||||
* circular wavy progress indicator's star and circle at runtime.
|
||||
*
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* Ported from androidx (https://github.com/androidx/androidx), commit
|
||||
* 27cf9a7d5788aa0f5f2d8b6699ce279560daf326:
|
||||
*
|
||||
* graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/CornerRounding.kt
|
||||
* graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Cubic.kt
|
||||
* graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/FeatureMapping.kt
|
||||
* graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/FloatMapping.kt
|
||||
* graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Morph.kt
|
||||
* graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Point.kt
|
||||
* graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/PolygonMeasure.kt
|
||||
* graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/RoundedPolygon.kt
|
||||
* graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Shapes.kt
|
||||
* graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Utils.kt
|
||||
*
|
||||
* Copyright 2022-2025 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* ---------------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const DISTANCE_EPSILON = 1e-4
|
||||
const ANGLE_EPSILON = 1e-6
|
||||
|
||||
// Point.kt / Utils.kt ---------------------------------------------------------------------
|
||||
|
||||
const point = (x, y) => ({ x, y })
|
||||
const plus = (a, b) => point(a.x + b.x, a.y + b.y)
|
||||
const minus = (a, b) => point(a.x - b.x, a.y - b.y)
|
||||
const times = (a, k) => point(a.x * k, a.y * k)
|
||||
const div = (a, k) => point(a.x / k, a.y / k)
|
||||
const dot = (a, b) => a.x * b.x + a.y * b.y
|
||||
const length = (a) => Math.sqrt(a.x * a.x + a.y * a.y)
|
||||
const rotate90 = (a) => point(-a.y, a.x)
|
||||
const lerp = (a, b, f) => (1 - f) * a + f * b
|
||||
const lerpPoint = (a, b, f) => point(lerp(a.x, b.x, f), lerp(a.y, b.y, f))
|
||||
const radialToCartesian = (radius, angle) => point(Math.cos(angle) * radius, Math.sin(angle) * radius)
|
||||
const convex = (previous, current, next) => {
|
||||
const [a, b] = [minus(current, previous), minus(next, current)]
|
||||
|
||||
return a.x * b.y - a.y * b.x > 0
|
||||
}
|
||||
|
||||
function direction(a) {
|
||||
const d = length(a)
|
||||
|
||||
if (!(d > 0)) {
|
||||
throw new Error("Can't get the direction of a 0-length vector")
|
||||
}
|
||||
|
||||
return div(a, d)
|
||||
}
|
||||
|
||||
// Cubic.kt ----------------------------------------------------------------------------------
|
||||
|
||||
/** A cubic is [anchor0X, anchor0Y, control0X, control0Y, control1X, control1Y, anchor1X, anchor1Y]. */
|
||||
const cubic = (a0, c0, c1, a1) => [a0.x, a0.y, c0.x, c0.y, c1.x, c1.y, a1.x, a1.y]
|
||||
|
||||
const straightLine = (x0, y0, x1, y1) => [x0, y0, lerp(x0, x1, 1 / 3), lerp(y0, y1, 1 / 3), lerp(x0, x1, 2 / 3), lerp(y0, y1, 2 / 3), x1, y1]
|
||||
|
||||
function circularArc(centerX, centerY, x0, y0, x1, y1) {
|
||||
const p0d = direction(point(x0 - centerX, y0 - centerY))
|
||||
const p1d = direction(point(x1 - centerX, y1 - centerY))
|
||||
const rotatedP0 = rotate90(p0d)
|
||||
const rotatedP1 = rotate90(p1d)
|
||||
const clockwise = dot(rotatedP0, point(x1 - centerX, y1 - centerY)) >= 0
|
||||
const cosa = dot(p0d, p1d)
|
||||
|
||||
if (cosa > 0.999) {
|
||||
return straightLine(x0, y0, x1, y1)
|
||||
}
|
||||
|
||||
const k =
|
||||
(((length(point(x0 - centerX, y0 - centerY)) * 4) / 3) * (Math.sqrt(2 * (1 - cosa)) - Math.sqrt(1 - cosa * cosa))) /
|
||||
(1 - cosa) *
|
||||
(clockwise ? 1 : -1)
|
||||
|
||||
return [x0, y0, x0 + rotatedP0.x * k, y0 + rotatedP0.y * k, x1 - rotatedP1.x * k, y1 - rotatedP1.y * k, x1, y1]
|
||||
}
|
||||
|
||||
function pointOnCurve(c, t) {
|
||||
const u = 1 - t
|
||||
|
||||
return point(
|
||||
c[0] * (u * u * u) + c[2] * (3 * t * u * u) + c[4] * (3 * t * t * u) + c[6] * (t * t * t),
|
||||
c[1] * (u * u * u) + c[3] * (3 * t * u * u) + c[5] * (3 * t * t * u) + c[7] * (t * t * t),
|
||||
)
|
||||
}
|
||||
|
||||
function split(c, t) {
|
||||
const u = 1 - t
|
||||
const p = pointOnCurve(c, t)
|
||||
|
||||
return [
|
||||
[c[0], c[1], c[0] * u + c[2] * t, c[1] * u + c[3] * t, c[0] * (u * u) + c[2] * (2 * u * t) + c[4] * (t * t), c[1] * (u * u) + c[3] * (2 * u * t) + c[5] * (t * t), p.x, p.y],
|
||||
[p.x, p.y, c[2] * (u * u) + c[4] * (2 * u * t) + c[6] * (t * t), c[3] * (u * u) + c[5] * (2 * u * t) + c[7] * (t * t), c[4] * u + c[6] * t, c[5] * u + c[7] * t, c[6], c[7]],
|
||||
]
|
||||
}
|
||||
|
||||
const reverse = (c) => [c[6], c[7], c[4], c[5], c[2], c[3], c[0], c[1]]
|
||||
const zeroLength = (c) => Math.abs(c[0] - c[6]) < DISTANCE_EPSILON && Math.abs(c[1] - c[7]) < DISTANCE_EPSILON
|
||||
const clamp = (value, min, max) => Math.min(Math.max(value, min), max)
|
||||
|
||||
/** The parameters in (0, 1) where one axis of a cubic turns: the roots of its derivative. */
|
||||
function turningPoints(c, axis) {
|
||||
const [p0, p1, p2, p3] = [c[axis], c[axis + 2], c[axis + 4], c[axis + 6]]
|
||||
const a = -p0 + 3 * p1 - 3 * p2 + p3
|
||||
const b = 2 * (p0 - 2 * p1 + p2)
|
||||
const k = p1 - p0
|
||||
const roots = []
|
||||
|
||||
if (Math.abs(a) < 1e-9) {
|
||||
if (Math.abs(b) > 1e-9) {
|
||||
roots.push(-k / b)
|
||||
}
|
||||
} else if (b * b - 4 * a * k >= 0) {
|
||||
const root = Math.sqrt(b * b - 4 * a * k)
|
||||
roots.push((-b + root) / (2 * a), (-b - root) / (2 * a))
|
||||
}
|
||||
|
||||
return roots.filter((t) => t > 1e-6 && t < 1 - 1e-6)
|
||||
}
|
||||
|
||||
/** Axis-aligned bounds of one cubic: of all four points when approximate, else of the curve itself. */
|
||||
function cubicBounds(c, approximate) {
|
||||
const xs = [c[0], c[6]]
|
||||
const ys = [c[1], c[7]]
|
||||
|
||||
if (approximate) {
|
||||
xs.push(c[2], c[4])
|
||||
ys.push(c[3], c[5])
|
||||
} else {
|
||||
turningPoints(c, 0).forEach((t) => xs.push(pointOnCurve(c, t).x))
|
||||
turningPoints(c, 1).forEach((t) => ys.push(pointOnCurve(c, t).y))
|
||||
}
|
||||
|
||||
return [Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys)]
|
||||
}
|
||||
|
||||
// CornerRounding.kt ---------------------------------------------------------------------------
|
||||
|
||||
const rounding = (radius = 0, smoothing = 0) => ({ radius, smoothing })
|
||||
const UNROUNDED = rounding()
|
||||
|
||||
// RoundedPolygon.kt ---------------------------------------------------------------------------
|
||||
|
||||
class RoundedCorner {
|
||||
constructor(p0, p1, p2, cornerRounding) {
|
||||
this.p0 = p0
|
||||
this.p1 = p1
|
||||
this.p2 = p2
|
||||
|
||||
const v01 = minus(p0, p1)
|
||||
const v21 = minus(p2, p1)
|
||||
const d01 = length(v01)
|
||||
const d21 = length(v21)
|
||||
|
||||
if (d01 > 0 && d21 > 0) {
|
||||
this.d1 = div(v01, d01)
|
||||
this.d2 = div(v21, d21)
|
||||
this.cornerRadius = cornerRounding?.radius ?? 0
|
||||
this.smoothing = cornerRounding?.smoothing ?? 0
|
||||
this.cosAngle = dot(this.d1, this.d2)
|
||||
this.sinAngle = Math.sqrt(1 - this.cosAngle * this.cosAngle)
|
||||
this.expectedRoundCut = this.sinAngle > 1e-3 ? (this.cornerRadius * (this.cosAngle + 1)) / this.sinAngle : 0
|
||||
} else {
|
||||
this.d1 = point(0, 0)
|
||||
this.d2 = point(0, 0)
|
||||
this.cornerRadius = 0
|
||||
this.smoothing = 0
|
||||
this.cosAngle = 0
|
||||
this.sinAngle = 0
|
||||
this.expectedRoundCut = 0
|
||||
}
|
||||
}
|
||||
|
||||
get expectedCut() {
|
||||
return (1 + this.smoothing) * this.expectedRoundCut
|
||||
}
|
||||
|
||||
getCubics(allowedCut0, allowedCut1 = allowedCut0) {
|
||||
const allowedCut = Math.min(allowedCut0, allowedCut1)
|
||||
|
||||
if (this.expectedRoundCut < DISTANCE_EPSILON || allowedCut < DISTANCE_EPSILON || this.cornerRadius < DISTANCE_EPSILON) {
|
||||
return [straightLine(this.p1.x, this.p1.y, this.p1.x, this.p1.y)]
|
||||
}
|
||||
|
||||
const actualRoundCut = Math.min(allowedCut, this.expectedRoundCut)
|
||||
const actualSmoothing0 = this.actualSmoothing(allowedCut0)
|
||||
const actualSmoothing1 = this.actualSmoothing(allowedCut1)
|
||||
const actualR = (this.cornerRadius * actualRoundCut) / this.expectedRoundCut
|
||||
const centerDistance = Math.sqrt(actualR * actualR + actualRoundCut * actualRoundCut)
|
||||
const center = plus(this.p1, times(direction(div(plus(this.d1, this.d2), 2)), centerDistance))
|
||||
const circleIntersection0 = plus(this.p1, times(this.d1, actualRoundCut))
|
||||
const circleIntersection2 = plus(this.p1, times(this.d2, actualRoundCut))
|
||||
const flanking0 = this.flankingCurve(actualRoundCut, actualSmoothing0, this.p1, this.p0, circleIntersection0, circleIntersection2, center, actualR)
|
||||
const flanking2 = reverse(this.flankingCurve(actualRoundCut, actualSmoothing1, this.p1, this.p2, circleIntersection2, circleIntersection0, center, actualR))
|
||||
|
||||
return [flanking0, circularArc(center.x, center.y, flanking0[6], flanking0[7], flanking2[0], flanking2[1]), flanking2]
|
||||
}
|
||||
|
||||
actualSmoothing(allowedCut) {
|
||||
if (allowedCut > this.expectedCut) {
|
||||
return this.smoothing
|
||||
}
|
||||
|
||||
if (allowedCut > this.expectedRoundCut) {
|
||||
return (this.smoothing * (allowedCut - this.expectedRoundCut)) / (this.expectedCut - this.expectedRoundCut)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
flankingCurve(actualRoundCut, smoothing, corner, sideStart, intersection, otherIntersection, circleCenter, actualR) {
|
||||
const sideDirection = direction(minus(sideStart, corner))
|
||||
const curveStart = plus(corner, times(sideDirection, actualRoundCut * (1 + smoothing)))
|
||||
const p = lerpPoint(intersection, div(plus(intersection, otherIntersection), 2), smoothing)
|
||||
const curveEnd = plus(circleCenter, times(direction(minus(p, circleCenter)), actualR))
|
||||
const circleTangent = rotate90(minus(curveEnd, circleCenter))
|
||||
const anchorEnd = lineIntersection(sideStart, sideDirection, curveEnd, circleTangent) ?? intersection
|
||||
const anchorStart = div(plus(curveStart, times(anchorEnd, 2)), 3)
|
||||
|
||||
return cubic(curveStart, anchorStart, anchorEnd, curveEnd)
|
||||
}
|
||||
}
|
||||
|
||||
function lineIntersection(p0, d0, p1, d1) {
|
||||
const rotatedD1 = rotate90(d1)
|
||||
const den = dot(d0, rotatedD1)
|
||||
|
||||
if (Math.abs(den) < DISTANCE_EPSILON) {
|
||||
return null
|
||||
}
|
||||
|
||||
const num = dot(minus(p1, p0), rotatedD1)
|
||||
|
||||
if (Math.abs(den) < DISTANCE_EPSILON * Math.abs(num)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return plus(p0, times(d0, num / den))
|
||||
}
|
||||
|
||||
/**
|
||||
* A polygon as androidx keeps it: its features (corners and the edges between them, each a
|
||||
* list of cubics) and a centre. `cubics` flattens the features exactly as RoundedPolygon does.
|
||||
*/
|
||||
class RoundedPolygon {
|
||||
constructor(features, center) {
|
||||
this.features = features
|
||||
this.center = center
|
||||
this.cubics = flatten(features, center)
|
||||
}
|
||||
|
||||
transformed(f) {
|
||||
const move = (c) => {
|
||||
const out = []
|
||||
|
||||
for (let i = 0; i < 8; i += 2) {
|
||||
const p = f(c[i], c[i + 1])
|
||||
out.push(p.x, p.y)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
return new RoundedPolygon(
|
||||
this.features.map((feature) => ({ ...feature, cubics: feature.cubics.map(move) })),
|
||||
f(this.center.x, this.center.y),
|
||||
)
|
||||
}
|
||||
|
||||
bounds(approximate) {
|
||||
const all = this.cubics.map((c) => (zeroLength(c) ? [c[0], c[1], c[0], c[1]] : cubicBounds(c, approximate)))
|
||||
|
||||
return [Math.min(...all.map((b) => b[0])), Math.min(...all.map((b) => b[1])), Math.max(...all.map((b) => b[2])), Math.max(...all.map((b) => b[3]))]
|
||||
}
|
||||
|
||||
/** RoundedPolygon.normalized: into the unit square, by the approximate (control point) bounds. */
|
||||
normalized() {
|
||||
const [left, top, right, bottom] = this.bounds(true)
|
||||
const width = right - left
|
||||
const height = bottom - top
|
||||
const side = Math.max(width, height)
|
||||
const offsetX = (side - width) / 2 - left
|
||||
const offsetY = (side - height) / 2 - top
|
||||
|
||||
return this.transformed((x, y) => point((x + offsetX) / side, (y + offsetY) / side))
|
||||
}
|
||||
}
|
||||
|
||||
function flatten(features, center) {
|
||||
const out = []
|
||||
let firstCubic = null
|
||||
let lastCubic = null
|
||||
let firstFeatureSplitStart = null
|
||||
let firstFeatureSplitEnd = null
|
||||
|
||||
if (features.length > 0 && features[0].cubics.length === 3) {
|
||||
const [start, end] = split(features[0].cubics[1], 0.5)
|
||||
firstFeatureSplitStart = [features[0].cubics[0], start]
|
||||
firstFeatureSplitEnd = [end, features[0].cubics[2]]
|
||||
}
|
||||
|
||||
for (let i = 0; i <= features.length; i++) {
|
||||
let featureCubics
|
||||
|
||||
if (i === 0 && firstFeatureSplitEnd !== null) {
|
||||
featureCubics = firstFeatureSplitEnd
|
||||
} else if (i === features.length) {
|
||||
if (firstFeatureSplitStart === null) {
|
||||
break
|
||||
}
|
||||
|
||||
featureCubics = firstFeatureSplitStart
|
||||
} else {
|
||||
featureCubics = features[i].cubics
|
||||
}
|
||||
|
||||
for (const c of featureCubics) {
|
||||
if (!zeroLength(c)) {
|
||||
if (lastCubic !== null) {
|
||||
out.push(lastCubic)
|
||||
}
|
||||
|
||||
lastCubic = c
|
||||
firstCubic ??= c
|
||||
} else if (lastCubic !== null) {
|
||||
lastCubic = [...lastCubic]
|
||||
lastCubic[6] = c[0]
|
||||
lastCubic[7] = c[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lastCubic !== null && firstCubic !== null) {
|
||||
out.push([...lastCubic.slice(0, 6), firstCubic[0], firstCubic[1]])
|
||||
} else {
|
||||
out.push([center.x, center.y, center.x, center.y, center.x, center.y, center.x, center.y])
|
||||
}
|
||||
|
||||
for (let i = 0; i < out.length; i++) {
|
||||
const previous = out[(i + out.length - 1) % out.length]
|
||||
|
||||
if (Math.abs(out[i][0] - previous[6]) > DISTANCE_EPSILON || Math.abs(out[i][1] - previous[7]) > DISTANCE_EPSILON) {
|
||||
throw new Error('RoundedPolygon must be contiguous')
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
/** RoundedPolygon(vertices, rounding, perVertexRounding, centerX, centerY). */
|
||||
function polygonFromVertices(vertices, { cornerRounding = UNROUNDED, perVertexRounding = null, center = null } = {}) {
|
||||
const n = vertices.length
|
||||
|
||||
if (n < 3) {
|
||||
throw new Error('Polygons must have at least 3 vertices')
|
||||
}
|
||||
|
||||
if (perVertexRounding !== null && perVertexRounding.length !== n) {
|
||||
throw new Error('perVertexRounding list should be either null or the same size as the number of vertices')
|
||||
}
|
||||
|
||||
const roundedCorners = vertices.map((vertex, i) => new RoundedCorner(vertices[(i + n - 1) % n], vertex, vertices[(i + 1) % n], perVertexRounding?.[i] ?? cornerRounding))
|
||||
|
||||
const cutAdjusts = vertices.map((vertex, i) => {
|
||||
const next = (i + 1) % n
|
||||
const expectedRoundCut = roundedCorners[i].expectedRoundCut + roundedCorners[next].expectedRoundCut
|
||||
const expectedCut = roundedCorners[i].expectedCut + roundedCorners[next].expectedCut
|
||||
const sideSize = length(minus(vertex, vertices[next]))
|
||||
|
||||
if (expectedRoundCut > sideSize) {
|
||||
return [sideSize / expectedRoundCut, 0]
|
||||
}
|
||||
|
||||
if (expectedCut > sideSize) {
|
||||
return [1, (sideSize - expectedRoundCut) / (expectedCut - expectedRoundCut)]
|
||||
}
|
||||
|
||||
return [1, 1]
|
||||
})
|
||||
|
||||
const corners = roundedCorners.map((corner, i) => {
|
||||
const allowedCuts = [0, 1].map((delta) => {
|
||||
const [roundCutRatio, cutRatio] = cutAdjusts[(i + n - 1 + delta) % n]
|
||||
|
||||
return corner.expectedRoundCut * roundCutRatio + (corner.expectedCut - corner.expectedRoundCut) * cutRatio
|
||||
})
|
||||
|
||||
return corner.getCubics(allowedCuts[0], allowedCuts[1])
|
||||
})
|
||||
|
||||
const features = []
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const previous = vertices[(i + n - 1) % n]
|
||||
const next = vertices[(i + 1) % n]
|
||||
const end = corners[i].at(-1)
|
||||
const start = corners[(i + 1) % n][0]
|
||||
|
||||
features.push({ type: 'corner', convex: convex(previous, vertices[i], next), cubics: corners[i] })
|
||||
features.push({ type: 'edge', cubics: [straightLine(end[6], end[7], start[0], start[1])] })
|
||||
}
|
||||
|
||||
if (center === null) {
|
||||
center = point(vertices.reduce((sum, v) => sum + v.x, 0) / n, vertices.reduce((sum, v) => sum + v.y, 0) / n)
|
||||
}
|
||||
|
||||
return new RoundedPolygon(features, center)
|
||||
}
|
||||
|
||||
// Shapes.kt -----------------------------------------------------------------------------------
|
||||
|
||||
/** RoundedPolygon(numVertices, radius, centerX, centerY, rounding, perVertexRounding). */
|
||||
function regularPolygon(numVertices, { radius = 1, cornerRounding = UNROUNDED, perVertexRounding = null } = {}) {
|
||||
const vertices = Array.from({ length: numVertices }, (_, i) => radialToCartesian(radius, (Math.PI / numVertices) * 2 * i))
|
||||
|
||||
return polygonFromVertices(vertices, { cornerRounding, perVertexRounding, center: point(0, 0) })
|
||||
}
|
||||
|
||||
/** RoundedPolygon.circle(numVertices, radius): a regular polygon rounded all the way round. */
|
||||
function circlePolygon(numVertices = 8, radius = 1) {
|
||||
const polygonRadius = radius / Math.cos(Math.PI / numVertices)
|
||||
|
||||
return regularPolygon(numVertices, { radius: polygonRadius, cornerRounding: rounding(radius) })
|
||||
}
|
||||
|
||||
/** RoundedPolygon.star(numVerticesPerRadius, radius, innerRadius, rounding, innerRounding). */
|
||||
function star(numVerticesPerRadius, { radius = 1, innerRadius = 0.5, cornerRounding = UNROUNDED, innerCornerRounding = cornerRounding } = {}) {
|
||||
const vertices = []
|
||||
const roundings = []
|
||||
|
||||
for (let i = 0; i < numVerticesPerRadius; i++) {
|
||||
vertices.push(radialToCartesian(radius, (Math.PI / numVerticesPerRadius) * 2 * i), radialToCartesian(innerRadius, (Math.PI / numVerticesPerRadius) * (2 * i + 1)))
|
||||
roundings.push(cornerRounding, innerCornerRounding)
|
||||
}
|
||||
|
||||
return polygonFromVertices(vertices, { perVertexRounding: roundings, center: point(0, 0) })
|
||||
}
|
||||
|
||||
// Morph: FloatMapping.kt, PolygonMeasure.kt, FeatureMapping.kt, Morph.kt ----------------------
|
||||
|
||||
const positiveModulo = (num, mod) => ((num % mod) + mod) % mod
|
||||
const progressInRange = (progress, from, to) => (to >= from ? progress >= from && progress <= to : progress >= from || progress <= to)
|
||||
const progressDistance = (a, b) => Math.min(Math.abs(a - b), 1 - Math.abs(a - b))
|
||||
|
||||
function linearMap(xValues, yValues, x) {
|
||||
const n = xValues.length
|
||||
const start = xValues.findIndex((_, i) => progressInRange(x, xValues[i], xValues[(i + 1) % n]))
|
||||
const end = (start + 1) % n
|
||||
const sizeX = positiveModulo(xValues[end] - xValues[start], 1)
|
||||
const sizeY = positiveModulo(yValues[end] - yValues[start], 1)
|
||||
const position = sizeX < 0.001 ? 0.5 : positiveModulo(x - xValues[start], 1) / sizeX
|
||||
|
||||
return positiveModulo(yValues[start] + sizeY * position, 1)
|
||||
}
|
||||
|
||||
const MEASURE_SEGMENTS = 3
|
||||
|
||||
/** LengthMeasurer.closestProgressTo: [the parameter at which `threshold` length is reached, the length]. */
|
||||
function closestProgressTo(c, threshold) {
|
||||
let total = 0
|
||||
let remainder = threshold
|
||||
let previous = point(c[0], c[1])
|
||||
|
||||
for (let i = 1; i <= MEASURE_SEGMENTS; i++) {
|
||||
const progress = i / MEASURE_SEGMENTS
|
||||
const p = pointOnCurve(c, progress)
|
||||
const segment = Math.hypot(p.x - previous.x, p.y - previous.y)
|
||||
|
||||
if (segment >= remainder) {
|
||||
return [progress - (1 - remainder / segment) / MEASURE_SEGMENTS, threshold]
|
||||
}
|
||||
|
||||
remainder -= segment
|
||||
total += segment
|
||||
previous = p
|
||||
}
|
||||
|
||||
return [1, total]
|
||||
}
|
||||
|
||||
class MeasuredCubic {
|
||||
constructor(c, startOutlineProgress, endOutlineProgress) {
|
||||
if (endOutlineProgress < startOutlineProgress) {
|
||||
throw new Error('endOutlineProgress is expected to be equal or greater than startOutlineProgress')
|
||||
}
|
||||
|
||||
this.cubic = c
|
||||
this.startOutlineProgress = startOutlineProgress
|
||||
this.endOutlineProgress = endOutlineProgress
|
||||
this.measuredSize = closestProgressTo(c, Infinity)[1]
|
||||
}
|
||||
|
||||
cutAtProgress(cutOutlineProgress) {
|
||||
const bounded = clamp(cutOutlineProgress, this.startOutlineProgress, this.endOutlineProgress)
|
||||
const relativeProgress = (bounded - this.startOutlineProgress) / (this.endOutlineProgress - this.startOutlineProgress)
|
||||
const t = closestProgressTo(this.cubic, relativeProgress * this.measuredSize)[0]
|
||||
const [c1, c2] = split(this.cubic, t)
|
||||
|
||||
return [new MeasuredCubic(c1, this.startOutlineProgress, bounded), new MeasuredCubic(c2, bounded, this.endOutlineProgress)]
|
||||
}
|
||||
}
|
||||
|
||||
class MeasuredPolygon {
|
||||
constructor(features, cubics, outlineProgress) {
|
||||
this.features = features
|
||||
this.cubics = []
|
||||
|
||||
let startOutlineProgress = 0
|
||||
|
||||
for (let i = 0; i < cubics.length; i++) {
|
||||
if (outlineProgress[i + 1] - outlineProgress[i] > DISTANCE_EPSILON) {
|
||||
this.cubics.push(new MeasuredCubic(cubics[i], startOutlineProgress, outlineProgress[i + 1]))
|
||||
startOutlineProgress = outlineProgress[i + 1]
|
||||
}
|
||||
}
|
||||
|
||||
this.cubics.at(-1).endOutlineProgress = 1
|
||||
}
|
||||
|
||||
static measure(polygon) {
|
||||
const cubics = []
|
||||
const featureToCubic = []
|
||||
|
||||
for (const feature of polygon.features) {
|
||||
feature.cubics.forEach((c, i) => {
|
||||
if (feature.type === 'corner' && i === Math.floor(feature.cubics.length / 2)) {
|
||||
featureToCubic.push([feature, cubics.length])
|
||||
}
|
||||
|
||||
cubics.push(c)
|
||||
})
|
||||
}
|
||||
|
||||
const measures = [0]
|
||||
|
||||
for (const c of cubics) {
|
||||
measures.push(measures.at(-1) + closestProgressTo(c, Infinity)[1])
|
||||
}
|
||||
|
||||
const outlineProgress = measures.map((measure) => measure / measures.at(-1))
|
||||
const features = featureToCubic.map(([feature, ix]) => ({
|
||||
progress: positiveModulo((outlineProgress[ix] + outlineProgress[ix + 1]) / 2, 1),
|
||||
feature,
|
||||
}))
|
||||
|
||||
return new MeasuredPolygon(features, cubics, outlineProgress)
|
||||
}
|
||||
|
||||
cutAndShift(cuttingPoint) {
|
||||
if (cuttingPoint < DISTANCE_EPSILON) {
|
||||
return this
|
||||
}
|
||||
|
||||
const n = this.cubics.length
|
||||
const targetIndex = this.cubics.findIndex((c) => cuttingPoint >= c.startOutlineProgress && cuttingPoint <= c.endOutlineProgress)
|
||||
const [b1, b2] = this.cubics[targetIndex].cutAtProgress(cuttingPoint)
|
||||
const cubics = [b2.cubic]
|
||||
|
||||
for (let i = 1; i < n; i++) {
|
||||
cubics.push(this.cubics[(i + targetIndex) % n].cubic)
|
||||
}
|
||||
|
||||
cubics.push(b1.cubic)
|
||||
|
||||
const outlineProgress = Array.from({ length: n + 2 }, (_, index) => {
|
||||
if (index === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
if (index === n + 1) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return positiveModulo(this.cubics[(targetIndex + index - 1) % n].endOutlineProgress - cuttingPoint, 1)
|
||||
})
|
||||
|
||||
const features = this.features.map(({ progress, feature }) => ({ progress: positiveModulo(progress - cuttingPoint, 1), feature }))
|
||||
|
||||
return new MeasuredPolygon(features, cubics, outlineProgress)
|
||||
}
|
||||
}
|
||||
|
||||
function featureDistSquared(f1, f2) {
|
||||
if (f1.type === 'corner' && f2.type === 'corner' && f1.convex !== f2.convex) {
|
||||
return Infinity
|
||||
}
|
||||
|
||||
const representative = (feature) => {
|
||||
const [first, last] = [feature.cubics[0], feature.cubics.at(-1)]
|
||||
|
||||
return point((first[0] + last[6]) / 2, (first[1] + last[7]) / 2)
|
||||
}
|
||||
|
||||
const [p1, p2] = [representative(f1), representative(f2)]
|
||||
|
||||
return (p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2
|
||||
}
|
||||
|
||||
function doMapping(features1, features2) {
|
||||
const distanceVertexList = []
|
||||
|
||||
for (const f1 of features1) {
|
||||
for (const f2 of features2) {
|
||||
const distance = featureDistSquared(f1.feature, f2.feature)
|
||||
|
||||
if (distance !== Infinity) {
|
||||
distanceVertexList.push({ distance, f1, f2 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
distanceVertexList.sort((a, b) => a.distance - b.distance)
|
||||
|
||||
if (distanceVertexList.length === 0) {
|
||||
return [
|
||||
[0, 0],
|
||||
[0.5, 0.5],
|
||||
]
|
||||
}
|
||||
|
||||
if (distanceVertexList.length === 1) {
|
||||
const { f1, f2 } = distanceVertexList[0]
|
||||
|
||||
return [
|
||||
[f1.progress, f2.progress],
|
||||
[(f1.progress + 0.5) % 1, (f2.progress + 0.5) % 1],
|
||||
]
|
||||
}
|
||||
|
||||
const mapping = []
|
||||
const usedF1 = new Set()
|
||||
const usedF2 = new Set()
|
||||
|
||||
for (const { f1, f2 } of distanceVertexList) {
|
||||
if (usedF1.has(f1) || usedF2.has(f2)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const insertionIndex = mapping.findIndex((m) => m[0] >= f1.progress)
|
||||
const index = insertionIndex === -1 ? mapping.length : insertionIndex
|
||||
|
||||
if (index < mapping.length && mapping[index][0] === f1.progress) {
|
||||
throw new Error("There can't be two features with the same progress")
|
||||
}
|
||||
|
||||
const n = mapping.length
|
||||
|
||||
if (n >= 1) {
|
||||
const [before1, before2] = mapping[(index + n - 1) % n]
|
||||
const [after1, after2] = mapping[index % n]
|
||||
|
||||
if (
|
||||
progressDistance(f1.progress, before1) < DISTANCE_EPSILON ||
|
||||
progressDistance(f1.progress, after1) < DISTANCE_EPSILON ||
|
||||
progressDistance(f2.progress, before2) < DISTANCE_EPSILON ||
|
||||
progressDistance(f2.progress, after2) < DISTANCE_EPSILON
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (n > 1 && !progressInRange(f2.progress, before2, after2)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
mapping.splice(index, 0, [f1.progress, f2.progress])
|
||||
usedF1.add(f1)
|
||||
usedF2.add(f2)
|
||||
}
|
||||
|
||||
return mapping
|
||||
}
|
||||
|
||||
/** Morph.match: both shapes cut into pairs of matching cubics. */
|
||||
function match(p1, p2) {
|
||||
const measuredPolygon1 = MeasuredPolygon.measure(p1)
|
||||
const measuredPolygon2 = MeasuredPolygon.measure(p2)
|
||||
const corners = (features) => features.filter(({ feature }) => feature.type === 'corner')
|
||||
const mappings = doMapping(corners(measuredPolygon1.features), corners(measuredPolygon2.features))
|
||||
const [sources, targets] = [mappings.map((m) => m[0]), mappings.map((m) => m[1])]
|
||||
const map = (x) => linearMap(sources, targets, x)
|
||||
const mapBack = (x) => linearMap(targets, sources, x)
|
||||
const polygon2CutPoint = map(0)
|
||||
const bs1 = measuredPolygon1.cubics
|
||||
const bs2 = measuredPolygon2.cutAndShift(polygon2CutPoint).cubics
|
||||
const pairs = []
|
||||
|
||||
let i1 = 0
|
||||
let i2 = 0
|
||||
let b1 = bs1[i1++]
|
||||
let b2 = bs2[i2++]
|
||||
|
||||
while (b1 !== undefined && b2 !== undefined) {
|
||||
const b1a = i1 === bs1.length ? 1 : b1.endOutlineProgress
|
||||
const b2a = i2 === bs2.length ? 1 : mapBack(positiveModulo(b2.endOutlineProgress + polygon2CutPoint, 1))
|
||||
const minb = Math.min(b1a, b2a)
|
||||
let seg1
|
||||
let seg2
|
||||
|
||||
if (b1a > minb + ANGLE_EPSILON) {
|
||||
;[seg1, b1] = b1.cutAtProgress(minb)
|
||||
} else {
|
||||
seg1 = b1
|
||||
b1 = bs1[i1++]
|
||||
}
|
||||
|
||||
if (b2a > minb + ANGLE_EPSILON) {
|
||||
;[seg2, b2] = b2.cutAtProgress(positiveModulo(map(minb) - polygon2CutPoint, 1))
|
||||
} else {
|
||||
seg2 = b2
|
||||
b2 = bs2[i2++]
|
||||
}
|
||||
|
||||
pairs.push([seg1.cubic, seg2.cubic])
|
||||
}
|
||||
|
||||
if (b1 !== undefined || b2 !== undefined) {
|
||||
throw new Error("Expected both Polygon's Cubic to be fully matched")
|
||||
}
|
||||
|
||||
return pairs
|
||||
}
|
||||
|
||||
/** Morph.asCubics: every matched pair interpolated at `progress`, closed on its first anchor. */
|
||||
function asCubics(pairs, progress) {
|
||||
const cubics = pairs.map(([start, end]) => start.map((value, i) => value + (end[i] - value) * progress))
|
||||
|
||||
cubics.at(-1)[6] = cubics[0][0]
|
||||
cubics.at(-1)[7] = cubics[0][1]
|
||||
|
||||
return cubics
|
||||
}
|
||||
|
||||
export {
|
||||
point,
|
||||
plus,
|
||||
minus,
|
||||
times,
|
||||
length,
|
||||
pointOnCurve,
|
||||
split,
|
||||
turningPoints,
|
||||
cubicBounds,
|
||||
rounding,
|
||||
UNROUNDED,
|
||||
polygonFromVertices,
|
||||
regularPolygon,
|
||||
circlePolygon,
|
||||
star,
|
||||
match,
|
||||
asCubics,
|
||||
}
|
||||
+21
-26
@@ -83,7 +83,6 @@ const TICK_MIN_SPACING = 8
|
||||
const TOUCH_SLOP = 8
|
||||
|
||||
const VALUE = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')
|
||||
const VALUE_AS_NUMBER = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'valueAsNumber')
|
||||
|
||||
const clamp = (value, min, max) => Math.min(Math.max(value, min), max)
|
||||
const near = (a, b) => Math.abs(a - b) < 1e-6
|
||||
@@ -206,6 +205,7 @@ function slider(root) {
|
||||
const drawing = root.querySelector('[data-md-slider-drawing]')
|
||||
const range = inputs.length > 1
|
||||
const cleanups = []
|
||||
const controller = new AbortController()
|
||||
let queued = false
|
||||
let release = null
|
||||
// Focus this script gave an input on a press, which draws no ring until a key is pressed.
|
||||
@@ -214,8 +214,7 @@ function slider(root) {
|
||||
let spaceHeld = false
|
||||
|
||||
const listen = (target, type, handler, capture = false) => {
|
||||
target.addEventListener(type, handler, capture)
|
||||
cleanups.push(() => target.removeEventListener(type, handler, capture))
|
||||
target.addEventListener(type, handler, { capture, signal: controller.signal })
|
||||
}
|
||||
|
||||
/** The inputs' min, max and step, as a native range reads them; `step` is null for `any`. */
|
||||
@@ -379,25 +378,20 @@ function slider(root) {
|
||||
const pressed = (index) => drawing?.querySelectorAll('[data-md-slider-handle]').forEach((thumb, i) => thumb.toggleAttribute('data-md-pressed', i === index))
|
||||
|
||||
inputs.forEach((input) => {
|
||||
// A value written by script fires no event: catch it on the input's own setters.
|
||||
for (const [property, descriptor] of [['value', VALUE], ['valueAsNumber', VALUE_AS_NUMBER]]) {
|
||||
Object.defineProperty(input, property, {
|
||||
configurable: true,
|
||||
get() {
|
||||
return descriptor.get.call(this)
|
||||
},
|
||||
set(value) {
|
||||
descriptor.set.call(this, value)
|
||||
schedule()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
cleanups.push(() => {
|
||||
delete input.value
|
||||
delete input.valueAsNumber
|
||||
// A value written by script fires no event: catch it on the input's own setter.
|
||||
Object.defineProperty(input, 'value', {
|
||||
configurable: true,
|
||||
get() {
|
||||
return VALUE.get.call(this)
|
||||
},
|
||||
set(value) {
|
||||
VALUE.set.call(this, value)
|
||||
schedule()
|
||||
},
|
||||
})
|
||||
|
||||
cleanups.push(() => delete input.value)
|
||||
|
||||
listen(input, 'focus', () => showFocus(input, focusedByPointer !== input && input.matches(':focus-visible')))
|
||||
listen(input, 'blur', () => {
|
||||
focusedByPointer = null
|
||||
@@ -510,6 +504,7 @@ function slider(root) {
|
||||
return {
|
||||
destroy() {
|
||||
release?.()
|
||||
controller.abort()
|
||||
cleanups.forEach((cleanup) => cleanup())
|
||||
},
|
||||
|
||||
@@ -613,17 +608,17 @@ function slider(root) {
|
||||
})
|
||||
}
|
||||
|
||||
const dragController = new AbortController()
|
||||
|
||||
release = () => {
|
||||
window.removeEventListener('pointermove', move)
|
||||
window.removeEventListener('pointerup', end)
|
||||
window.removeEventListener('pointercancel', end)
|
||||
dragController.abort()
|
||||
pressed(null)
|
||||
release = null
|
||||
}
|
||||
|
||||
window.addEventListener('pointermove', move)
|
||||
window.addEventListener('pointerup', end)
|
||||
window.addEventListener('pointercancel', end)
|
||||
window.addEventListener('pointermove', move, { signal: dragController.signal })
|
||||
window.addEventListener('pointerup', end, { signal: dragController.signal })
|
||||
window.addEventListener('pointercancel', end, { signal: dragController.signal })
|
||||
|
||||
if (dragging) {
|
||||
follow(start)
|
||||
|
||||
+4
-10
@@ -17,6 +17,8 @@
|
||||
* `previewScheme(name)` shows another one on this page without storing anything — the
|
||||
* application saves a choice itself, and the next full load draws what it saved.
|
||||
*/
|
||||
import { remember } from './util.js'
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
const root = document.documentElement
|
||||
const media = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
@@ -53,11 +55,7 @@ document.addEventListener('alpine:init', () => {
|
||||
root.dataset.themeChoice = choice
|
||||
root.dataset.theme = this.resolved
|
||||
|
||||
try {
|
||||
localStorage.setItem(root.dataset.themeKey || 'material-theme', choice)
|
||||
} catch {
|
||||
// Blocked storage: the page still switches, it just will not remember.
|
||||
}
|
||||
remember(root.dataset.themeKey || 'material-theme', choice)
|
||||
},
|
||||
|
||||
toggle() {
|
||||
@@ -80,11 +78,7 @@ document.addEventListener('alpine:init', () => {
|
||||
root.dataset.contrast = this.resolvedContrast
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem(root.dataset.contrastKey || 'material-contrast', choice)
|
||||
} catch {
|
||||
// Blocked storage: the page still switches, it just will not remember.
|
||||
}
|
||||
remember(root.dataset.contrastKey || 'material-contrast', choice)
|
||||
},
|
||||
|
||||
previewScheme(name) {
|
||||
|
||||
+14
-25
@@ -208,13 +208,16 @@ document.addEventListener('alpine:init', () => {
|
||||
return false
|
||||
},
|
||||
|
||||
/** The allowed time nearest to one that is not, searching outwards a minute at a time. */
|
||||
nearest(time) {
|
||||
/**
|
||||
* The allowed time nearest to one that is not, searching outwards a minute at a time; `pm`
|
||||
* limits the search to that half of the day, for a period switch that lands outside it.
|
||||
*/
|
||||
nearest(time, pm = null) {
|
||||
for (let distance = 0; distance <= 720; distance++) {
|
||||
for (const candidate of [time - distance, time + distance]) {
|
||||
const wrapped = (candidate + 1440) % 1440
|
||||
|
||||
if (this.allowed(Math.floor(wrapped / 60), wrapped % 60)) {
|
||||
if ((pm === null || (wrapped >= 720) === pm) && this.allowed(Math.floor(wrapped / 60), wrapped % 60)) {
|
||||
return wrapped
|
||||
}
|
||||
}
|
||||
@@ -370,7 +373,8 @@ document.addEventListener('alpine:init', () => {
|
||||
if (this.hourAllowed(hour)) {
|
||||
this.setHour(hour)
|
||||
} else {
|
||||
const time = this.nearestInPeriod(pm, hour * 60 + this.minute)
|
||||
const raw = hour * 60 + this.minute
|
||||
const time = this.nearest(raw, pm) ?? raw
|
||||
|
||||
this.hour = Math.floor(time / 60)
|
||||
this.minute = time % 60
|
||||
@@ -395,19 +399,6 @@ document.addEventListener('alpine:init', () => {
|
||||
event.currentTarget.querySelector(`[data-md-timepicker-period-option="${pm ? 'pm' : 'am'}"]`)?.focus()
|
||||
},
|
||||
|
||||
/** The allowed time in a period nearest to `time` (periodAllowed has said there is one). */
|
||||
nearestInPeriod(pm, time) {
|
||||
let best = null
|
||||
|
||||
for (let candidate = pm ? 720 : 0; candidate < (pm ? 1440 : 720); candidate++) {
|
||||
if (this.allowed(Math.floor(candidate / 60), candidate % 60) && (best === null || Math.abs(candidate - time) < Math.abs(best - time))) {
|
||||
best = candidate
|
||||
}
|
||||
}
|
||||
|
||||
return best ?? time
|
||||
},
|
||||
|
||||
// ---- The dial --------------------------------------------------------------------------
|
||||
|
||||
angleFor() {
|
||||
@@ -450,10 +441,10 @@ document.addEventListener('alpine:init', () => {
|
||||
this.pick(moveEvent, false)
|
||||
}
|
||||
|
||||
const controller = new AbortController()
|
||||
|
||||
const release = (upEvent, cancelled = false) => {
|
||||
dial.removeEventListener('pointermove', move)
|
||||
dial.removeEventListener('pointerup', release)
|
||||
dial.removeEventListener('pointercancel', abandon)
|
||||
controller.abort()
|
||||
this.dragging = false
|
||||
|
||||
if (cancelled) {
|
||||
@@ -473,11 +464,9 @@ document.addEventListener('alpine:init', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const abandon = (cancelEvent) => release(cancelEvent, true)
|
||||
|
||||
dial.addEventListener('pointermove', move)
|
||||
dial.addEventListener('pointerup', release)
|
||||
dial.addEventListener('pointercancel', abandon)
|
||||
dial.addEventListener('pointermove', move, { signal: controller.signal })
|
||||
dial.addEventListener('pointerup', release, { signal: controller.signal })
|
||||
dial.addEventListener('pointercancel', (cancelEvent) => release(cancelEvent, true), { signal: controller.signal })
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
+6
-15
@@ -4,12 +4,13 @@
|
||||
* on a right-to-left page; Home and End to its ends). Every control stays in the Tab order, so a
|
||||
* control added by a Livewire render is reachable without any bookkeeping.
|
||||
*/
|
||||
import { ringIndex } from './util.js'
|
||||
|
||||
const CONTROLS = 'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
window.Alpine.data('materialToolbar', (vertical = false) => ({
|
||||
move(event) {
|
||||
const keys = vertical ? { ArrowUp: -1, ArrowDown: 1 } : { ArrowLeft: -1, ArrowRight: 1 }
|
||||
const controls = [...this.$root.querySelectorAll(CONTROLS)].filter((control) => control.getClientRects().length > 0)
|
||||
const at = controls.indexOf(document.activeElement)
|
||||
|
||||
@@ -17,22 +18,12 @@ document.addEventListener('alpine:init', () => {
|
||||
return
|
||||
}
|
||||
|
||||
let next = null
|
||||
const rtl = !vertical && getComputedStyle(this.$root).direction === 'rtl'
|
||||
const next = ringIndex(event.key, at, controls.length, { rtl, vertical })
|
||||
|
||||
if (event.key === 'Home') {
|
||||
next = controls[0]
|
||||
} else if (event.key === 'End') {
|
||||
next = controls.at(-1)
|
||||
} else if (event.key in keys) {
|
||||
const rtl = !vertical && getComputedStyle(this.$root).direction === 'rtl'
|
||||
const by = keys[event.key] * (rtl ? -1 : 1)
|
||||
|
||||
next = controls[(at + by + controls.length) % controls.length]
|
||||
}
|
||||
|
||||
if (next) {
|
||||
if (next !== null) {
|
||||
event.preventDefault()
|
||||
next.focus()
|
||||
controls[next].focus()
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
+51
-34
@@ -13,25 +13,58 @@
|
||||
import { openingFocus } from './layers.js'
|
||||
|
||||
const HOVER_DELAY_MS = 500
|
||||
const LEAVE_DELAY_MS = 1500
|
||||
export const LEAVE_DELAY_MS = 1500
|
||||
|
||||
let visible = null
|
||||
|
||||
/**
|
||||
* The hover/focus/Escape timing a tooltip and a transient rich tooltip (rich-tooltip.js) share:
|
||||
* shows after HOVER_DELAY_MS on a pointer that can hover, at once on a focus `focusable` accepts
|
||||
* (never the focus layers.js moves as a sheet or dialog opens), hides after LEAVE_DELAY_MS once
|
||||
* the pointer leaves, and at once on Escape. `open`, `show` and `hide` do the actual popover call;
|
||||
* a caller adds its own extra listeners (a press, a blur) on the returned `signal`, and calls
|
||||
* `hide` from them.
|
||||
*/
|
||||
export function hoverPopover(el, { open, show, hide, focusable }) {
|
||||
const controller = new AbortController()
|
||||
const { signal } = controller
|
||||
let timer = null
|
||||
|
||||
const delayed = (action, delay) => {
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(action, delay)
|
||||
}
|
||||
|
||||
const showLater = (delay) => delayed(() => !open() && show(), delay)
|
||||
const hideLater = (delay = 0) => delayed(() => open() && hide(), delay)
|
||||
|
||||
el.addEventListener('pointerenter', (event) => event.pointerType === 'mouse' && showLater(HOVER_DELAY_MS), { signal })
|
||||
el.addEventListener('pointerleave', () => hideLater(LEAVE_DELAY_MS), { signal })
|
||||
el.addEventListener('focusin', (event) => !openingFocus(event) && focusable(event) && showLater(0), { signal })
|
||||
document.addEventListener('keydown', (event) => event.key === 'Escape' && hideLater(), { signal })
|
||||
|
||||
return {
|
||||
hide: hideLater,
|
||||
signal,
|
||||
destroy() {
|
||||
clearTimeout(timer)
|
||||
controller.abort()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
window.Alpine.data('materialTooltip', () => ({
|
||||
timer: null,
|
||||
listeners: [],
|
||||
popover: null,
|
||||
|
||||
init() {
|
||||
const tip = this.$el
|
||||
const trigger = tip.parentElement
|
||||
|
||||
const open = () => tip.matches(':popover-open')
|
||||
|
||||
const show = (delay) => {
|
||||
clearTimeout(this.timer)
|
||||
this.timer = setTimeout(() => {
|
||||
if (!tip.isConnected || open()) {
|
||||
const popover = (this.popover = hoverPopover(trigger, {
|
||||
open: () => tip.matches(':popover-open'),
|
||||
show: () => {
|
||||
if (!tip.isConnected) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -41,43 +74,27 @@ document.addEventListener('alpine:init', () => {
|
||||
|
||||
visible = tip
|
||||
tip.showPopover()
|
||||
}, delay)
|
||||
}
|
||||
|
||||
const hide = (delay = 0) => {
|
||||
clearTimeout(this.timer)
|
||||
this.timer = setTimeout(() => {
|
||||
if (open()) {
|
||||
tip.hidePopover()
|
||||
}
|
||||
},
|
||||
hide: () => {
|
||||
tip.hidePopover()
|
||||
|
||||
if (visible === tip) {
|
||||
visible = null
|
||||
}
|
||||
}, delay)
|
||||
}
|
||||
},
|
||||
focusable: () => trigger.matches(':focus-within:has(:focus-visible), :focus-visible'),
|
||||
}))
|
||||
|
||||
this.listen(trigger, 'pointerenter', (event) => event.pointerType === 'mouse' && show(HOVER_DELAY_MS))
|
||||
this.listen(trigger, 'pointerleave', () => hide(LEAVE_DELAY_MS))
|
||||
this.listen(trigger, 'pointerdown', () => hide())
|
||||
this.listen(trigger, 'focusin', (event) => !openingFocus(event) && trigger.matches(':focus-within:has(:focus-visible), :focus-visible') && show(0))
|
||||
this.listen(trigger, 'focusout', () => hide(LEAVE_DELAY_MS))
|
||||
this.listen(document, 'keydown', (event) => event.key === 'Escape' && hide())
|
||||
},
|
||||
|
||||
listen(target, type, handler) {
|
||||
target.addEventListener(type, handler)
|
||||
this.listeners.push(() => target.removeEventListener(type, handler))
|
||||
trigger.addEventListener('pointerdown', () => popover.hide(), { signal: popover.signal })
|
||||
trigger.addEventListener('focusout', () => popover.hide(LEAVE_DELAY_MS), { signal: popover.signal })
|
||||
},
|
||||
|
||||
destroy() {
|
||||
clearTimeout(this.timer)
|
||||
this.popover.destroy()
|
||||
|
||||
if (visible === this.$el) {
|
||||
visible = null
|
||||
}
|
||||
|
||||
this.listeners.forEach((remove) => remove())
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Small pieces shared by more than one component, in the pattern of breakpoints.js: a plain
|
||||
* function or two, not a framework.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The index an Arrow/Home/End key moves to from `index` (which may be -1, nothing chosen yet),
|
||||
* wrapping around the ends; null when the key means nothing here. `vertical` picks Up/Down over
|
||||
* Left/Right; `rtl` mirrors Left/Right (Up/Down never mirror). A caller that answers to both axes
|
||||
* at once — a chip set's row, which also takes Up/Down — calls this twice and takes whichever
|
||||
* answers.
|
||||
*/
|
||||
export function ringIndex(key, index, length, { rtl = false, vertical = false } = {}) {
|
||||
if (key === 'Home') {
|
||||
return 0
|
||||
}
|
||||
|
||||
if (key === 'End') {
|
||||
return length - 1
|
||||
}
|
||||
|
||||
const forward = vertical ? 'ArrowDown' : rtl ? 'ArrowLeft' : 'ArrowRight'
|
||||
const backward = vertical ? 'ArrowUp' : rtl ? 'ArrowRight' : 'ArrowLeft'
|
||||
|
||||
if (key === forward) {
|
||||
return (index + 1 + length) % length
|
||||
}
|
||||
|
||||
if (key === backward) {
|
||||
return index < 0 ? length - 1 : (index - 1 + length) % length
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/** A CSS custom property's time value in milliseconds, whichever unit it is written in; null when unset. */
|
||||
export function ms(element, token) {
|
||||
const value = getComputedStyle(element).getPropertyValue(token).trim()
|
||||
const number = parseFloat(value)
|
||||
|
||||
if (Number.isNaN(number)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return value.endsWith('ms') ? number : number * 1000
|
||||
}
|
||||
|
||||
/** Writes to localStorage, quietly: a blocked or full store still lets the page work, it just won't remember. */
|
||||
export function remember(key, value) {
|
||||
try {
|
||||
localStorage.setItem(key, value)
|
||||
} catch {
|
||||
// Blocked storage: the page still works, it just will not remember.
|
||||
}
|
||||
}
|
||||
|
||||
const REOPEN_GUARD_MS = 250
|
||||
|
||||
/**
|
||||
* A `popover="auto"` closes on the press that lands on its trigger, and the click that follows
|
||||
* would open it again. A close this recent is taken as that press. It is timed from
|
||||
* `beforetoggle`, which fires as the popover closes: `toggle` is queued, and arrives after that
|
||||
* click. Only a close the browser made on its own (light dismiss) counts — `closing()` marks one
|
||||
* this script makes as explicit, so a keyboard command that shuts it is never mistaken for that
|
||||
* race, only the browser's own light dismiss racing the click it precedes is.
|
||||
*/
|
||||
export function reopenGuard() {
|
||||
let closedAt = -Infinity
|
||||
let closingExplicitly = false
|
||||
|
||||
return {
|
||||
beforeToggle(event) {
|
||||
if (event.newState === 'closed' && !closingExplicitly) {
|
||||
closedAt = performance.now()
|
||||
}
|
||||
|
||||
closingExplicitly = false
|
||||
},
|
||||
|
||||
closing() {
|
||||
closingExplicitly = true
|
||||
},
|
||||
|
||||
canOpen() {
|
||||
return performance.now() - closedAt > REOPEN_GUARD_MS
|
||||
},
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -6,13 +6,13 @@
|
||||
`color` (alias `tone`): `info` (the default), `success`, `warning`, `error`, `primary`,
|
||||
`secondary`, `tertiary`, or `neutral` for surface-container-high. `icon` replaces the state's
|
||||
icon; `:icon="false"` drops it. `dismissible` adds a close button that hides it in the browser,
|
||||
40px drawn with the 48px target M3 asks for (ACT-18).
|
||||
40px drawn with the 48px target M3 asks for.
|
||||
|
||||
It is a `role="status"`, whatever its colour: an alert is usually part of the page as it
|
||||
renders, and `role="alert"` is an assertive live region that some screen readers announce over
|
||||
the page title on load — and that a Livewire morph re-announces. `assertive` opts into
|
||||
`role="alert"` for the case the prop is named after: a notice put on screen in answer to
|
||||
something the person just did (ACT-36). M3 publishes no banner; the nearest thing it does
|
||||
something the person just did. M3 publishes no banner; the nearest thing it does
|
||||
publish, the snackbar, says polite and never assertive.
|
||||
|
||||
Drawn by resources/css/components/alert.css from `data-md-alert` and `data-md-color`; the
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
default => 'outline',
|
||||
};
|
||||
|
||||
$attributes = $attributes->merge(array_filter([
|
||||
$attributes = $attributes->merge([
|
||||
'data-md-badge' => true,
|
||||
'data-md-dot' => $dot ? true : null,
|
||||
'data-md-variant' => $variant,
|
||||
@@ -74,7 +74,7 @@
|
||||
'data-md-floating' => $floating ? true : null,
|
||||
'aria-label' => $label,
|
||||
'aria-hidden' => $label === null && ! $status ? 'true' : null,
|
||||
], fn ($value): bool => $value !== null));
|
||||
]);
|
||||
@endphp
|
||||
|
||||
<span {{ $attributes }}>@unless ($dot)@if ($markup){{ $slot }}@else{{ $text }}@endif@endunless</span>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
The handle's wrapper is the drag target and M3's 48dp hit area: the button's own visual bar is
|
||||
32×4px, and `md-touch-target` (foundation/interaction.css) extends it, while the wrapper's own
|
||||
22px top/bottom padding (`SheetDefaults.kt`'s `DragHandleVerticalPadding`) gives the row its
|
||||
M3-specified height regardless — 4 + 22 + 22 = 48px (C-01). The button renders the shared
|
||||
M3-specified height regardless — 4 + 22 + 22 = 48px. The button renders the shared
|
||||
`md-focus-ring` and `md-touch-target` classes rather than a hand-rolled ring: unlike the
|
||||
datepicker's day, its own visible bar is not a smaller indicator drawn inside a bigger box the
|
||||
classes cannot reach — the box the classes draw is the button's whole hit area.
|
||||
@@ -95,7 +95,7 @@
|
||||
|
||||
<div
|
||||
x-data="{
|
||||
...materialBottomSheet({{ $standard ? 'true' : 'false' }}, {{ $presets }}),
|
||||
...materialBottomSheet({{ $presets }}),
|
||||
@if ($model !== null) open: @entangle($attributes->wire('model')).live, @endif
|
||||
}"
|
||||
x-bind:data-md-open="open ? '' : null"
|
||||
|
||||
@@ -129,10 +129,7 @@
|
||||
// Read from the prop, not from `:disabled`: a spinner disables its button while the action
|
||||
// runs, and that FAB is busy, not unavailable.
|
||||
'data-md-unavailable' => $fab && $disabled ? true : null,
|
||||
'href' => $isLink ? $link : null,
|
||||
'target' => $isLink && $external ? '_blank' : null,
|
||||
'rel' => $isLink && $external ? 'noopener' : null,
|
||||
'wire:navigate' => $isLink && ! $external && ! $noWireNavigate && ! $attributes->has('wire:navigate') ? true : null,
|
||||
...\NoNameWeb\LivewireMaterial\Support\Link::attributes($isLink, $link, $external, $attributes, $noWireNavigate),
|
||||
'aria-disabled' => $inert ? 'true' : null,
|
||||
'tabindex' => $inert ? '-1' : null,
|
||||
'type' => $isLink ? null : $type,
|
||||
|
||||
@@ -33,8 +33,7 @@
|
||||
lands on the card itself. Elevation is separate from the tint — M3 gives each variant its own
|
||||
per-state box-shadow (elevated 1dp rest → 2dp hover → 1dp focus/pressed; filled and outlined
|
||||
0dp rest → 1dp hover → 0dp focus/pressed) — so it is drawn directly on `[data-md-card]`, never
|
||||
inside the shared class. Its corner does not move, because
|
||||
M3 gives a card one shape (docs/audits/m3-alignment/containment.md, C-13, C-23).
|
||||
inside the shared class. Its corner does not move, because M3 gives a card one shape.
|
||||
|
||||
`data-md-dragged` is M3's dragged card, row or not: the top of a card's elevation scale — 8dp
|
||||
elevated, 6dp filled and outlined — under the shared class's own 16% dragged tint (it already
|
||||
@@ -68,14 +67,14 @@
|
||||
@endphp
|
||||
|
||||
<div
|
||||
{{ $attributes->merge(array_filter([
|
||||
{{ $attributes->merge([
|
||||
'data-md-card' => $variant,
|
||||
'data-md-list-row' => $actionable ? true : null,
|
||||
'data-md-list-actionable' => $actionable ? true : null,
|
||||
'tabindex' => $actionable ? '0' : null,
|
||||
'role' => $role,
|
||||
'aria-label' => $actionable && filled($title) ? $title : null,
|
||||
], fn ($value): bool => $value !== null))->class([
|
||||
])->class([
|
||||
// Every card: the application sets `data-md-dragged` from script, after this renders.
|
||||
'md-state-layer',
|
||||
'md-focus-ring' => $isRow,
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
for under text on an image, and the scrim is `--md-sys-color-scrim` — black in every scheme,
|
||||
theme and contrast level — so its ink is the one absolute the theme keeps, `white`; a role
|
||||
(`inverse-on-surface`) is dark ink in a dark scheme and fails on the scrim there
|
||||
(docs/audits/m3-alignment/containment.md, C-25, deliberately kept).
|
||||
(deliberately kept).
|
||||
|
||||
A focusable `group` with `aria-roledescription="slide"`, named "n of m" by the carousel
|
||||
around it. M3 puts the tab stop on the item, not on the row: Tab reaches the first item, the
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
$paddingStart = $vertical ? 0.0 : $padding;
|
||||
$paddingEnd = $vertical || $layout === 'uncontained' || $multiAspect ? 0.0 : $padding;
|
||||
|
||||
$attributes = $attributes->merge(array_filter([
|
||||
$attributes = $attributes->merge([
|
||||
'data-md-carousel' => $layout,
|
||||
'data-md-centered' => $layout === 'hero' && $centered ? true : null,
|
||||
'data-md-padding' => (string) $paddingStart,
|
||||
@@ -153,7 +153,7 @@
|
||||
$multiAspect ? "--material-carousel-pad: {$paddingStart}px" : null,
|
||||
'--material-carousel-height: '.($cssLength($height) ?? '205px'),
|
||||
])),
|
||||
], fn ($value): bool => $value !== null));
|
||||
]);
|
||||
@endphp
|
||||
|
||||
<div x-data="materialCarousel" {{ $attributes }}>
|
||||
|
||||
@@ -29,9 +29,9 @@
|
||||
@php
|
||||
$model = $attributes->whereStartsWith('wire:model')->first();
|
||||
$id = $attributes->get('id') ?? 'check-'.substr(md5($model.'|'.$label.'|'.$attributes->get('value')), 0, 12);
|
||||
// A plain form's field is named, not bound: its errors are under its name (`files[]` → `files`, `a[b]` → `a.b`).
|
||||
$errorKey = $model ?? (filled($attributes->get('name')) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $attributes->get('name')) : null);
|
||||
$messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : [];
|
||||
$field = \NoNameWeb\LivewireMaterial\Support\Field::class;
|
||||
$errorKey = $field::key($model, $attributes->get('name'));
|
||||
$messages = $field::messages($errors ?? null, $errorKey);
|
||||
@endphp
|
||||
|
||||
<div data-md-checkbox {{ $attributes->only(['class', 'style', 'wire:key']) }}>
|
||||
|
||||
@@ -34,9 +34,7 @@
|
||||
|
||||
@php
|
||||
$key = \Illuminate\Support\Str::lower(\Illuminate\Support\Str::random(10));
|
||||
$messages = filled($errorField) && isset($errors)
|
||||
? array_values(array_unique(\Illuminate\Support\Arr::flatten([$errors->get($errorField), $errors->get($errorField.'.*')])))
|
||||
: [];
|
||||
$messages = \NoNameWeb\LivewireMaterial\Support\Field::messages($errors ?? null, filled($errorField) ? $errorField : null, wildcard: true);
|
||||
$describedBy = $messages !== [] || filled($hint) ? "material-chip-set-{$key}-hint" : null;
|
||||
@endphp
|
||||
|
||||
|
||||
@@ -127,18 +127,15 @@
|
||||
]));
|
||||
|
||||
$action = $attributes->except($outer)
|
||||
->merge(array_filter([
|
||||
->merge([
|
||||
'data-md-chip-action' => '',
|
||||
'href' => $isLink ? $link : null,
|
||||
'target' => $isLink && $external ? '_blank' : null,
|
||||
'rel' => $isLink && $external ? 'noopener' : null,
|
||||
'wire:navigate' => $isLink && ! $external && ! $noWireNavigate && ! $attributes->has('wire:navigate') ? true : null,
|
||||
...\NoNameWeb\LivewireMaterial\Support\Link::attributes($isLink, $link, $external, $attributes, $noWireNavigate),
|
||||
'aria-disabled' => $inert ? 'true' : null,
|
||||
'tabindex' => $inert ? '-1' : null,
|
||||
'type' => $actionTag === 'button' ? 'button' : null,
|
||||
'disabled' => $actionTag === 'button' && $disabled ? true : null,
|
||||
'aria-pressed' => $actionTag === 'button' && $selected !== null ? ($selected ? 'true' : 'false') : null,
|
||||
], fn ($attribute): bool => $attribute !== null));
|
||||
]);
|
||||
} else {
|
||||
$tag = match (true) {
|
||||
$checkbox => 'label',
|
||||
@@ -146,22 +143,21 @@
|
||||
default => 'button',
|
||||
};
|
||||
$control = $checkbox
|
||||
? $attributes->except($outer)->merge(array_filter([
|
||||
? $attributes->except($outer)->merge([
|
||||
'type' => 'checkbox',
|
||||
'name' => $name,
|
||||
'value' => $value,
|
||||
'checked' => $checked ? true : null,
|
||||
'disabled' => $disabled ? true : null,
|
||||
'class' => 'md-visually-hidden',
|
||||
], fn ($attribute): bool => $attribute !== null))
|
||||
])
|
||||
: null;
|
||||
|
||||
// $state's own filtering (above) drops a null 'style' here same as everywhere else, so this
|
||||
// one is not wrapped again.
|
||||
$container = ($checkbox ? $attributes->only($outer) : $attributes)
|
||||
->merge($state(array_filter([
|
||||
'href' => $isLink ? $link : null,
|
||||
'target' => $isLink && $external ? '_blank' : null,
|
||||
'rel' => $isLink && $external ? 'noopener' : null,
|
||||
'wire:navigate' => $isLink && ! $external && ! $noWireNavigate && ! $attributes->has('wire:navigate') ? true : null,
|
||||
->merge($state([
|
||||
...\NoNameWeb\LivewireMaterial\Support\Link::attributes($isLink, $link, $external, $attributes, $noWireNavigate),
|
||||
'aria-disabled' => $inert ? 'true' : null,
|
||||
'tabindex' => $inert ? '-1' : null,
|
||||
'type' => $tag === 'button' ? 'button' : null,
|
||||
@@ -170,7 +166,11 @@
|
||||
'value' => $tag === 'button' ? $value : null,
|
||||
'aria-pressed' => $kind === 'filter' && ! $checkbox ? ($selected ? 'true' : 'false') : null,
|
||||
'style' => $anchor ? "anchor-name: {$anchor}" : null,
|
||||
], fn ($attribute): bool => $attribute !== null)));
|
||||
]))
|
||||
// The chip itself is the control here (never for an input chip, whose action and remove
|
||||
// button draw their own layers): the foundation's state layer and 48px target, its
|
||||
// `:has(:focus-visible)` and `[data-md-disabled]` extras kept in chip.css alone.
|
||||
->class(['md-state-layer', 'md-touch-target']);
|
||||
}
|
||||
@endphp
|
||||
|
||||
|
||||
@@ -45,11 +45,9 @@
|
||||
|
||||
@php
|
||||
$model = $attributes->wire('model')->value() ?: null;
|
||||
// A plain form's field is named, not bound: its errors are under its name (`files[]` → `files`, `a[b]` → `a.b`).
|
||||
$errorKey = $model ?? (filled($attributes->get('name')) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $attributes->get('name')) : null);
|
||||
$messages = $errorKey !== null && isset($errors)
|
||||
? array_values(array_unique(\Illuminate\Support\Arr::flatten([$errors->get($errorKey), $errors->get($errorKey.'.*')])))
|
||||
: [];
|
||||
$field = \NoNameWeb\LivewireMaterial\Support\Field::class;
|
||||
$errorKey = $field::key($model, $attributes->get('name'));
|
||||
$messages = $field::messages($errors ?? null, $errorKey, wildcard: true);
|
||||
$choices = collect($options)->map(fn ($option): array => [
|
||||
'value' => data_get($option, $optionValue),
|
||||
'label' => (string) data_get($option, $optionLabel),
|
||||
@@ -58,10 +56,7 @@
|
||||
$single = $single || $searchable;
|
||||
|
||||
// Rendered as the bound property already says, so nothing flips once Alpine starts.
|
||||
$current = $value;
|
||||
if ($model !== null && ($component = \Livewire\Livewire::current()) !== null) {
|
||||
$current = data_get($component, $model);
|
||||
}
|
||||
$current = $field::bound($model, $value);
|
||||
$isSelected = fn ($candidate): bool => $single
|
||||
? $current !== null && (string) $current === (string) $candidate
|
||||
: in_array((string) $candidate, array_map('strval', array_filter((array) $current, 'is_scalar')), true);
|
||||
|
||||
@@ -35,12 +35,8 @@
|
||||
@php
|
||||
$model = $attributes->wire('model')->value() ?: null;
|
||||
$bound = $model !== null || count($attributes->whereStartsWith('x-model')->getAttributes()) > 0;
|
||||
$expanded = (bool) $open;
|
||||
$expanded = (bool) \NoNameWeb\LivewireMaterial\Support\Field::bound($model, $open);
|
||||
$variant = $variant === 'filled' ? 'filled' : 'plain';
|
||||
|
||||
if ($model !== null && ($component = \Livewire\Livewire::current()) !== null) {
|
||||
$expanded = (bool) data_get($component, $model);
|
||||
}
|
||||
@endphp
|
||||
|
||||
<details
|
||||
|
||||
@@ -76,11 +76,9 @@
|
||||
$mode = in_array($mode, ['docked', 'modal', 'input'], true) ? $mode : 'docked';
|
||||
$id = $attributes->get('id') ?? 'field-'.substr(md5($model.'|'.$label.'|datepicker'), 0, 12);
|
||||
$anchor = '--material-datepicker-'.preg_replace('/[^A-Za-z0-9_-]/', '-', $id);
|
||||
// A plain form's field is named, not bound: its errors are under its name (`files[]` → `files`, `a[b]` → `a.b`).
|
||||
$errorKey = $model ?? (filled($attributes->get('name')) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $attributes->get('name')) : null);
|
||||
$messages = $errorKey !== null && isset($errors)
|
||||
? array_values(array_unique(\Illuminate\Support\Arr::flatten([$errors->get($errorKey), $errors->get($errorKey.'.*')])))
|
||||
: [];
|
||||
$field = \NoNameWeb\LivewireMaterial\Support\Field::class;
|
||||
$errorKey = $field::key($model, $attributes->get('name'));
|
||||
$messages = $field::messages($errors ?? null, $errorKey, wildcard: true);
|
||||
$locale = str_replace('_', '-', app()->getLocale());
|
||||
$weekStart = (is_int($weekStart) || is_string($weekStart)) && preg_match('/^[0-6]\z/', (string) $weekStart) === 1 ? (int) $weekStart : null;
|
||||
$format = is_string($format) && preg_match('/^(dd|MM|yyyy)([.\/-])(dd|MM|yyyy)\2(dd|MM|yyyy)\z/', $format, $units) === 1 && count(array_unique([$units[1], $units[3], $units[4]])) === 3
|
||||
@@ -100,10 +98,7 @@
|
||||
};
|
||||
|
||||
// Rendered as the bound property already says, so the field does not change once Alpine starts.
|
||||
$current = $value;
|
||||
if ($model !== null && ($component = \Livewire\Livewire::current()) !== null) {
|
||||
$current = data_get($component, $model);
|
||||
}
|
||||
$current = $field::bound($model, $value);
|
||||
$current = $range
|
||||
? ['start' => $toIso(data_get($current, 'start')), 'end' => $toIso(data_get($current, 'end'))]
|
||||
: $toIso($current);
|
||||
@@ -343,45 +338,32 @@
|
||||
</div>
|
||||
|
||||
<div data-md-datepicker-nav data-md-docked x-show="presentation === 'docked'">
|
||||
<span data-md-datepicker-stepper>
|
||||
<span data-md-datepicker-arrows x-bind:data-md-concealed="view !== 'days' ? '' : null">
|
||||
<x-livewire-material::button icon="chevron_left" :tooltip="__('Previous month')" x-on:click="step(-1)" x-bind:disabled="view !== 'days' || ! canStep(-1)" />
|
||||
{{-- Months and years: the same stepper either side of a menu button, a step of
|
||||
1 or 12 apart. --}}
|
||||
@foreach ([
|
||||
['unit' => 'months', 'step' => 1, 'label' => 'monthLabel', 'previous' => __('Previous month'), 'next' => __('Next month'), 'switch' => __('Switch to selecting a month')],
|
||||
['unit' => 'years', 'step' => 12, 'label' => 'yearLabel', 'previous' => __('Previous year'), 'next' => __('Next year'), 'switch' => __('Switch to selecting a year')],
|
||||
] as $stepper)
|
||||
<span data-md-datepicker-stepper>
|
||||
<span data-md-datepicker-arrows x-bind:data-md-concealed="view !== 'days' ? '' : null">
|
||||
<x-livewire-material::button icon="chevron_left" :tooltip="$stepper['previous']" x-on:click="step(-{{ $stepper['step'] }})" x-bind:disabled="view !== 'days' || ! canStep(-{{ $stepper['step'] }})" />
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="md-state-layer md-focus-ring"
|
||||
data-md-datepicker-menu-button="{{ $stepper['unit'] }}"
|
||||
x-on:click="toggleView('{{ $stepper['unit'] }}')"
|
||||
x-bind:aria-expanded="(view === '{{ $stepper['unit'] }}').toString()"
|
||||
x-bind:aria-label="{{ $stepper['label'] }} + ', ' + @js($stepper['switch'])"
|
||||
>
|
||||
<span x-text="{{ $stepper['label'] }}"></span>
|
||||
<x-livewire-material::icon name="arrow_drop_down" size="18" data-md-datepicker-menu-arrow />
|
||||
</button>
|
||||
<span data-md-datepicker-arrows x-bind:data-md-concealed="view !== 'days' ? '' : null">
|
||||
<x-livewire-material::button icon="chevron_right" :tooltip="$stepper['next']" x-on:click="step({{ $stepper['step'] }})" x-bind:disabled="view !== 'days' || ! canStep({{ $stepper['step'] }})" />
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="md-state-layer md-focus-ring"
|
||||
data-md-datepicker-menu-button="months"
|
||||
x-on:click="toggleView('months')"
|
||||
x-bind:aria-expanded="(view === 'months').toString()"
|
||||
x-bind:aria-label="monthLabel + ', ' + @js(__('Switch to selecting a month'))"
|
||||
>
|
||||
<span x-text="monthLabel"></span>
|
||||
<x-livewire-material::icon name="arrow_drop_down" size="18" data-md-datepicker-menu-arrow />
|
||||
</button>
|
||||
<span data-md-datepicker-arrows x-bind:data-md-concealed="view !== 'days' ? '' : null">
|
||||
<x-livewire-material::button icon="chevron_right" :tooltip="__('Next month')" x-on:click="step(1)" x-bind:disabled="view !== 'days' || ! canStep(1)" />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span data-md-datepicker-stepper>
|
||||
<span data-md-datepicker-arrows x-bind:data-md-concealed="view !== 'days' ? '' : null">
|
||||
<x-livewire-material::button icon="chevron_left" :tooltip="__('Previous year')" x-on:click="step(-12)" x-bind:disabled="view !== 'days' || ! canStep(-12)" />
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="md-state-layer md-focus-ring"
|
||||
data-md-datepicker-menu-button="years"
|
||||
x-on:click="toggleView('years')"
|
||||
x-bind:aria-expanded="(view === 'years').toString()"
|
||||
x-bind:aria-label="yearLabel + ', ' + @js(__('Switch to selecting a year'))"
|
||||
>
|
||||
<span x-text="yearLabel"></span>
|
||||
<x-livewire-material::icon name="arrow_drop_down" size="18" data-md-datepicker-menu-arrow />
|
||||
</button>
|
||||
<span data-md-datepicker-arrows x-bind:data-md-concealed="view !== 'days' ? '' : null">
|
||||
<x-livewire-material::button icon="chevron_right" :tooltip="__('Next year')" x-on:click="step(12)" x-bind:disabled="view !== 'days' || ! canStep(12)" />
|
||||
</span>
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<table role="grid" aria-labelledby="{{ $id }}-month" x-show="view === 'days'" x-on:keydown="gridKey($event)" data-md-datepicker-grid>
|
||||
@@ -479,40 +461,29 @@
|
||||
|
||||
<div x-show="typing" data-md-datepicker-entry>
|
||||
<div @if ($range) data-md-range @endif data-md-datepicker-entry-fields>
|
||||
<x-livewire-material::field id="{{ $id }}-entry" :label="$range ? __('Start date') : __('Date')" :$variant x-bind:data-md-invalid="entryError !== '' ? '' : null">
|
||||
<input
|
||||
id="{{ $id }}-entry"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
placeholder=" "
|
||||
x-bind:placeholder="format.placeholder"
|
||||
x-bind:aria-invalid="entryError !== '' ? 'true' : null"
|
||||
aria-describedby="{{ $id }}-entry-support"
|
||||
x-ref="entry"
|
||||
x-model="entry"
|
||||
x-on:input="typeEntry()"
|
||||
x-on:keydown.enter.prevent="confirm()"
|
||||
data-md-field-control
|
||||
/>
|
||||
</x-livewire-material::field>
|
||||
|
||||
@if ($range)
|
||||
<x-livewire-material::field id="{{ $id }}-entry-end" :label="__('End date')" :$variant x-bind:data-md-invalid="entryError !== '' ? '' : null">
|
||||
@foreach ($range ? [
|
||||
['id' => "{$id}-entry", 'label' => __('Start date'), 'model' => 'entry', 'ref' => true],
|
||||
['id' => "{$id}-entry-end", 'label' => __('End date'), 'model' => 'entryEnd', 'ref' => false],
|
||||
] : [
|
||||
['id' => "{$id}-entry", 'label' => __('Date'), 'model' => 'entry', 'ref' => true],
|
||||
] as $entry)
|
||||
<x-livewire-material::field id="{{ $entry['id'] }}" :label="$entry['label']" :$variant x-bind:data-md-invalid="entryError !== '' ? '' : null">
|
||||
<input
|
||||
id="{{ $id }}-entry-end"
|
||||
id="{{ $entry['id'] }}"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
placeholder=" "
|
||||
x-bind:placeholder="format.placeholder"
|
||||
x-bind:aria-invalid="entryError !== '' ? 'true' : null"
|
||||
aria-describedby="{{ $id }}-entry-support"
|
||||
x-model="entryEnd"
|
||||
@if ($entry['ref']) x-ref="entry" @endif
|
||||
x-model="{{ $entry['model'] }}"
|
||||
x-on:input="typeEntry()"
|
||||
x-on:keydown.enter.prevent="confirm()"
|
||||
data-md-field-control
|
||||
/>
|
||||
</x-livewire-material::field>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div id="{{ $id }}-entry-support" data-md-datepicker-support>
|
||||
|
||||
@@ -32,11 +32,11 @@
|
||||
@endphp
|
||||
|
||||
@if (filled($text) && ! $vertical)
|
||||
<div {{ $attributes->merge(array_filter([
|
||||
<div {{ $attributes->merge([
|
||||
'data-md-divider-heading' => true,
|
||||
'data-md-inset' => $inset ? true : null,
|
||||
'data-md-middle' => $middle ? true : null,
|
||||
], fn ($value): bool => $value !== null)) }}>
|
||||
]) }}>
|
||||
<span data-md-divider-text>{{ $text }}</span>
|
||||
<div
|
||||
data-md-divider
|
||||
@@ -49,9 +49,9 @@
|
||||
data-md-divider
|
||||
data-md-orientation="{{ $orientation }}"
|
||||
@if ($decorative) aria-hidden="true" @else role="separator" aria-orientation="{{ $orientation }}" @endif
|
||||
{{ $attributes->merge(array_filter([
|
||||
{{ $attributes->merge([
|
||||
'data-md-inset' => $inset && ! $vertical ? true : null,
|
||||
'data-md-middle' => $middle ? true : null,
|
||||
], fn ($value): bool => $value !== null)) }}
|
||||
]) }}
|
||||
></div>
|
||||
@endif
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
M3 **requires** a close affordance on a side sheet — without one nobody can predict the
|
||||
sheet's open/close flow or tell whether it is transient or permanent
|
||||
(docs/reference/m3/components-actions-communication-containment.md § Side sheets →
|
||||
Accessibility, C-04) — so `with-close-button` is on by default, and
|
||||
Accessibility) — so `with-close-button` is on by default, and
|
||||
`:with-close-button="false"` is ignored where nothing else closes the sheet: Escape off, the
|
||||
scrim off, or `standard`, which has neither from `expanded`.
|
||||
|
||||
@@ -88,11 +88,7 @@
|
||||
// A standard sheet bound to Livewire is drawn open from the first paint when its property is
|
||||
// (truthy as Alpine reads it), so it stands in the row before the script starts rather than
|
||||
// growing into it; drawer.css keeps any other standard sheet out of the row until then.
|
||||
$startsOpen = false;
|
||||
|
||||
if ($standard && $model !== null && ($component = \Livewire\Livewire::current()) !== null) {
|
||||
$startsOpen = ! in_array(data_get($component, $model), [null, false, 0, 0.0, ''], true);
|
||||
}
|
||||
$startsOpen = $standard && ! in_array(\NoNameWeb\LivewireMaterial\Support\Field::bound($model, false), [null, false, 0, 0.0, ''], true);
|
||||
|
||||
// M3 requires a close affordance; the prop can only ever add one, never take away the last
|
||||
// way out of the sheet. A standard sheet keeps no scrim and no trap from `expanded`, and
|
||||
@@ -231,7 +227,7 @@
|
||||
@isset($actions)
|
||||
{{-- The side-sheet specs table: bottom actions 72dp tall, 16dp above them, 24dp below
|
||||
(the sheet's own padding), aligned **left** — a dialog's are trailing-aligned, and
|
||||
M3 specifies the two differently (C-10). --}}
|
||||
M3 specifies the two differently. --}}
|
||||
<div data-md-drawer-actions>{{ $actions }}</div>
|
||||
@endisset
|
||||
</aside>
|
||||
|
||||
@@ -22,17 +22,14 @@
|
||||
$tag = $isLink ? 'a' : 'button';
|
||||
$color = in_array($color, ['primary', 'secondary', 'tertiary'], true) ? $color : 'primary';
|
||||
|
||||
$attributes = $attributes->merge(array_filter([
|
||||
$attributes = $attributes->merge([
|
||||
'data-md-fab-menu-item' => true,
|
||||
'data-md-color' => $color,
|
||||
'role' => 'menuitem',
|
||||
'tabindex' => '-1',
|
||||
'type' => $isLink ? null : 'button',
|
||||
'href' => $isLink ? $link : null,
|
||||
'target' => $isLink && $external ? '_blank' : null,
|
||||
'rel' => $isLink && $external ? 'noopener' : null,
|
||||
'wire:navigate' => $isLink && ! $external && ! $attributes->has('wire:navigate') ? true : null,
|
||||
], fn ($value): bool => $value !== null))->class(['md-state-layer', 'md-focus-ring']);
|
||||
...\NoNameWeb\LivewireMaterial\Support\Link::attributes($isLink, $link, $external, $attributes),
|
||||
])->class(['md-state-layer', 'md-focus-ring']);
|
||||
@endphp
|
||||
|
||||
<{{ $tag }} {{ $attributes }}>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
Two to six items. The FAB (`icon`, `add` by default, in `color`'s container) turns into a
|
||||
round close button in the colour itself while the list is open above it, end-aligned; the
|
||||
list is a `popover="auto"` menu with the menu keyboard of `<x-menu>`. `label` names the FAB
|
||||
and the menu for screen readers, and defaults to "Toggle menu" (ACT-21) — the icons inside
|
||||
and the menu for screen readers, and defaults to "Toggle menu" — the icons inside
|
||||
the button are aria-hidden, so without it the trigger has no name at all. Give the items the
|
||||
same `color`. Like `<x-menu>`'s, the list is keyed for Livewire, so it stays open through a
|
||||
render of the component around it.
|
||||
@@ -20,8 +20,8 @@
|
||||
|
||||
@media (width >= 1200px) { .my-fab-menu { inset-inline-end: 24px; bottom: 24px } }
|
||||
|
||||
A window too short for the list scrolls it, behind the close button, which stays fixed
|
||||
(ACT-32); the items rise into place as it opens and sink back as it closes (ACT-33).
|
||||
A window too short for the list scrolls it, behind the close button, which stays fixed;
|
||||
the items rise into place as it opens and sink back as it closes.
|
||||
|
||||
FabMenuBaselineTokens (androidx Compose Material 3, Apache-2.0): 56px close button, full
|
||||
corner when open, 20px close icon, 8px to the first item, 56px items 4px apart.
|
||||
|
||||
@@ -64,10 +64,7 @@
|
||||
'data-md-variant' => $variant,
|
||||
'data-md-extended' => $extended ? true : null,
|
||||
'data-md-collapse-on-scroll' => $collapsing ? true : null,
|
||||
'href' => $isLink ? $link : null,
|
||||
'target' => $isLink && $external ? '_blank' : null,
|
||||
'rel' => $isLink && $external ? 'noopener' : null,
|
||||
'wire:navigate' => $isLink && ! $external && ! $attributes->has('wire:navigate') ? true : null,
|
||||
...\NoNameWeb\LivewireMaterial\Support\Link::attributes($isLink, $link, $external, $attributes),
|
||||
'type' => $isLink ? null : $type,
|
||||
'x-data' => $collapsing ? 'materialFab' : null,
|
||||
'x-bind:data-md-collapsed' => $collapsing ? "collapsed ? '' : null" : null,
|
||||
|
||||
@@ -39,11 +39,11 @@
|
||||
$element = $layout::element($as);
|
||||
$minItem = $layout::length($minItem) ?? '240px';
|
||||
|
||||
$attributes = $attributes->merge(array_filter([
|
||||
$attributes = $attributes->merge([
|
||||
'data-md-feed' => true,
|
||||
'data-md-gap' => $layout::spacing($gap),
|
||||
'style' => "--md-min-item: {$minItem};",
|
||||
] + $layout::visibility($hideBelow, $hideFrom), fn ($value): bool => $value !== null));
|
||||
] + $layout::visibility($hideBelow, $hideFrom));
|
||||
@endphp
|
||||
|
||||
<{{ $element }} {{ $attributes }}>{{ $slot }}</{{ $element }}>
|
||||
|
||||
@@ -23,11 +23,9 @@
|
||||
@php
|
||||
$model = $attributes->whereStartsWith('wire:model')->first();
|
||||
$id = $attributes->get('id') ?? 'field-'.substr(md5('file|'.$model.'|'.$label), 0, 12);
|
||||
// A plain form's field is named, not bound: its errors are under its name (`files[]` → `files`, `a[b]` → `a.b`).
|
||||
$errorKey = $model ?? (filled($attributes->get('name')) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $attributes->get('name')) : null);
|
||||
$messages = $errorKey !== null && isset($errors)
|
||||
? array_values(array_unique(\Illuminate\Support\Arr::flatten([$errors->get($errorKey), $errors->get($errorKey.'.*')])))
|
||||
: [];
|
||||
$field = \NoNameWeb\LivewireMaterial\Support\Field::class;
|
||||
$errorKey = $field::key($model, $attributes->get('name'));
|
||||
$messages = $field::messages($errors ?? null, $errorKey, wildcard: true);
|
||||
@endphp
|
||||
|
||||
<x-livewire-material::field :$id :$label :$hint :$messages :$variant :$full floated :class="$attributes->get('class')" :style="$attributes->get('style')" data-md-file>
|
||||
|
||||
@@ -37,13 +37,13 @@
|
||||
|
||||
$style = $layout::columnStyle($layout::columns($columns)).($minItem !== null ? " --md-min-item: {$minItem};" : '');
|
||||
|
||||
$attributes = $attributes->merge(array_filter([
|
||||
$attributes = $attributes->merge([
|
||||
'data-md-grid' => true,
|
||||
'data-md-gap' => $layout::spacing($gap),
|
||||
'data-md-min-item' => $minItem !== null ? true : null,
|
||||
'data-md-columns' => $minItem !== null && $columns !== null ? true : null,
|
||||
'style' => $style,
|
||||
] + $layout::visibility($hideBelow, $hideFrom), fn ($value): bool => $value !== null));
|
||||
] + $layout::visibility($hideBelow, $hideFrom));
|
||||
@endphp
|
||||
|
||||
<{{ $element }} {{ $attributes }}>{{ $slot }}</{{ $element }}>
|
||||
|
||||
@@ -49,9 +49,9 @@
|
||||
@php
|
||||
$model = $attributes->whereStartsWith('wire:model')->first();
|
||||
$name ??= $model;
|
||||
// A plain form's field is named, not bound: its errors are under its name (`files[]` → `files`, `a[b]` → `a.b`).
|
||||
$errorKey = $model ?? (filled($attributes->get('name')) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $attributes->get('name')) : null);
|
||||
$messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : [];
|
||||
$field = \NoNameWeb\LivewireMaterial\Support\Field::class;
|
||||
$errorKey = $field::key($model, $attributes->get('name'));
|
||||
$messages = $field::messages($errors ?? null, $errorKey);
|
||||
$size = in_array($size, ['xs', 'sm', 'md', 'lg', 'xl'], true) ? $size : 'sm';
|
||||
$variant = in_array($variant, ['tonal', 'filled', 'outlined'], true) ? $variant : 'tonal';
|
||||
// M3's "Default shape | Round, square": a square group's ends take the corner its inner edges
|
||||
@@ -63,14 +63,14 @@
|
||||
// Below `medium` a segment draws under 48px, so it needs the foundation's touch target too.
|
||||
$segmentNeedsTouchTarget = in_array($size, ['xs', 'sm'], true);
|
||||
|
||||
$root = $attributes->only(['class', 'style', 'wire:key'])->merge(array_filter([
|
||||
$root = $attributes->only(['class', 'style', 'wire:key'])->merge([
|
||||
'data-md-group' => true,
|
||||
'data-md-size' => $size,
|
||||
'data-md-variant' => $variant,
|
||||
'data-md-shape' => $shape,
|
||||
'data-md-multiple' => $multiple ? true : null,
|
||||
'data-md-inline' => $inline ? true : null,
|
||||
], fn ($value): bool => $value !== null));
|
||||
]);
|
||||
@endphp
|
||||
|
||||
<fieldset {{ $root }}>
|
||||
@@ -82,6 +82,7 @@
|
||||
@foreach ($options as $option)
|
||||
<label data-md-group-segment class="md-state-layer @if ($segmentNeedsTouchTarget) md-touch-target @endif">
|
||||
<input
|
||||
class="md-visually-hidden"
|
||||
{{ $attributes->whereStartsWith(['wire:model', 'x-model']) }}
|
||||
type="{{ $multiple ? 'checkbox' : 'radio' }}"
|
||||
name="{{ $multiple ? $name.'[]' : $name }}"
|
||||
|
||||
@@ -38,9 +38,9 @@
|
||||
$model = $attributes->whereStartsWith('wire:model')->first();
|
||||
$placeholder = filled($attributes->get('placeholder')) ? $attributes->get('placeholder') : ' ';
|
||||
$id = $attributes->get('id') ?? 'field-'.substr(md5($model.'|'.$label.'|'.$placeholder), 0, 12);
|
||||
// A plain form's field is named, not bound: its errors are under its name (`files[]` → `files`, `a[b]` → `a.b`).
|
||||
$errorKey = $model ?? (filled($attributes->get('name')) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $attributes->get('name')) : null);
|
||||
$messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : [];
|
||||
$field = \NoNameWeb\LivewireMaterial\Support\Field::class;
|
||||
$errorKey = $field::key($model, $attributes->get('name'));
|
||||
$messages = $field::messages($errors ?? null, $errorKey);
|
||||
// M3's counter needs a maximum to count against: without `maxlength` there is nothing to show.
|
||||
$max = $counter ? $attributes->get('maxlength') : null;
|
||||
$fieldIconSize = ['sm' => 20, 'xs' => 16][$size] ?? 24;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user