Compare commits
457
Commits
db179e6b4e
...
main
+4
-1
@@ -2,18 +2,21 @@
|
||||
|
||||
*.woff2 binary
|
||||
|
||||
# Development only: kept out of the archives Composer installs.
|
||||
# Development only: kept out of the archives Composer installs, and out of the copy a path
|
||||
# repository with "symlink": false makes (Composer mirrors it through these rules).
|
||||
/.editorconfig export-ignore
|
||||
/.gitattributes export-ignore
|
||||
/.github export-ignore
|
||||
/.gitignore export-ignore
|
||||
/bin export-ignore
|
||||
/docs export-ignore
|
||||
/node_modules export-ignore
|
||||
/package-lock.json export-ignore
|
||||
/package.json export-ignore
|
||||
/phpunit.xml export-ignore
|
||||
/pint.json export-ignore
|
||||
/testbench.yaml export-ignore
|
||||
/tests export-ignore
|
||||
/vendor export-ignore
|
||||
/vite.config.js export-ignore
|
||||
/workbench export-ignore
|
||||
|
||||
+10
-25
@@ -11,28 +11,6 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
|
||||
with:
|
||||
php-version: '8.5'
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --no-progress --prefer-dist
|
||||
|
||||
- name: Check formatting
|
||||
run: vendor/bin/pint --test
|
||||
|
||||
feature:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -54,19 +32,26 @@ jobs:
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
# material:scheme's tests run the bundled colour script through Node.
|
||||
# material:scheme's tests run the bundled colour script through Node, and the prebuilt
|
||||
# stylesheets' staleness test rebuilds them with the installed Vite.
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --no-progress --prefer-dist
|
||||
run: |
|
||||
composer install --no-interaction --no-progress --prefer-dist
|
||||
npm ci
|
||||
|
||||
- name: Check formatting
|
||||
if: matrix.php == '8.5'
|
||||
run: vendor/bin/pint --test
|
||||
|
||||
- name: Run feature tests
|
||||
run: vendor/bin/pest --testsuite=Feature
|
||||
|
||||
# One job per engine: the floor is Chrome 125, Firefox 147 and Safari 18.4, and the
|
||||
# One job per engine: the floor is Chrome 125, Firefox 151 and Safari 18.4, and the
|
||||
# components lean on platform features (dialog, popover, anchor positioning) that
|
||||
# differ between them. Pest calls the engines chrome, firefox and safari; Playwright
|
||||
# installs them as chromium, firefox and webkit.
|
||||
|
||||
@@ -10,3 +10,4 @@
|
||||
|
||||
# Planning notes stay local
|
||||
/docs/plans
|
||||
.browser-lock
|
||||
|
||||
@@ -5,6 +5,8 @@ This package bundles or derives from the following works.
|
||||
|
||||
Material Symbols (resources/svg/symbols)
|
||||
From google/material-design-icons, commit recorded in resources/svg/symbols/SOURCE.
|
||||
Rounded, weight 400, grade 0, in two cuts of the same source: optical size 24
|
||||
(outlined, filled) and optical size 20 (outlined-20, filled-20).
|
||||
Copyright Google LLC. Apache License 2.0.
|
||||
|
||||
M3 Expressive shapes (resources/svg/shapes)
|
||||
@@ -50,4 +52,18 @@ Spring motion constants (resources/css/tokens/motion.css)
|
||||
From androidx Compose Material 3, tokens/ExpressiveMotionTokens.kt.
|
||||
Copyright The Android Open Source Project. Apache License 2.0.
|
||||
|
||||
CSS reset (resources/css/foundation/reset.css)
|
||||
Derived rule for rule from Tailwind CSS 4's preflight, after modern-normalize.
|
||||
Copyright Tailwind Labs. MIT License.
|
||||
|
||||
Material Design 3 documentation (resources/boost/guidelines/material-3.blade.php,
|
||||
resources/boost/skills/material-3-design/SKILL.md, docs/reference/m3)
|
||||
Rules, tables and wording condensed from https://m3.material.io (Foundations, Styles,
|
||||
Components), which Google publishes under the Creative Commons Attribution 4.0 License
|
||||
except as otherwise noted; the numeric token values from androidx Compose Material 3's
|
||||
token files.
|
||||
Copyright Google LLC (CC BY 4.0, https://creativecommons.org/licenses/by/4.0/);
|
||||
Copyright The Android Open Source Project (Apache License 2.0).
|
||||
|
||||
A copy of the Apache License 2.0 is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
A copy of the MIT License is available at https://opensource.org/licenses/MIT.
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
# Livewire Material
|
||||
|
||||
Material 3 Expressive components for Laravel and Livewire, built on Tailwind CSS 4.
|
||||
Material 3 Expressive components for Laravel and Livewire, in plain CSS.
|
||||
|
||||
- Anonymous Blade components for the current M3 Expressive catalogue: buttons and FABs, menus, chips, text fields, selection controls, sliders, pickers, dialogs and sheets, lists, cards, carousels, progress and loading indicators, snackbars, tabs, app bars, toolbars, navigation bars and rails, an adaptive app shell, data tables and pagination.
|
||||
- Anonymous Blade components for the current M3 Expressive catalogue: buttons and FABs, menus, chips, text fields, selection controls, sliders, pickers, dialogs and sheets, lists, cards, carousels, progress and loading indicators, snackbars, tabs, app bars, toolbars, navigation bars and rails, an adaptive scaffold, the layout components (panes, list-detail, supporting pane, feed), data tables and pagination.
|
||||
- A colour scheme generated from one seed colour with Google's colour science (`php artisan material:scheme`), light and dark, and a theme that is chosen before the first paint.
|
||||
- The full Material Symbols Rounded set and the M3 Expressive shapes, drawn inline without an icon package.
|
||||
- Error pages and a Markdown mail theme in the same scheme.
|
||||
- A showcase of every component in the application's own scheme, a design guard for tests, and Laravel Boost guidelines and a skill for AI agents.
|
||||
- A showcase of every component in the application's own scheme, a design guard for tests, and for AI agents two Laravel Boost guidelines and two skills: the library's own, and Material 3's rules and tables beside its utilities.
|
||||
|
||||
No JavaScript libraries beyond the Alpine that ships with Livewire. Browsers: Chrome 125+, Firefox 147+, Safari 18.4+.
|
||||
No JavaScript libraries beyond the Alpine that ships with Livewire. Browsers: Chrome 125+, Firefox 151+, Safari 18.4+.
|
||||
|
||||
## Requirements
|
||||
|
||||
PHP 8.4+, Laravel 13, Livewire 4, Tailwind CSS 4 with Vite, and Node (for `material:scheme`).
|
||||
PHP 8.4+, Laravel 13, Livewire 4, Vite, and Node (for `material:scheme`).
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -27,14 +27,13 @@ composer require nonameweb/livewire-material
|
||||
|
||||
The application's build imports from `vendor/`, so Composer packages must be installed before `npm run build` — in a Dockerfile, copy `composer.json`, run `composer install`, then build the assets.
|
||||
|
||||
The package's stylesheets are plain CSS, no build step of its own. `all.css` brings the foundation and every component; an application can instead import `foundation.css` first and then the stylesheet of each component its views render (`resources/css/components/button.css`, `resources/css/layout/scaffold.css`, …), since each imports the stylesheets of the components it draws and Vite keeps a file several of them import once. The error pages need no import: they take the foundation, scheme and font from the application's build and inline their own layout's rules beside it.
|
||||
|
||||
```css
|
||||
/* resources/css/app.css */
|
||||
@import 'tailwindcss';
|
||||
@import '../../vendor/nonameweb/livewire-material/resources/css/material.css';
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
@import '../../vendor/nonameweb/livewire-material/resources/css/all.css';
|
||||
@import './material-scheme.css';
|
||||
|
||||
@source '../../vendor/nonameweb/livewire-material/resources/views';
|
||||
@source '../../vendor/nonameweb/livewire-material/src';
|
||||
```
|
||||
|
||||
```js
|
||||
@@ -44,6 +43,8 @@ import '../../vendor/nonameweb/livewire-material/resources/js/material.js'
|
||||
|
||||
Do not install Alpine separately; Livewire provides it.
|
||||
|
||||
An application writes no utility layer of its own: layout components (`<x-scaffold>`, `<x-pane>`, `<x-stack>`, `<x-row>`, `<x-grid>`, the canonical layouts) take M3's spacing tokens and breakpoints as props, a small set of `md-type-*` and `md-ink-*` classes covers text on plain elements, and `--md-sys-*` custom properties serve the rest of an application's own stylesheet. Breakpoints are M3's window size classes and only those — compact below `medium` (600px), then `expanded` (840px), `large` (1200px) and `extra-large` (1600px) — written as literal range media queries (`@media (width >= 840px)`) in the package's CSS; a script asks `resources/js/breakpoints.js` (`from()` / `upTo()`) for the same numbers rather than writing its own query.
|
||||
|
||||
### Layout
|
||||
|
||||
The theme script goes in `<head>`, before `@vite`, so the page paints in the visitor's theme:
|
||||
@@ -57,7 +58,7 @@ The theme script goes in `<head>`, before `@vite`, so the page paints in the vis
|
||||
<x-theme-script />
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="bg-surface font-sans text-on-surface antialiased">
|
||||
<body>
|
||||
{{ $slot }}
|
||||
<x-toast />
|
||||
</body>
|
||||
@@ -72,11 +73,13 @@ Generate the scheme from a seed colour. It writes `resources/css/material-scheme
|
||||
php artisan material:scheme "#4f46e5" --variant=tonal-spot
|
||||
```
|
||||
|
||||
Variants: `tonal-spot`, `vibrant`, `expressive`, `neutral`, `fidelity`, `content`, `monochrome`, `rainbow`, `fruit-salad`. `--contrast` runs from -1 to 1; `--success`, `--warning` and `--info` seed the state colours. Regenerate instead of editing the file.
|
||||
Variants: `tonal-spot`, `vibrant`, `expressive`, `neutral`, `fidelity`, `content`, `monochrome`, `rainbow`, `fruit-salad`. `--spec` is the colour spec: `2025` (M3 Expressive, the default) or `2021` (M3 as it first shipped, for a palette generated before Expressive). `--success`, `--warning` and `--info` seed the state colours, which are built exactly as M3 builds `error`; `--harmonize` pulls them towards the seed. The stylesheet's header records the command that regenerates it; regenerate instead of editing the file.
|
||||
|
||||
Every scheme is written at M3's three contrast levels — standard, medium (3:1) and high (7:1), for light and dark — keyed on `<html data-contrast>`, which the head script sets before the first paint from the visitor's choice or the operating system's. `--contrast` sets the standard level alone (below 0.5); `<x-theme-toggle mode="contrast" />` lets someone choose.
|
||||
|
||||
#### Colour profiles
|
||||
|
||||
To let an installation switch between several schemes, list them as `profiles` in the config (name ⇒ `label`, `seed`, `variant`) and run `php artisan material:scheme` without a seed: every profile lands in the same stylesheet under `<html data-scheme>`. Tell the package which one is active — `Scheme::resolveProfileUsing(fn () => Setting::get('color_profile'))` in a service provider — and the head script, mails and error pages follow it. `<x-scheme-picker wire:model="colorProfile" />` lets someone choose, previewing each profile on the page.
|
||||
To let an installation switch between several schemes, list them as `profiles` in the config (name ⇒ `label`, `seed`, `variant`, and optionally `contrast`, `harmonize`, `spec`, `success`, `warning`, `info`, which otherwise come from the command's options) and run `php artisan material:scheme` without a seed: every profile lands in the same stylesheet under `<html data-scheme>`. Tell the package which one is active — `Scheme::resolveProfileUsing(fn () => Setting::get('color_profile'))` in a service provider — and the head script, mails and error pages follow it. `<x-scheme-picker wire:model="colorProfile" />` lets someone choose, previewing each profile on the page.
|
||||
|
||||
### Configuration
|
||||
|
||||
@@ -86,9 +89,12 @@ php artisan vendor:publish --tag=livewire-material-config
|
||||
|
||||
- `prefix` — components are `<x-button>`, `<x-card>`… Set `'m'` when a name clashes with the application's own components, and they become `<x-m::button>`. `<x-livewire-material::button>` always works.
|
||||
- `theme.default` (`light`, `dark` or `system`), `theme.storage_key`, `theme.legacy_keys` (an earlier toggle's localStorage keys, adopted once).
|
||||
- `theme.contrast.default` (`system`, `standard`, `medium` or `high`) and `theme.contrast.storage_key` — M3's contrast level, resolved before the first paint into `<html data-contrast>` and followed on the operating system while `system`.
|
||||
- `theme.meta` — keep `<meta name="theme-color">` (an installed web app's or a mobile browser's bar) on the resolved theme's `surface` and the active colour profile, before the first paint and after every change, `wire:navigate` included; one is added when the page has none (default `false`).
|
||||
- `motion.scheme` — M3's motion scheme: `expressive` (default) or `standard`, the restrained springs, written to `<html data-motion>`.
|
||||
- `profiles`, `profile` — colour profiles and the default one (see Colour profiles).
|
||||
- `fields.variant` — text fields `outlined` (default) or `filled`.
|
||||
- `pagination` — draw Laravel's and Livewire's paginators in M3 (default `true`).
|
||||
- `pagination` — draw Laravel's and Livewire's paginators in M3 (default `true`); the provider also sets `livewire.pagination_theme` to `material` whenever it still reads as Livewire's own default (`tailwind`), unless an application's own theme, or a component's `$paginationTheme` property or `paginationView()` method, already chose one.
|
||||
- `showcase.enabled`, `showcase.path`, `showcase.middleware`, `showcase.vite`.
|
||||
- `node` — the Node binary for `material:scheme`.
|
||||
|
||||
@@ -129,7 +135,7 @@ Every component, prop and slot is documented in the Boost skill (`resources/boos
|
||||
|
||||
## Showcase
|
||||
|
||||
While the application runs locally (or with `MATERIAL_SHOWCASE=true`), `/material` shows every token and component, in every variant, in the application's own scheme and theme: an overview, and a page per section behind a navigation rail (the package's own app shell), with a search over every section, example and component (press `/`).
|
||||
While the application runs locally (or with `MATERIAL_SHOWCASE=true`), `/material` shows every token and component, in every variant, in the application's own scheme and theme: an overview, and a page per section behind a navigation rail (the package's own scaffold), with a search over every section, example and component (press `/`).
|
||||
|
||||
## Testing the design
|
||||
|
||||
@@ -137,17 +143,20 @@ While the application runs locally (or with `MATERIAL_SHOWCASE=true`), `/materia
|
||||
use NoNameWeb\LivewireMaterial\Testing\DesignGuard;
|
||||
|
||||
it('uses only what compiles', function () {
|
||||
expect(DesignGuard::scan([resource_path('views'), resource_path('js'), app_path()])
|
||||
expect(DesignGuard::scan([resource_path('views'), resource_path('js'), resource_path('css'), app_path()])
|
||||
->missingStylesheets(resource_path('css/app.css'))
|
||||
->forbidColours(['tertiary'])
|
||||
->violations())->toBe([]);
|
||||
});
|
||||
```
|
||||
|
||||
The guard fails on maryUI tags, daisyUI classes, colours the theme does not declare, unknown Material Symbol names and Blade directives written inside component tags.
|
||||
The guard reads every path it is given and fails, with `path:line`, on a class written in a view or PHP file that no stylesheet declares — not the application's own CSS, not the package's `md-*` — whatever it came from: a utility of a framework that isn't in the stack, a typo, or a class whose rule was deleted. Without a `.css` file among the scanned paths or a `missingStylesheets()` entry to supply the application's import graph, it has no declared class to compare against and reports every one of them. In the `.css` files it is given (`material-scheme.css` skipped) it fails on a literal colour, radius, shadow, font, easing or duration, and on a media query off M3's 600/840/1200/1600px; a `var()`, or a `calc()`/`min()`/`max()`/`clamp()` built on one, is fine. It also fails on unknown Material Symbol names and Blade directives written inside component tags. Markdown mail components (under `mail.markdown.paths`, `resources/views/vendor/mail` by default) are the mail theme's to style: their classes are not read by the class check, and a mail theme stylesheet there is neither checked for literals nor a source of declared classes.
|
||||
|
||||
`missingStylesheets($cssEntry)` checks the entry's relative `@import` graph, followed through every package file's own imports, against the package tags the views render (unprefixed, under the configured prefix, or `<x-livewire-material::…>`), `->links()` and the rows they write by hand (`data-md-list-row` on anything but `<x-card>` needs `components/list-item.css`), and names each missing `@import` line once; a tag the application shadows with its own component is reported instead. It reads imports only, so leave `resource_path('css')` out of `scan()` while the stylesheets still hold literals — as long as `missingStylesheets()` is given an entry, its import graph still supplies the declared classes family (i) needs. `forbidColours([...])` names roles the application leaves out, and fails wherever one (with its `on-` and container roles) is still written: `var(--md-sys-color-…)` in CSS or an inline `style`, its `md-ink-*` class, or a component's `color`/`tone` prop. `forbid($pattern, $reason)` adds any pattern of your own, line by line.
|
||||
|
||||
## AI agents
|
||||
|
||||
With [Laravel Boost](https://github.com/laravel/boost), `php artisan boost:install` (or `boost:update --discover`) picks up the package's guideline and the `livewire-material-development` skill.
|
||||
With [Laravel Boost](https://github.com/laravel/boost), `php artisan boost:install` (or `boost:update --discover`) picks up the package's two guidelines — the library's own, and `material-3`, a page of M3's rules an agent reads in every session — and two skills: `livewire-material-development` (every component, prop, slot and trap) and `material-3-design` (M3's colour roles, surfaces, elevation, shape, type, motion, states, window size classes and accessibility, with the library's utility beside each M3 name and Google's source page for each chapter).
|
||||
|
||||
## Developing the package
|
||||
|
||||
|
||||
+363
@@ -0,0 +1,363 @@
|
||||
# Upgrading
|
||||
|
||||
## From 2.1.0 to 2.2.0
|
||||
|
||||
- **Run `php artisan view:clear` after upgrading.** The components are no longer registered under a
|
||||
view namespace named after a hash of their folder, which only views compiled before 1.0.1 still
|
||||
named; such a view shows the tag as text until it is compiled again.
|
||||
- **`DesignGuard`** no longer reports the 1.x package's own names (`text-meta`, `focus-ring`,
|
||||
`rounded-corner-lg`, `shadow-elevation-2`, `type-body-md`, `ease-spatial-fast`) with their 2.0.0
|
||||
replacement. A colour utility on a 1.x ink is still reported, as a colour of the application's own
|
||||
theme; the rest compile to nothing and pass unnoticed.
|
||||
- **`<x-slider>`** redraws when a script sets its input's `value`, as before, but no longer when it
|
||||
sets `valueAsNumber`; write `value` instead.
|
||||
- **The showcase's stylesheet** is served from `assets/css/showcase.{hash}.css`, prebuilt in
|
||||
`resources/dist/`, and the error page inlines prebuilt bundles too. `Stylesheets::bundle()` and
|
||||
`Stylesheets::resetCache()` are gone; nothing outside the package called them.
|
||||
- **`$store.rail.toggle()`** flips what the first rail on the page draws, as its menu button does.
|
||||
With nothing stored, `<x-scaffold>`'s rail is drawn collapsed from 840 to 1199px whatever
|
||||
`rail.default` says, and `toggle()` collapsed it again, so the first press of an application's
|
||||
shortcut changed nothing; it now expands the rail. Below 840px, where there is no room to expand
|
||||
it in the layout, `toggle()` opens and closes the modal rail (`show()`, `hide()`) instead of
|
||||
changing a choice nothing draws there. A shortcut that pressed the rail's menu button to get this
|
||||
can call `$store.rail.toggle()` again.
|
||||
- **`DesignGuard`** no longer reads a Markdown mail component's classes as Tailwind: a view under
|
||||
`mail.markdown.paths` (`resources/views/vendor/mail` by default) is drawn by the mail theme, whose
|
||||
`table`, `button` and `panel` share Tailwind's names, so only its icon names, the directives in
|
||||
its component tags and the application's own bans are checked there. A mail theme stylesheet
|
||||
under that path is no longer checked for literal values, and its classes no longer exempt the
|
||||
same names in the application's other views. A filter an application wrote around such a
|
||||
finding can go.
|
||||
- **`DesignGuard`** counts a row written by hand. `data-md-list-row` on an `<li>`, a `<div>`, a
|
||||
`<tr>` or `<x-row>` takes its hover, focus and press state layer and its selected fill from
|
||||
`components/list-item.css`, but `missingStylesheets()` and `unusedStylesheets()` read only tags
|
||||
and `->links()`: an application had the stylesheet through `table.css` or `<x-list-item>`, lost
|
||||
it without a finding when the last of those left its views, and an `@import` of `list-item.css`
|
||||
for its own rows was reported unused. A view of the application that writes `data-md-list-row`
|
||||
on anything but `<x-card>`, whose row card.css draws, now needs `list-item.css`; a selector such
|
||||
as `[data-md-list-row]` and the package's own views do not count. A test that expects no findings
|
||||
may name the `@import` to add, and a filter written around the unused report can go.
|
||||
- **A selected row written by hand** — `data-md-list-row` with `data-md-selected` on an `<li>`, a
|
||||
`<div>` or a `<tr>`, not `<x-list-item>` or `<x-card>` — is drawn in `secondary-container` with
|
||||
`on-secondary-container` ink again, as 1.x's `data-list-row` with `data-selected` was; 2.0.0
|
||||
dropped the fill without saying so. Hover, focus and press now tint the fill instead of hiding
|
||||
it. In `<x-table>` a plain row takes `data-md-selected` too, beside `aria-selected="true"`. A
|
||||
card still takes no selected fill.
|
||||
- **67 Material Symbols** in each cut (`auto_awesome`, `insights`, `tips_and_updates`,
|
||||
`battery_50` …) are drawn whole at every size. Google publishes them in 24 units (20 in the 20
|
||||
cut) with a width and height but no viewBox, and the package dropped the size, so they fitted
|
||||
only at exactly 24px and were cut off or misplaced at any other — the 20 cut every small button
|
||||
draws from included. Nothing to change.
|
||||
- **A focused text field keeps its focus edge under the pointer.** The hover rule outweighed focus,
|
||||
so a focused field the pointer rested on drew a 2px `on-surface` edge instead of `primary`, and
|
||||
a field in error `on-error-container` instead of `error` — in the filled variant too, and on a
|
||||
customizable `<x-select>` while its menu is open. An open select in error now draws `error`
|
||||
rather than `primary`. A test that read the hover colour off a focused field reads `primary`
|
||||
now.
|
||||
- **Escape closes one layer.** A dialog, menu, customizable select or searchable choice opened
|
||||
over or inside a modal `<x-drawer>`, `<x-bottom-sheet>` or modal navigation rail — or a sheet
|
||||
opened from a sheet, or inside `<x-modal>` — closed the layer under it on the same Escape, since
|
||||
each sheet closed on any Escape the window heard. Now only the topmost layer closes; a sheet
|
||||
with `close-on-escape` off still keeps the Escape from closing what is under it. A dialog or
|
||||
sheet opened from a modal sheet but rendered elsewhere on the page was also hidden from screen
|
||||
readers by the sheet's `aria-hidden`, and Tab could not move inside the dialog; both work now.
|
||||
An application's own guard around a sheet's Escape can go.
|
||||
- **A standard `<x-drawer>` sticks under the top safe area** (`--material-safe-top`, else the
|
||||
device's inset) instead of at the window's edge, so its head and close button no longer slide
|
||||
under an installed app's status bar as the page scrolls; inside `<x-scaffold>` it also sticks
|
||||
under a sticky `<x-app-bar>` in the `top` slot, which covered it. It is as tall as the window
|
||||
below that, and every side sheet keeps the bottom safe area inside its bottom padding.
|
||||
- **A standard `<x-drawer>` takes its place from the first paint.** Until Alpine started, a closed
|
||||
standard sheet was a zero-wide item that still cost its row one gap, and a sheet bound to an open
|
||||
Livewire property had no width yet, so the content beside them changed width when the script
|
||||
ran. A standard sheet is now out of the row until then, and one whose `wire:model` property is
|
||||
open is rendered open and stands at its width. One opened from an Alpine scope alone still
|
||||
appears when Alpine starts, which only the script can know.
|
||||
- **A long value in `<x-select>` ends before the arrow, with an ellipsis.** Where the browser has
|
||||
the customizable select (Chrome, Safari), the closed select drew its value in a box of its own
|
||||
that took neither `text-overflow` nor a width from the select, so a long option in a narrow
|
||||
field, or at `size="sm"` and `xs`, ran on under the arrow; the native select (Firefox) cut it at
|
||||
the arrow with no ellipsis. The select's first child is now a `<button type="button"
|
||||
wire:ignore>` holding `<selectedcontent>`, the browser's copy of the chosen option, which
|
||||
select.css cuts; a browser without the customizable select never renders it. A test that
|
||||
matched a select's markup up to its first `<option>` finds the button there.
|
||||
- **A switch beside long text in a row keeps its track.** `<x-toggle>`'s root could shrink to
|
||||
nothing as a flex item, so beside a paragraph in `<x-row justify="between">`, or with a long
|
||||
label of its own, the 52px track ran out of the row and past a card's edge on a phone.
|
||||
`<x-checkbox>` did the same with its box. Both roots now keep their automatic minimum — the
|
||||
control, and beside a label its longest word — so the text wraps instead. A `flex: none` an
|
||||
application put on the switch to hold it can go.
|
||||
- **A disabled `<x-button fab>` is not drawn on a compact window.** Below `medium` it was still
|
||||
the extended FAB, greyed out and fixed over the content; M3 never shows a disabled FAB and
|
||||
removes one whose action is unavailable, so a `fab` given `disabled` (on a link too) now renders
|
||||
`data-md-unavailable` and is `display: none` there, out of the accessibility tree with it. From
|
||||
`medium` it is the disabled filled button, as before. A `spinner` that disables the button while
|
||||
its action runs leaves the FAB on screen. An application's own `hide-below` or condition around
|
||||
a disabled fab button can go.
|
||||
- **`<x-choices searchable>`, `<x-datepicker>` and `<x-timepicker>` take `full`**, as `<x-input>`,
|
||||
`<x-textarea>`, `<x-select>` and `<x-file>` do, to take the 40rem bound off a field from
|
||||
`medium`; the prop did nothing there, so the field stopped at 40rem beside rows that filled their
|
||||
card. `<x-choices>`' chips have no bound and take `full` without a change, so a call site can
|
||||
switch `searchable` on and off. A width rule an application wrote around one of them for this
|
||||
can go.
|
||||
- **A searchable `<x-choices>`' list is as wide as its field.** It was anchored to a wrapper
|
||||
around the field, which fills its container, so from `medium`, where the field stops at 40rem,
|
||||
the list ran on past the field's end across the whole container. The field itself is the anchor
|
||||
now, and the wrapper `<div>` is gone; the list keeps hanging under the field's supporting text.
|
||||
- **A sheet or dialog that opens shows no tooltip on the control it focuses.** A modal
|
||||
`<x-drawer>`, `<x-bottom-sheet>`, the modal rail and `<x-modal>` move the focus to their first
|
||||
control as they open, and on a page loaded with the sheet open (`wire:model` already set, say
|
||||
from `?workout=` in the URL) or a layer opened from the keyboard, browsers count that focus as
|
||||
keyboard focus: the close button's plain tooltip stood over the sheet the moment it appeared, in
|
||||
Chrome, Firefox and Safari. Plain and rich tooltips now leave out focus a modal layer moves into
|
||||
itself from outside; a Tab onto the control, including the one that wraps round to it, and focus
|
||||
moved within the layer still show them. An `autofocus` an application put on another control to
|
||||
keep the focus off the close button can go.
|
||||
- **`DesignGuard`** reports any class a view or PHP file writes that no stylesheet declares —
|
||||
not the package's `md-*`, not the application's own CSS — instead of naming Tailwind's
|
||||
utilities, variants and theme colours with their 2.0.0 replacement. It now catches a dead class
|
||||
from any source: a typo, a utility of a framework that isn't in the stack, a class whose rule
|
||||
was deleted — not only Tailwind's — but a class assembled in a PHP string (`'text-'.$tone`) is
|
||||
still not seen. A test that asserted on the old Tailwind-shaped message needs the new one
|
||||
instead.
|
||||
- **The pagination views are `material.blade.php` and `simple-material.blade.php`**, no longer named
|
||||
after Tailwind. The provider sets `Paginator::defaultView('pagination::material')` and
|
||||
`defaultSimpleView('pagination::simple-material')`, and switches `livewire.pagination_theme` to
|
||||
`material` whenever it still reads as Livewire's own default (`tailwind`, including when the key
|
||||
is missing), so an application can no longer forget the config and silently render Livewire's own
|
||||
Tailwind view. An application that already chose another theme, or a component with its own
|
||||
`$paginationTheme` property or `paginationView()` method, is untouched — either still wins.
|
||||
- **`foundation.css` no longer explains how to order layers beside Tailwind.** No consumer builds
|
||||
Tailwind any more, so the paragraph about opening both entries with
|
||||
`@layer properties, theme, base, material, components, utilities;` is gone; the layer statement
|
||||
and the order it declares are otherwise unchanged.
|
||||
|
||||
## From 2.0.0 to 2.1.0
|
||||
|
||||
- **Browsers:** Firefox 151 or later (was 147), for container style queries on a custom property;
|
||||
Chrome 125 and Safari 18.4 are unchanged.
|
||||
- **`<x-navigation-rail>`** publishes its value as `--md-navigation-rail-value`, `collapsed` or
|
||||
`expanded` (M3's two rail values). Content an application puts in a rail reads it with
|
||||
`@container style(--md-navigation-rail-value: collapsed)` instead of copying the rail's
|
||||
conditions from `navigation-rail.css`.
|
||||
- **`<x-loading size="96">`** sizes the loading indicator in px, 24 to 240 (M3's responsive range),
|
||||
with the container and the shape in proportion. A width and height of the application's own
|
||||
still work.
|
||||
|
||||
## From 1.x to 2.0.0
|
||||
|
||||
2.0.0 aligns the library with Material Design 3 Expressive as Google documents it (m3.material.io,
|
||||
checked page by page). Most of the change is inside the components. What reaches an application is
|
||||
below, in the order to do it.
|
||||
|
||||
### 1. Breakpoints are M3's window size classes
|
||||
|
||||
Tailwind's `sm:`, `md:`, `lg:`, `xl:` and `2xl:` no longer compile, and there are no responsive
|
||||
variants in their place: the breakpoints are M3's window size classes — compact below 600px, then
|
||||
medium (600px), expanded (840px), large (1200px) and extra-large (1600px) — and only those. A
|
||||
layout component takes the class as a prop (`hide-below`, `hide-from`, `stack-below`, `<x-grid>`'s
|
||||
`columns` map); the application's own CSS writes the width as a range media query:
|
||||
|
||||
| Was | Becomes |
|
||||
|---|---|
|
||||
| `sm:` / `max-sm:` (640) | `medium` · `@media (width >= 600px)` / `(width < 600px)` |
|
||||
| `md:` / `max-md:` (768) | `medium` or `expanded` — choose by what the layout means |
|
||||
| `lg:` / `max-lg:` (1024) | `expanded` · `@media (width >= 840px)` / `(width < 840px)` |
|
||||
| `xl:` / `max-xl:` (1280) | `large` · `@media (width >= 1200px)` / `(width < 1200px)` |
|
||||
| `2xl:` (1536) | `extra-large` · `@media (width >= 1600px)` |
|
||||
|
||||
`<div class="hidden lg:block">` is `<x-stack hide-below="expanded">`; `flex flex-col sm:flex-row` is
|
||||
`<x-row stack-below="medium">`; `grid-cols-1 lg:grid-cols-2` is
|
||||
`<x-grid :columns="['compact' => 1, 'expanded' => 2]">`. Scripts read the same numbers from
|
||||
`resources/js/breakpoints.js` (`from('expanded')`, `upTo('medium')`). `DesignGuard` reports every
|
||||
leftover prefix with its replacement.
|
||||
|
||||
### 2. Only M3's scales, as tokens
|
||||
|
||||
Tailwind's radius, shadow, text-size, weight, leading, tracking and easing utilities compile to
|
||||
nothing, and so do the 1.x utilities that stood for M3's scales. A text style is a class; the
|
||||
rest is a token in the application's own CSS:
|
||||
|
||||
| Was | Becomes |
|
||||
|---|---|
|
||||
| `rounded-sm` … `rounded-4xl`, `rounded-corner-*` (1.x) | `var(--md-sys-shape-corner-xs)` … `var(--md-sys-shape-corner-xxl)` (`-full`, `-none`), or `<x-surface corner="xs">` |
|
||||
| `shadow-sm` … `shadow-2xl`, `shadow-elevation-*` (1.x) | `var(--md-sys-elevation-1)` … `var(--md-sys-elevation-5)` |
|
||||
| `text-xs` … `text-9xl`, `leading-*`, `tracking-*`, `font-medium` … `font-black`, `type-*` (1.x) | one `md-type-*` class (`md-type-body-md`, `md-type-emphasized-title-md` …), or `font: var(--md-sys-typescale-body-md)` with its `-tracking` |
|
||||
| `ease-in`, `ease-out`, `ease-in-out`, `duration-300`, `ease-spatial-*` (1.x) | `var(--md-sys-motion-spatial-*)` / `var(--md-sys-motion-effects-*)` with its `-duration`, in a `transition` |
|
||||
| `gap-4`, `p-4`, `space-y-2` | `gap="space200"`, `<x-surface padding="space200">`, `<x-stack gap="space100">`, or `var(--md-sys-measurement-space200)` |
|
||||
| `state-layer`, `focus-ring`, `touch-target`, `link` (1.x) | `md-state-layer`, `md-focus-ring`, `md-touch-target`, `md-link` |
|
||||
|
||||
The spacing tokens are the 4px grid Tailwind's scale was (`space200` is 16px). `DesignGuard` names
|
||||
each of Tailwind's own utilities with its replacement; the 1.x names are no longer reported.
|
||||
|
||||
### 3. Regenerate the colour scheme
|
||||
|
||||
```bash
|
||||
php artisan material:scheme "#4f46e5" --variant=tonal-spot # the command in the file's header
|
||||
```
|
||||
|
||||
- `success`, `warning` and `info` are now built on the 2025 colour spec with the contrast level,
|
||||
like `error`; their values change. `--harmonize` blends them toward the seed (off by default).
|
||||
- The stylesheet gains medium and high contrast blocks, and `material-scheme.json` gains a
|
||||
`contrast` key; `light`/`dark` at the top level are still the standard scheme, so the mail
|
||||
theme needs nothing. `--contrast` must now be below 0.5; medium (0.5) and high (1.0) are
|
||||
generated alongside.
|
||||
- Config: `theme.contrast` (`default` `system` | `standard` | `medium` | `high`, `storage_key`)
|
||||
and `motion.scheme` (`expressive` | `standard`). `$store.theme` gains `contrast`,
|
||||
`resolvedContrast` and `setContrast()`; `<x-theme-toggle mode="contrast">` is a row of three.
|
||||
|
||||
### 4. Inks are roles, not opacities
|
||||
|
||||
1.x's `text-meta`, `text-body`, `text-quiet`, `border-chrome`, `border-structure` and
|
||||
`border-divider` are gone with the rest of the utilities. Text takes the role's `md-ink-*` class —
|
||||
`md-ink-variant` (on-surface-variant) for `text-meta` and `text-body`, `md-ink-quiet` (outline) for
|
||||
`text-quiet` — and a line is `<x-divider>`, `<x-surface outlined>` or
|
||||
`var(--md-sys-color-outline-variant)` in the application's CSS. Where the old translucent grey was
|
||||
intended, the role is the same colour; where a template relied on the opacity to blend over a
|
||||
colour, use the role directly.
|
||||
|
||||
### 5. Changed defaults and props
|
||||
|
||||
- `<x-toast>` no longer draws a state icon; `type` only picks the announcement role (`alert` for
|
||||
error and warning, `status` otherwise). An actioned snackbar stays until acted on; Escape
|
||||
dismisses a focused one.
|
||||
- `<x-fab>` has no `disabled` prop: M3 says to remove a FAB whose action is unavailable, so hide
|
||||
it instead (a form-submit FAB uses `wire:loading.attr="disabled"`).
|
||||
- `<x-alert>` is `role="status"` for every colour; pass `assertive` where the notice answers
|
||||
something the person just did.
|
||||
- `<x-button size="xs">` and `<x-group size="xs">` are 8px wider; a `<x-button-group>` no longer
|
||||
wraps; connected `xs`/`sm` segments have a 48px minimum width.
|
||||
- `<x-menu-item>` rows are 48px with 16px sides; a selected item draws a trailing check unless it
|
||||
has `icon-right`; a long menu scrolls.
|
||||
- `<x-drawer>` renders a close button by default (`:with-close-button="false"` to drop it, ignored
|
||||
on a `standard` sheet or when Escape and the scrim are off) and left-aligns its `actions` in a
|
||||
72px row. `<x-drawer pane>` is removed; use `<x-list-detail>` for the second pane of a
|
||||
list-detail layout.
|
||||
- `<x-bottom-sheet>` opens at `50dvh` (was `90dvh`); any `height` is capped at
|
||||
`calc(100dvh - 72px)`.
|
||||
- `<x-carousel>` `padding` defaults to `16`; `layout="full-screen"` scrolls vertically with
|
||||
edge-to-edge items; items, not the row, are the tab stops.
|
||||
- `<x-list dividers>` draws a 16px-inset rule; `<x-list selectable>` or `selection="single|multi"`
|
||||
makes it a `listbox` of `option`s with `aria-selected`.
|
||||
- `<x-card>` changes elevation on hover instead of its corner; `data-md-card` carries the variant.
|
||||
- A button directly in `<x-stack>` (stretched, the default) or `<x-form>` keeps its label's width at
|
||||
the start edge instead of filling the width, as M3 asks; a full-width submit is your own CSS.
|
||||
- `<x-modal>` pins its header and actions and scrolls only the body; a `box-class` that set
|
||||
`overflow` no longer applies.
|
||||
- `<x-table>` rows are 52px and the automatic fine-pointer density is gone: pass `dense` for 36px
|
||||
rows (`size="xs" dense` is 24px).
|
||||
- A field in error draws a trailing `error` icon unless it already trails something; a read-only
|
||||
field no longer draws a dashed outline.
|
||||
- The time picker's AM/PM buttons are radios (`aria-checked`); the password reveal no longer sets
|
||||
`aria-pressed`.
|
||||
- `<x-slider size="md">` grows from 44 to 52px and every value indicator from 32 to 44px tall.
|
||||
- `<x-theme-toggle mode="picker"|"contrast">` is a connected group over native radios: the
|
||||
`data-theme-option` hooks are gone; target `input[name="material-theme"][value="…"]`.
|
||||
- `<x-tab>` panels render `id`, `aria-labelledby` and `display: none` from the server.
|
||||
- `<x-section-nav>` with five or more sections is a scrollable tab bar, not a grid. The tab bar is
|
||||
10px taller; the focus ring sits outside.
|
||||
- The navigation bar's horizontal label is `label-md`; `place="bottom"` toolbars sit above
|
||||
`--material-bottom-bar`; a standard toolbar's icon buttons are `primary`. An application that
|
||||
set `--material-bottom-bar` to lift the snackbar over its own bottom toolbar removes it: the
|
||||
toolbar now reads it to place itself, and publishes `--material-bottom-toolbar`, which the
|
||||
snackbar clears and the page pads its end with.
|
||||
- The centred app bar headline is a grid column; the `search` variant bounds its own width, so
|
||||
drop hand-written `max-w-*` wrappers.
|
||||
- `<x-app-shell>` is renamed `<x-scaffold>`, with no alias; it is a column with a nested row
|
||||
and gains `banner` and `fab` slots. `<x-navigation-rail>`'s `header` slot takes one
|
||||
`<x-fab label icon>` that morphs (replace the two-FAB `rail-collapsed:` swap); new `divider`
|
||||
and `fill` props. The `rail-collapsed:` variant is gone: style what the application puts in a
|
||||
rail by the rail's value, `@container style(--md-navigation-rail-value: collapsed)` (2.1.0).
|
||||
`data-app-shell`, `data-app-shell-bar`, `data-app-shell-actions` and
|
||||
`data-app-shell-banner` are `data-md-scaffold`, `data-md-scaffold-bar`, `data-md-scaffold-actions`
|
||||
and `data-md-scaffold-banner`; the skip link is `data-md-skip-link`.
|
||||
- `<x-icon optical="20">` selects the optical-size-20 cut; the components pass it for their own
|
||||
small icons, applications pass it for icons drawn at 20px or less.
|
||||
- A text field stops at 40rem wide from medium (600px), as M3 bounds fields on wider windows; a
|
||||
width rule of the application's own on the field beats it, and `full` removes it.
|
||||
- Every `<x-modal>` draws a rule under its header and over its actions while its body scrolls,
|
||||
and neither when the body fits. `separator` now means "always draw both rules" and no longer
|
||||
renders two `<x-divider>` elements; the spacing between header, body and actions moved to
|
||||
M3's split gaps, so a dialog that fits is a few pixels shorter.
|
||||
- A snackbar with a `description` is 68px tall, and below medium (600px) a two-line snackbar with an
|
||||
action puts the action under the text. Alt+G moves focus to an actioned snackbar.
|
||||
- `<x-bottom-sheet>`'s drag now follows the pointer and settles on the nearest preset height when
|
||||
`heights` or `snap` is set; without stops it behaves as before.
|
||||
- A chip set with `scroll` shows a scroll button over each overflowing edge on fine pointers.
|
||||
|
||||
### 6. `material.css` is gone, and so is Tailwind
|
||||
|
||||
The 1.x single-import shortcut no longer exists, and Tailwind has left the whole stack — the
|
||||
package, its showcase, error pages and Workbench build carry none, and an application drops it
|
||||
too. The package's CSS is plain, no build step of its own, in one entry:
|
||||
`resources/css/all.css` for everything, or `foundation.css` first and then the stylesheet of each
|
||||
component the views render, opening with the layer statement every package stylesheet does
|
||||
(see Installation in `README.md`). An application's views write no utility layer of their own
|
||||
either: layout components (`<x-scaffold>`, `<x-pane>`, `<x-stack>`, `<x-row>`, `<x-grid>`, the
|
||||
canonical layouts) take M3's spacing tokens and breakpoints as props, a small set of `md-type-*`
|
||||
and `md-ink-*` classes covers text on plain elements, and `--md-sys-*` custom properties serve the
|
||||
rest of an application's own stylesheet. The foundation smooths text in grayscale, as Tailwind's
|
||||
`antialiased` class did: drop the class.
|
||||
|
||||
### 7. New in 2.0.0
|
||||
|
||||
Nothing to migrate, but worth knowing: submenus (`<x-menu-item submenu>`), a filtering menu
|
||||
(`<x-menu filter>`), a menu that opens as a bottom sheet on compact windows
|
||||
(`<x-menu sheet-at-compact>`), gap-grouped menu items (`<x-menu-group gap>`), square button groups
|
||||
(`shape="square"`), managed selection on connected groups (`selection="single|multi" required`),
|
||||
FAB collapse on scroll (`<x-fab collapse-on-scroll>`), the tall navigation bar and hide-on-scroll
|
||||
(`tall`, `hide-on-scroll`; `tall-bar`, `hide-bar-on-scroll` on the shell), the narrow, centred and
|
||||
hide-when-collapsed rail (`width="narrow"`, `align="center"`, `hide-when-collapsed`), a docked
|
||||
toolbar with a FAB and a rounded large-screen form (`rounded`), app bar actions that overflow into
|
||||
a menu (`:actions="[…]"`), the list item's `video` slot, the card's dragged state, the standard
|
||||
side sheet (`<x-drawer standard>`), bottom sheet preset heights, the multi-aspect carousel
|
||||
(`layout="multi-aspect"`), the divider with a subheader (`<x-divider text>`), the character counter
|
||||
(`counter`), the vertical slider (`orientation="vertical"`), the full-screen range date picker on
|
||||
compact windows, search's icon entry point and suggestions (`trigger="icon"`, `suggestions` slot),
|
||||
the contrast toggle (`<x-theme-toggle mode="contrast">`) and the Standard motion scheme.
|
||||
|
||||
### 8. Tests and guards
|
||||
|
||||
`DesignGuard` changes with the stack:
|
||||
|
||||
- **Removed:** the maryUI tag and daisyUI class checks, `forbidAbsolutes()` and `forbidOpacityInk()`.
|
||||
Every Tailwind utility now compiles to nothing and is reported with its replacement, so
|
||||
`bg-white` and `text-on-surface/60` still are.
|
||||
- **Retargeted:** `forbidColours([...])` keeps its signature and reports a left-out role where 2.0.0
|
||||
writes one: `var(--md-sys-color-…)` in CSS or an inline `style`, an `md-ink-*` class, a
|
||||
component's `color`/`tone` prop.
|
||||
- **New:** `missingStylesheets($cssEntry)` names each `@import` the views need, and
|
||||
`unusedStylesheets($cssEntry)` each one they no longer do; a `.css` file passed
|
||||
to `scan()` is checked for literal values and off-scale media queries.
|
||||
|
||||
Browser tests that assert widths switch at 640/1024/1280 now
|
||||
switch at 600/840/1200; tests that read `role="alert"` on an alert, `aria-pressed` on the time
|
||||
picker's period buttons or `data-theme-option` need the new hooks above.
|
||||
|
||||
Every component hook is prefixed `data-md-`: `data-toolbar-place` is `data-md-toolbar-place`,
|
||||
`data-account-menu` is `data-md-account-menu`, `data-field-copy` is `data-md-field-copy`. The
|
||||
attributes on `<html>` keep their names (`data-theme`, `data-contrast`, `data-scheme`,
|
||||
`data-motion`, `data-rail`). A component's inner parts also carry its name:
|
||||
|
||||
| 1.x | 2.0.0 |
|
||||
|---|---|
|
||||
| `data-app-shell`, `-bar`, `-actions`, `-banner` | `data-md-scaffold`, `-bar`, `-actions`, `-banner` |
|
||||
| `data-theme-option="dark"` | `input[name="material-theme"][value="dark"]` |
|
||||
| `data-scheme-option="teal"` | `data-md-scheme-picker-option="teal"` |
|
||||
| `data-account-theme` | `data-md-account-menu-theme` |
|
||||
| `data-section-picker` | `data-md-section-nav-picker` |
|
||||
| `data-material-carousel`, `-item`, `-content`, `-label`, `-surface` | `data-md-carousel`, `-item`, `-content`, `-label`, `-surface` |
|
||||
| `data-sheet` (drawer) | `data-md-drawer-sheet` |
|
||||
| `data-drag-handle` (bottom sheet) | `data-md-bottom-sheet-handle` |
|
||||
| `data-check`, `data-mixed` (checkbox) | `data-md-checkbox-check`, `data-md-checkbox-mixed` |
|
||||
| `data-on`, `data-off`, `data-handle` (toggle) | `data-md-switch-on`, `data-md-switch-off`, `data-md-switch-handle` |
|
||||
| `data-handle`, `data-thumb`, `data-tick`, `data-stop`, `data-segment`, `data-track-icon` (slider) | `data-md-slider-handle`, `-thumb`, `-tick`, `-stop`, `-segment`, `-icon` |
|
||||
|
||||
Rename the negative assertions too: `->not->toContain('data-app-bar')` passes against 2.0.0
|
||||
whatever the page renders.
|
||||
|
||||
### 9. For AI agents
|
||||
|
||||
`php artisan boost:update --discover` picks up the new `material-3` guideline and the
|
||||
`material-3-design` skill, which state M3's rules and tables beside the library's classes, props
|
||||
and tokens.
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Prints the variable axes of the packaged Google Sans Flex subset.
|
||||
*
|
||||
* Run from the repository root with `node bin/check-font.mjs [woff2]`.
|
||||
* tests/Feature/FontTest.php runs it through the configured `node` binary and asserts the ranges
|
||||
* the CSS needs (wght, ROND — see that test for why), because PHP cannot open a woff2 without the
|
||||
* Brotli extension.
|
||||
*
|
||||
* Output is one JSON object on stdout — {file, postscriptName, numGlyphs, axes: {tag: {name, min,
|
||||
* default, max}}}. The exit status is 1, with the reason on stderr, only when the file itself
|
||||
* can't be opened.
|
||||
*/
|
||||
import { openSync } from 'fontkit'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const file = process.argv[2] ?? fileURLToPath(new URL('../resources/fonts/google-sans-flex/GoogleSansFlex-Latin.woff2', import.meta.url))
|
||||
|
||||
let font
|
||||
|
||||
try {
|
||||
font = openSync(file)
|
||||
} catch (error) {
|
||||
process.stderr.write(`${file}: ${error.message}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
file,
|
||||
postscriptName: font.postscriptName,
|
||||
numGlyphs: font.numGlyphs,
|
||||
axes: font.variationAxes ?? {},
|
||||
})}\n`)
|
||||
+44
-18
@@ -2,12 +2,15 @@
|
||||
#
|
||||
# Refresh resources/svg/symbols from google/material-design-icons (Apache-2.0).
|
||||
#
|
||||
# Material Symbols Rounded at weight 400, grade 0 and optical size 24, outlined and
|
||||
# filled, for every symbol Google publishes. Maintenance only: applications never run
|
||||
# this, and bin/ is not part of the archive Composer installs.
|
||||
# Material Symbols Rounded at weight 400 and grade 0, outlined and filled, in two cuts:
|
||||
# optical size 24 (the standard size) and optical size 20. M3 draws an icon at 20 dp or
|
||||
# smaller from the 20 cut, whose strokes stay as heavy as the 24 dp design's instead of
|
||||
# thinning as the glyph is scaled down (docs/reference/m3/styles.md § Icons, the optical
|
||||
# size axis). Maintenance only: applications never run this, and bin/ is not part of the
|
||||
# archive Composer installs.
|
||||
#
|
||||
# The repository is ~5 GB, so it is cloned without blobs and checked out sparsely:
|
||||
# git downloads only the two files per symbol that are kept. The @material-symbols
|
||||
# git downloads only the four files per symbol that are kept. The @material-symbols
|
||||
# npm packages were not used because their SVGs are drawn at optical size 48, whose
|
||||
# strokes are thinner than the 24px design.
|
||||
|
||||
@@ -24,6 +27,7 @@ cd "$work/repo"
|
||||
|
||||
git sparse-checkout set --no-cone \
|
||||
'/symbols/web/*/materialsymbolsrounded/*_24px.svg' \
|
||||
'/symbols/web/*/materialsymbolsrounded/*_20px.svg' \
|
||||
'!/symbols/web/*/materialsymbolsrounded/*_wght[0-9]*' \
|
||||
'!/symbols/web/*/materialsymbolsrounded/*_grad200*' \
|
||||
'!/symbols/web/*/materialsymbolsrounded/*_gradN25*'
|
||||
@@ -31,14 +35,33 @@ git sparse-checkout set --no-cone \
|
||||
git checkout --quiet "$(git symbolic-ref --short HEAD)"
|
||||
|
||||
out="$root/resources/svg/symbols"
|
||||
rm -rf "$out/outlined" "$out/filled"
|
||||
mkdir -p "$out/outlined" "$out/filled"
|
||||
|
||||
# The 24 cut keeps the folder names it has always had; the 20 cut is suffixed, so a
|
||||
# name resolves to a folder without a lookup table (Support\SvgFile::symbol()).
|
||||
folder() { # optical size, style
|
||||
case "$1" in
|
||||
24) echo "$out/$2" ;;
|
||||
*) echo "$out/$2-$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
for size in 24 20; do
|
||||
for style in outlined filled; do
|
||||
rm -rf "$(folder "$size" "$style")"
|
||||
mkdir -p "$(folder "$size" "$style")"
|
||||
done
|
||||
done
|
||||
|
||||
# Google's files carry width="24" height="24" and no fill. Drop the size so CSS decides
|
||||
# it, and paint in currentColor so an icon takes the colour of the text around it.
|
||||
# it, and paint in currentColor so an icon takes the colour of the text around it. Nearly
|
||||
# every file draws in a viewBox="0 -960 960 960"; a few carry none and draw in the units of
|
||||
# their width and height (24, or 20 in the 20 cut), so the size becomes their viewBox before
|
||||
# it goes, or the glyph would only fit at exactly that many pixels.
|
||||
normalise() {
|
||||
sed -E \
|
||||
-e 's/ (width|height)="[0-9]+"//g' \
|
||||
-e '/viewBox=/!s/<svg ([^>]*)height="([0-9]+)(px)?"([^>]*)width="([0-9]+)(px)?"/<svg \1height="\2"\4width="\5" viewBox="0 0 \5 \2"/' \
|
||||
-e '/viewBox=/!s/<svg ([^>]*)width="([0-9]+)(px)?"([^>]*)height="([0-9]+)(px)?"/<svg \1width="\2"\4height="\5" viewBox="0 0 \2 \5"/' \
|
||||
-e 's/ (width|height)="[0-9]+(px)?"//g' \
|
||||
-e 's/<svg /<svg fill="currentColor" /' \
|
||||
"$1"
|
||||
}
|
||||
@@ -47,19 +70,22 @@ missing=0
|
||||
|
||||
for dir in symbols/web/*/materialsymbolsrounded; do
|
||||
name="$(basename "$(dirname "$dir")")"
|
||||
outlined="$dir/${name}_24px.svg"
|
||||
filled="$dir/${name}_fill1_24px.svg"
|
||||
|
||||
if [[ ! -f "$outlined" || ! -f "$filled" ]]; then
|
||||
echo "skipped $name: no outlined and filled 24px pair" >&2
|
||||
missing=$((missing + 1))
|
||||
continue
|
||||
fi
|
||||
for size in 24 20; do
|
||||
outlined="$dir/${name}_${size}px.svg"
|
||||
filled="$dir/${name}_fill1_${size}px.svg"
|
||||
|
||||
normalise "$outlined" > "$out/outlined/$name.svg"
|
||||
normalise "$filled" > "$out/filled/$name.svg"
|
||||
if [[ ! -f "$outlined" || ! -f "$filled" ]]; then
|
||||
echo "skipped $name: no outlined and filled ${size}px pair" >&2
|
||||
missing=$((missing + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
normalise "$outlined" > "$(folder "$size" outlined)/$name.svg"
|
||||
normalise "$filled" > "$(folder "$size" filled)/$name.svg"
|
||||
done
|
||||
done
|
||||
|
||||
git rev-parse HEAD > "$out/SOURCE"
|
||||
|
||||
echo "$(ls "$out/outlined" | wc -l | tr -d ' ') symbols from google/material-design-icons@$(cut -c1-12 "$out/SOURCE"), $missing skipped"
|
||||
echo "$(ls "$out/outlined" | wc -l | tr -d ' ') symbols at optical size 24 and $(ls "$out/outlined-20" | wc -l | tr -d ' ') at 20 from google/material-design-icons@$(cut -c1-12 "$out/SOURCE"), $missing skipped"
|
||||
|
||||
+5
-341
@@ -42,12 +42,11 @@
|
||||
* compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/LoadingIndicator.kt
|
||||
* compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/LoadingIndicatorTokens.kt
|
||||
* compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/SpringSimulation.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/PolygonMeasure.kt
|
||||
* graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/RoundedPolygon.kt (calculateMaxBounds)
|
||||
*
|
||||
* The feature matching and Morph itself (FeatureMapping.kt, FloatMapping.kt, Morph.kt,
|
||||
* PolygonMeasure.kt) are shared with progress.js: see resources/js/shapes.js.
|
||||
*
|
||||
* Copyright 2022-2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -65,13 +64,11 @@
|
||||
*/
|
||||
import { mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { cubicBounds, point, pointOnCurve, split, SHAPES } from './shapes.mjs'
|
||||
import { SHAPES } from './shapes.mjs'
|
||||
import { asCubics, cubicBounds, match, pointOnCurve } from '../resources/js/shapes.js'
|
||||
|
||||
const OUTPUT = 'resources/svg/loading-indicator'
|
||||
|
||||
const DISTANCE_EPSILON = 1e-4
|
||||
const ANGLE_EPSILON = 1e-6
|
||||
|
||||
// LoadingIndicator.kt / LoadingIndicatorTokens.kt --------------------------------------
|
||||
|
||||
const SEQUENCE = ['soft-burst', 'cookie-9', 'pentagon', 'pill', 'sunny', 'cookie-4', 'oval']
|
||||
@@ -94,339 +91,6 @@ const HANDOVER_LEAD = 0.001
|
||||
/** The fastest collapse a keySpline can give: half the size within a frame, then a long tail. */
|
||||
const COLLAPSE = [0, 1, 0, 1]
|
||||
|
||||
// Utils.kt / FloatMapping.kt ------------------------------------------------------------
|
||||
|
||||
const positiveModulo = (num, mod) => ((num % mod) + mod) % mod
|
||||
const progressInRange = (progress, from, to) =>
|
||||
to >= from ? progress >= from && progress <= to : progress >= from || progress <= to
|
||||
|
||||
function progressDistance(a, b) {
|
||||
const d = Math.abs(a - b)
|
||||
|
||||
return Math.min(d, 1 - d)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/** DoubleMapper: maps outline progress on one shape to the other and back, from [source, target] pairs. */
|
||||
function doubleMapper(mappings) {
|
||||
const sources = mappings.map((m) => m[0])
|
||||
const targets = mappings.map((m) => m[1])
|
||||
|
||||
return { map: (x) => linearMap(sources, targets, x), mapBack: (x) => linearMap(targets, sources, x) }
|
||||
}
|
||||
|
||||
// PolygonMeasure.kt ---------------------------------------------------------------------
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
const measureCubic = (c) => closestProgressTo(c, Infinity)[1]
|
||||
const findCubicCutPoint = (c, measure) => closestProgressTo(c, measure)[0]
|
||||
|
||||
class MeasuredCubic {
|
||||
constructor(cubic, startOutlineProgress, endOutlineProgress) {
|
||||
if (endOutlineProgress < startOutlineProgress) {
|
||||
throw new Error('endOutlineProgress is expected to be equal or greater than startOutlineProgress')
|
||||
}
|
||||
|
||||
this.cubic = cubic
|
||||
this.startOutlineProgress = startOutlineProgress
|
||||
this.endOutlineProgress = endOutlineProgress
|
||||
this.measuredSize = measureCubic(cubic)
|
||||
}
|
||||
|
||||
cutAtProgress(cutOutlineProgress) {
|
||||
const bounded = Math.min(Math.max(cutOutlineProgress, this.startOutlineProgress), this.endOutlineProgress)
|
||||
const relativeProgress =
|
||||
(bounded - this.startOutlineProgress) / (this.endOutlineProgress - this.startOutlineProgress)
|
||||
const t = findCubicCutPoint(this.cubic, relativeProgress * this.measuredSize)
|
||||
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((cubic, i) => {
|
||||
if (feature.type === 'corner' && i === Math.floor(feature.cubics.length / 2)) {
|
||||
featureToCubic.push([feature, cubics.length])
|
||||
}
|
||||
|
||||
cubics.push(cubic)
|
||||
})
|
||||
}
|
||||
|
||||
const measures = [0]
|
||||
|
||||
for (const cubic of cubics) {
|
||||
measures.push(measures.at(-1) + measureCubic(cubic))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// FeatureMapping.kt ---------------------------------------------------------------------
|
||||
|
||||
function featureRepresentativePoint(feature) {
|
||||
const first = feature.cubics[0]
|
||||
const last = feature.cubics.at(-1)
|
||||
|
||||
return point((first[0] + last[6]) / 2, (first[1] + last[7]) / 2)
|
||||
}
|
||||
|
||||
function featureDistSquared(f1, f2) {
|
||||
if (f1.type === 'corner' && f2.type === 'corner' && f1.convex !== f2.convex) {
|
||||
return Infinity
|
||||
}
|
||||
|
||||
const p1 = featureRepresentativePoint(f1)
|
||||
const p2 = featureRepresentativePoint(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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Array.prototype.sort is stable, like Kotlin's sortedBy.
|
||||
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
|
||||
}
|
||||
|
||||
function featureMapper(features1, features2) {
|
||||
const corners = (features) => features.filter(({ feature }) => feature.type === 'corner')
|
||||
|
||||
return doubleMapper(doMapping(corners(features1), corners(features2)))
|
||||
}
|
||||
|
||||
// Morph.kt ------------------------------------------------------------------------------
|
||||
|
||||
/** Morph.match: the start and end shapes cut into pairs of matching cubics. */
|
||||
function match(p1, p2) {
|
||||
const measuredPolygon1 = MeasuredPolygon.measure(p1)
|
||||
const measuredPolygon2 = MeasuredPolygon.measure(p2)
|
||||
const mapper = featureMapper(measuredPolygon1.features, measuredPolygon2.features)
|
||||
const polygon2CutPoint = mapper.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 : mapper.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(mapper.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 exactly 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
|
||||
}
|
||||
|
||||
// LoadingIndicator.kt: calculateScaleFactor, processPath --------------------------------
|
||||
|
||||
/** RoundedPolygon.calculateMaxBounds: a square holding the shape in any rotation. */
|
||||
|
||||
+216
-30
@@ -4,14 +4,23 @@
|
||||
* Rebuild with `npm run build:scheme` after changing this file or upgrading
|
||||
* @material/material-color-utilities; the bundle is committed so applications need
|
||||
* nothing but `node`. (The published library imports without file extensions, which
|
||||
* plain Node refuses, so it cannot be run unbundled anyway.)
|
||||
* plain Node refuses, so it cannot be run unbundled anyway — and two of the classes
|
||||
* below are imported by path, which only a bundler resolves.)
|
||||
*
|
||||
* Input: one JSON argument — {seed, variant, contrast, success, warning, info}.
|
||||
* Output: JSON on stdout — {seed, variant, spec, contrast, light: {role: hex}, dark: {role: hex}}.
|
||||
* Input: one JSON argument — {seed, variant, spec, contrast, harmonize, success, warning,
|
||||
* info}. `spec` is the colour spec, '2025' (M3 Expressive, the default) or '2021' (M3 as it
|
||||
* first shipped). `contrast` is the standard level, below M3's medium (0.5).
|
||||
* Output: JSON on stdout — {seed, variant, spec, harmonize, contrast: {standard, medium:
|
||||
* {light, dark}, high: {light, dark}}, light: {role: hex}, dark: {role: hex}}. The top-level
|
||||
* light and dark are the standard level; medium (0.5) and high (1.0) are M3's other two
|
||||
* contrast levels, generated for both themes so a stylesheet can key them on an attribute.
|
||||
*/
|
||||
import {
|
||||
argbFromHex,
|
||||
customColor,
|
||||
Blend,
|
||||
clampDouble,
|
||||
DynamicColor,
|
||||
extendSpecVersion,
|
||||
hexFromArgb,
|
||||
Hct,
|
||||
MaterialDynamicColors,
|
||||
@@ -24,7 +33,12 @@ import {
|
||||
SchemeRainbow,
|
||||
SchemeTonalSpot,
|
||||
SchemeVibrant,
|
||||
TonalPalette,
|
||||
Variant,
|
||||
} from '@material/material-color-utilities'
|
||||
// Google exports neither from the package's index; the bundler resolves the path.
|
||||
import { ContrastCurve } from '../node_modules/@material/material-color-utilities/dynamiccolor/contrast_curve.js'
|
||||
import { ToneDeltaPair } from '../node_modules/@material/material-color-utilities/dynamiccolor/tone_delta_pair.js'
|
||||
|
||||
const VARIANTS = {
|
||||
'tonal-spot': SchemeTonalSpot,
|
||||
@@ -38,9 +52,15 @@ const VARIANTS = {
|
||||
'fruit-salad': SchemeFruitSalad,
|
||||
}
|
||||
|
||||
// M3's three contrast levels (styles/color/roles, "What's new May 2025"): standard is the
|
||||
// scheme's own level, medium and high are fixed. Every level is generated for both themes.
|
||||
const LEVELS = { standard: 0, medium: 0.5, high: 1 }
|
||||
|
||||
// Key colours seed the palettes; they are not roles a stylesheet paints with.
|
||||
const NOT_ROLES = /_palette_key_color$/
|
||||
|
||||
const STATES = ['success', 'warning', 'info']
|
||||
|
||||
function fail(message) {
|
||||
process.stderr.write(`${message}\n`)
|
||||
process.exit(1)
|
||||
@@ -56,25 +76,186 @@ try {
|
||||
|
||||
const hex = /^#[0-9a-f]{6}$/i
|
||||
const Scheme = VARIANTS[input.variant]
|
||||
const SPECS = ['2021', '2025']
|
||||
const spec = input.spec ?? '2025'
|
||||
|
||||
if (!hex.test(input.seed ?? '')) fail(`The seed must be a #rrggbb colour, "${input.seed}" given.`)
|
||||
if (!Scheme) fail(`Unknown variant "${input.variant}". Use one of: ${Object.keys(VARIANTS).join(', ')}.`)
|
||||
if (!SPECS.includes(spec)) fail(`Unknown spec "${spec}". Use one of: ${SPECS.join(', ')}.`)
|
||||
|
||||
for (const state of ['success', 'warning', 'info']) {
|
||||
for (const state of STATES) {
|
||||
if (!hex.test(input[state] ?? '')) fail(`The ${state} colour must be a #rrggbb colour, "${input[state]}" given.`)
|
||||
}
|
||||
|
||||
const contrast = Number(input.contrast ?? 0)
|
||||
const harmonize = Boolean(input.harmonize ?? false)
|
||||
|
||||
if (!(contrast >= -1 && contrast <= 1)) fail(`The contrast level must be between -1 and 1, "${input.contrast}" given.`)
|
||||
if (!(contrast >= -1 && contrast < LEVELS.medium)) {
|
||||
fail(`The contrast level ${input.contrast} is the standard block's, from -1 to below ${LEVELS.medium}. Medium (${LEVELS.medium}) and high (${LEVELS.high}) are always generated beside it, under [data-contrast]; the head script picks one.`)
|
||||
}
|
||||
|
||||
const source = Hct.fromInt(argbFromHex(input.seed))
|
||||
const colors = new MaterialDynamicColors()
|
||||
|
||||
function roles(isDark) {
|
||||
/**
|
||||
* The contrast curves of Google's 2025 spec, from its private getCurve()
|
||||
* (dynamiccolor/color_spec_2025.js).
|
||||
*/
|
||||
function curve(defaultContrast) {
|
||||
return {
|
||||
1.5: new ContrastCurve(1.5, 1.5, 3, 5.5),
|
||||
3: new ContrastCurve(3, 3, 4.5, 7),
|
||||
4.5: new ContrastCurve(4.5, 4.5, 7, 11),
|
||||
6: new ContrastCurve(6, 6, 7, 11),
|
||||
7: new ContrastCurve(7, 7, 11, 21),
|
||||
}[defaultContrast]
|
||||
}
|
||||
|
||||
/** The tone that holds the palette's chroma, searching up or down — Google's findBestToneForChroma. */
|
||||
function bestToneForChroma(hue, chroma, tone, byDecreasingTone) {
|
||||
let answer = tone
|
||||
let best = Hct.from(hue, chroma, answer)
|
||||
|
||||
while (best.chroma < chroma) {
|
||||
if (tone < 0 || tone > 100) break
|
||||
|
||||
tone += byDecreasingTone ? -1 : 1
|
||||
|
||||
const candidate = Hct.from(hue, chroma, tone)
|
||||
|
||||
if (best.chroma < candidate.chroma) {
|
||||
best = candidate
|
||||
answer = tone
|
||||
}
|
||||
}
|
||||
|
||||
return answer
|
||||
}
|
||||
|
||||
/** Google's tMaxC: the most chromatic tone, from the top. */
|
||||
function tMaxC(palette, lowerBound = 0, upperBound = 100) {
|
||||
return clampDouble(lowerBound, upperBound, bestToneForChroma(palette.hue, palette.chroma, 100, true))
|
||||
}
|
||||
|
||||
/** Google's tMinC: the most chromatic tone, from the bottom. */
|
||||
function tMinC(palette, lowerBound = 0, upperBound = 100) {
|
||||
return clampDouble(lowerBound, upperBound, bestToneForChroma(palette.hue, palette.chroma, 0, false))
|
||||
}
|
||||
|
||||
/**
|
||||
* success, warning and info as M3 custom colours: the four roles of a semantic colour, built on
|
||||
* their own tonal palette exactly as Google builds error / on-error / error-container /
|
||||
* on-error-container — the same tones, contrast curves and tone delta pairs in both specs
|
||||
* (dynamiccolor/color_spec_2021.js and color_spec_2025.js, "Errors [E]"). Because they are
|
||||
* DynamicColors against the page's own DynamicScheme, they follow its contrast level, its dark
|
||||
* tones, its spec version and its platform the way every other role does; the 2021 recipe they
|
||||
* used before (customColor) was fixed at tones 40/100/90/10 whatever the scheme asked for.
|
||||
*/
|
||||
function stateRoles(name, palette) {
|
||||
const of = () => palette
|
||||
let color, onColor, container, onContainer
|
||||
|
||||
color = extendSpecVersion(
|
||||
DynamicColor.fromPalette({
|
||||
name,
|
||||
palette: of,
|
||||
tone: (s) => (s.isDark ? 80 : 40),
|
||||
isBackground: true,
|
||||
background: (s) => colors.highestSurface(s),
|
||||
contrastCurve: () => new ContrastCurve(3, 4.5, 7, 7),
|
||||
toneDeltaPair: () => new ToneDeltaPair(container, color, 10, 'nearer', false),
|
||||
}),
|
||||
'2025',
|
||||
DynamicColor.fromPalette({
|
||||
name,
|
||||
palette: of,
|
||||
tone: (s) => (s.platform === 'phone' ? (s.isDark ? tMinC(palette, 0, 98) : tMaxC(palette)) : tMinC(palette)),
|
||||
isBackground: true,
|
||||
background: (s) => (s.platform === 'phone' ? colors.highestSurface(s) : colors.surfaceContainerHigh()),
|
||||
contrastCurve: (s) => (s.platform === 'phone' ? curve(4.5) : curve(7)),
|
||||
toneDeltaPair: (s) => (s.platform === 'phone' ? new ToneDeltaPair(container, color, 5, 'relative_lighter', true, 'farther') : undefined),
|
||||
}),
|
||||
)
|
||||
|
||||
onColor = extendSpecVersion(
|
||||
DynamicColor.fromPalette({
|
||||
name: `on_${name}`,
|
||||
palette: of,
|
||||
tone: (s) => (s.isDark ? 20 : 100),
|
||||
background: () => color,
|
||||
contrastCurve: () => new ContrastCurve(4.5, 7, 11, 21),
|
||||
}),
|
||||
'2025',
|
||||
DynamicColor.fromPalette({
|
||||
name: `on_${name}`,
|
||||
palette: of,
|
||||
// Google's on-error sits on error-dim off the phone; a state colour has no dim role.
|
||||
background: () => color,
|
||||
contrastCurve: (s) => (s.platform === 'phone' ? curve(6) : curve(7)),
|
||||
}),
|
||||
)
|
||||
|
||||
container = extendSpecVersion(
|
||||
DynamicColor.fromPalette({
|
||||
name: `${name}_container`,
|
||||
palette: of,
|
||||
tone: (s) => (s.isDark ? 30 : 90),
|
||||
isBackground: true,
|
||||
background: (s) => colors.highestSurface(s),
|
||||
contrastCurve: () => new ContrastCurve(1, 1, 3, 4.5),
|
||||
toneDeltaPair: () => new ToneDeltaPair(container, color, 10, 'nearer', false),
|
||||
}),
|
||||
'2025',
|
||||
DynamicColor.fromPalette({
|
||||
name: `${name}_container`,
|
||||
palette: of,
|
||||
tone: (s) => (s.platform === 'watch' ? 30 : s.isDark ? tMinC(palette, 30, 93) : tMaxC(palette, 0, 90)),
|
||||
isBackground: true,
|
||||
background: (s) => (s.platform === 'phone' ? colors.highestSurface(s) : undefined),
|
||||
toneDeltaPair: (s) => (s.platform === 'watch' ? new ToneDeltaPair(container, color, 10, 'darker', true, 'farther') : undefined),
|
||||
contrastCurve: (s) => (s.platform === 'phone' && s.contrastLevel > 0 ? curve(1.5) : undefined),
|
||||
}),
|
||||
)
|
||||
|
||||
onContainer = extendSpecVersion(
|
||||
DynamicColor.fromPalette({
|
||||
name: `on_${name}_container`,
|
||||
palette: of,
|
||||
tone: (s) => (s.variant === Variant.MONOCHROME ? (s.isDark ? 90 : 10) : s.isDark ? 90 : 30),
|
||||
background: () => container,
|
||||
contrastCurve: () => new ContrastCurve(3, 4.5, 7, 11),
|
||||
}),
|
||||
'2025',
|
||||
DynamicColor.fromPalette({
|
||||
name: `on_${name}_container`,
|
||||
palette: of,
|
||||
background: () => container,
|
||||
contrastCurve: (s) => (s.platform === 'phone' ? curve(4.5) : curve(7)),
|
||||
}),
|
||||
)
|
||||
|
||||
return {
|
||||
[name]: color,
|
||||
[`on-${name}`]: onColor,
|
||||
[`${name}-container`]: container,
|
||||
[`on-${name}-container`]: onContainer,
|
||||
}
|
||||
}
|
||||
|
||||
// Harmonisation pulls a state's hue towards the seed, so the three read as part of the scheme
|
||||
// (styles/color/advanced/define-new-colors). Off by default: a state has to stay recognisable
|
||||
// whatever the brand is, and a green that has turned blue no longer says "success".
|
||||
const states = Object.assign({}, ...STATES.map((state) => {
|
||||
const value = argbFromHex(input[state])
|
||||
|
||||
return stateRoles(state, TonalPalette.fromInt(harmonize ? Blend.harmonize(value, argbFromHex(input.seed)) : value))
|
||||
}))
|
||||
|
||||
function roles(isDark, contrastLevel) {
|
||||
// The 2025 spec is M3 Expressive's colour; the library falls back to 2021 for the
|
||||
// variants the new spec does not define (fidelity, content, monochrome, …).
|
||||
const scheme = new Scheme(source, isDark, contrast, '2025')
|
||||
// variants the new spec does not define (fidelity, content, monochrome, …). 2021 is
|
||||
// M3's original colour, for an application whose palette was generated with it.
|
||||
const scheme = new Scheme(source, isDark, contrastLevel, spec)
|
||||
const out = {}
|
||||
|
||||
for (const color of colors.allColors) {
|
||||
@@ -83,36 +264,41 @@ function roles(isDark) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const [role, color] of Object.entries(states)) {
|
||||
out[role] = hexFromArgb(color.getArgb(scheme))
|
||||
}
|
||||
|
||||
return { scheme, out }
|
||||
}
|
||||
|
||||
const light = roles(false)
|
||||
const dark = roles(true)
|
||||
/** One contrast level: both themes, with each theme's inverse state colours from the other. */
|
||||
function level(contrastLevel) {
|
||||
const light = roles(false, contrastLevel)
|
||||
const dark = roles(true, contrastLevel)
|
||||
|
||||
// success, warning and info are M3 custom colours, harmonisation off: harmonising pulls
|
||||
// each hue towards the seed, and a state has to stay recognisable whatever the brand is.
|
||||
for (const state of ['success', 'warning', 'info']) {
|
||||
const group = customColor(argbFromHex(input.seed), { name: state, value: argbFromHex(input[state]), blend: false })
|
||||
|
||||
for (const [theme, colours] of [['light', light.out], ['dark', dark.out]]) {
|
||||
colours[state] = hexFromArgb(group[theme].color)
|
||||
colours[`on-${state}`] = hexFromArgb(group[theme].onColor)
|
||||
colours[`${state}-container`] = hexFromArgb(group[theme].colorContainer)
|
||||
colours[`on-${state}-container`] = hexFromArgb(group[theme].onColorContainer)
|
||||
// A state colour drawn on the inverse surface (a snackbar's icon) is the other theme's.
|
||||
for (const state of ['error', ...STATES]) {
|
||||
light.out[`inverse-${state}`] = dark.out[state]
|
||||
dark.out[`inverse-${state}`] = light.out[state]
|
||||
}
|
||||
|
||||
return { spec: light.scheme.specVersion, light: light.out, dark: dark.out }
|
||||
}
|
||||
|
||||
// A state colour drawn on the inverse surface (a snackbar's icon) is the other theme's.
|
||||
for (const state of ['error', 'success', 'warning', 'info']) {
|
||||
light.out[`inverse-${state}`] = dark.out[state]
|
||||
dark.out[`inverse-${state}`] = light.out[state]
|
||||
}
|
||||
const standard = level(contrast)
|
||||
const medium = level(LEVELS.medium)
|
||||
const high = level(LEVELS.high)
|
||||
|
||||
process.stdout.write(JSON.stringify({
|
||||
seed: input.seed.toLowerCase(),
|
||||
variant: input.variant,
|
||||
spec: light.scheme.specVersion,
|
||||
contrast,
|
||||
light: light.out,
|
||||
dark: dark.out,
|
||||
spec: standard.spec,
|
||||
harmonize,
|
||||
contrast: {
|
||||
standard: contrast,
|
||||
medium: { light: medium.light, dark: medium.dark },
|
||||
high: { light: high.light, dark: high.dark },
|
||||
},
|
||||
light: standard.light,
|
||||
dark: standard.dark,
|
||||
}))
|
||||
|
||||
+6
-486
@@ -20,12 +20,9 @@
|
||||
* compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MaterialShapes.kt
|
||||
* compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/ShapeUtil.kt
|
||||
* compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Matrix.kt (rotateZ, scale)
|
||||
* 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/Point.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
|
||||
*
|
||||
* The RoundedPolygon construction itself (CornerRounding, Cubic, Point, RoundedPolygon, Shapes,
|
||||
* Utils) is shared with bin/loading-indicator.mjs and progress.js: see resources/js/shapes.js.
|
||||
*
|
||||
* Copyright 2022-2024 The Android Open Source Project
|
||||
*
|
||||
@@ -45,480 +42,14 @@
|
||||
import { mkdirSync, readdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { circlePolygon, length, minus, plus, point, polygonFromVertices, regularPolygon, rounding, split, star, times, turningPoints, UNROUNDED } from '../resources/js/shapes.js'
|
||||
|
||||
const OUTPUT = 'resources/svg/shapes'
|
||||
const VIEWBOX = 100
|
||||
const FILL = 96
|
||||
|
||||
const DISTANCE_EPSILON = 1e-4
|
||||
|
||||
// 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 clockwise = (a, b) => a.x * b.y - a.y * b.x > 0
|
||||
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))
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const radialToCartesian = (radius, angle) => point(Math.cos(angle) * radius, Math.sin(angle) * radius)
|
||||
const convex = (previous, current, next) => clockwise(minus(current, previous), minus(next, current))
|
||||
|
||||
// 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]
|
||||
|
||||
function straightLine(x0, y0, x1, y1) {
|
||||
return [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 isClockwise = 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) *
|
||||
(isClockwise ? 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
|
||||
|
||||
/** 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])),
|
||||
]
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
if (firstCubic === null) {
|
||||
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) })
|
||||
}
|
||||
|
||||
function circlePolygon(numVertices = 8, radius = 1) {
|
||||
const polygonRadius = radius / Math.cos(Math.PI / numVertices)
|
||||
|
||||
return regularPolygon(numVertices, { radius: polygonRadius, cornerRounding: rounding(radius) })
|
||||
}
|
||||
|
||||
function rectangle({ width = 2, height = 2, cornerRounding = UNROUNDED, perVertexRounding = null } = {}) {
|
||||
const [left, top, right, bottom] = [-width / 2, -height / 2, width / 2, height / 2]
|
||||
|
||||
@@ -529,17 +60,6 @@ function rectangle({ width = 2, height = 2, cornerRounding = UNROUNDED, perVerte
|
||||
})
|
||||
}
|
||||
|
||||
function star(numVerticesPerRadius, { radius = 1, innerRadius = 0.5, cornerRounding = UNROUNDED } = {}) {
|
||||
const vertices = []
|
||||
|
||||
for (let i = 0; i < numVerticesPerRadius; i++) {
|
||||
vertices.push(radialToCartesian(radius, (Math.PI / numVerticesPerRadius) * 2 * i))
|
||||
vertices.push(radialToCartesian(innerRadius, (Math.PI / numVerticesPerRadius) * (2 * i + 1)))
|
||||
}
|
||||
|
||||
return polygonFromVertices(vertices, { cornerRounding, center: point(0, 0) })
|
||||
}
|
||||
|
||||
// Matrix.kt (rotateZ, scale) and ShapeUtil.kt (RoundedPolygon.transformed(Matrix)) --------
|
||||
|
||||
function rotateZ(degrees) {
|
||||
@@ -865,8 +385,8 @@ function pathData(polygon) {
|
||||
return `${d}Z`
|
||||
}
|
||||
|
||||
/** The geometry, for other build scripts (bin/loading-indicator.mjs); importing this module writes nothing. */
|
||||
export { point, pointOnCurve, split, cubicBounds, RoundedPolygon, SHAPES }
|
||||
/** The shape catalog, for other build scripts (bin/loading-indicator.mjs); importing this module writes nothing. */
|
||||
export { SHAPES }
|
||||
|
||||
if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
mkdirSync(OUTPUT, { recursive: true })
|
||||
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Samples M3's motion springs into the `linear()` easings of resources/css/tokens/motion.css.
|
||||
*
|
||||
* Run from the repository root with `npm run build:springs` (or `node bin/springs.mjs`).
|
||||
* Maintenance only: plain Node 22+, no dependencies, deterministic output — running it twice
|
||||
* changes nothing. Paste the blocks it prints over the spring tokens in motion.css.
|
||||
*
|
||||
* node bin/springs.mjs every spring of both schemes, over Google's web durations
|
||||
* node bin/springs.mjs --settle the same springs sampled to their own settle time instead
|
||||
* node bin/springs.mjs 0.6 800 350 one spring: damping ratio, stiffness, duration in ms
|
||||
*
|
||||
* The maths is androidx's `SpringSimulation.updateValues` (Apache-2.0, © Google LLC) for a
|
||||
* spring that starts at rest and travels from 0 to 1, which is what an easing has to describe.
|
||||
* With the natural frequency ω = √stiffness, r = −ζω and the damped frequency ωd = ω√(1 − ζ²):
|
||||
*
|
||||
* underdamped (ζ < 1) x(t) = 1 + e^(rt) · (−cos(ωd·t) + (r / ωd) · sin(ωd·t))
|
||||
* critical (ζ = 1) x(t) = 1 − (1 + ω·t) · e^(−ω·t)
|
||||
*
|
||||
* The spatial springs are underdamped and overshoot; the effects springs are critically damped
|
||||
* and never do (a colour must not overshoot). An overdamped spring (ζ > 1) is not in either
|
||||
* scheme and is not implemented.
|
||||
*
|
||||
* *Settle time* here is the first whole 10 ms after which the spring stays within 0.1 % of its
|
||||
* travel — found by scanning, because an underdamped spring crosses that band on the way to a
|
||||
* later peak that may still be outside it. It is what the spring costs in real time, not a
|
||||
* token: M3 publishes a duration per spring for the web, and that is what motion.css uses.
|
||||
*
|
||||
* *Sampling* takes the spring's value at real time `duration · i / 48` for i = 0…48 — 49 points,
|
||||
* finer than a frame at 60 Hz — and rounds to four decimals, so the physics play out in real
|
||||
* milliseconds whatever the duration is. Where the duration is past the settle time the tail is
|
||||
* flat at 1; where it falls short the last point is pinned to 1 so the animation still lands
|
||||
* exactly on its target, closing a gap of at most 0.3 % of the travel in the final frame.
|
||||
*/
|
||||
|
||||
/** Compose's two motion schemes: {Expressive,Standard}MotionTokens.kt, androidx-main @ 1608250. */
|
||||
const SCHEMES = {
|
||||
expressive: {
|
||||
'spatial-fast': { damping: 0.6, stiffness: 800 },
|
||||
'spatial-default': { damping: 0.8, stiffness: 380 },
|
||||
'spatial-slow': { damping: 0.8, stiffness: 200 },
|
||||
'effects-fast': { damping: 1, stiffness: 3800 },
|
||||
'effects-default': { damping: 1, stiffness: 1600 },
|
||||
'effects-slow': { damping: 1, stiffness: 800 },
|
||||
},
|
||||
standard: {
|
||||
'spatial-fast': { damping: 0.9, stiffness: 1400 },
|
||||
'spatial-default': { damping: 0.9, stiffness: 700 },
|
||||
'spatial-slow': { damping: 0.9, stiffness: 300 },
|
||||
'effects-fast': { damping: 1, stiffness: 3800 },
|
||||
'effects-default': { damping: 1, stiffness: 1600 },
|
||||
'effects-slow': { damping: 1, stiffness: 800 },
|
||||
},
|
||||
}
|
||||
|
||||
/** The durations M3 publishes for the web, one per spring (docs/reference/m3/styles.md § Motion). */
|
||||
const DURATIONS = {
|
||||
expressive: {
|
||||
'spatial-fast': 350,
|
||||
'spatial-default': 500,
|
||||
'spatial-slow': 650,
|
||||
'effects-fast': 150,
|
||||
'effects-default': 200,
|
||||
'effects-slow': 300,
|
||||
},
|
||||
standard: {
|
||||
'spatial-fast': 350,
|
||||
'spatial-default': 500,
|
||||
'spatial-slow': 750,
|
||||
'effects-fast': 150,
|
||||
'effects-default': 200,
|
||||
'effects-slow': 300,
|
||||
},
|
||||
}
|
||||
|
||||
/** How close to its target the spring has to stay to count as settled: 0.1 % of the travel. */
|
||||
const THRESHOLD = 0.001
|
||||
|
||||
/** The number of points a sampled easing has; 48 intervals divide every duration evenly. */
|
||||
const POINTS = 49
|
||||
|
||||
/** The spring's value `ms` after it starts at rest and travels from 0 to 1. */
|
||||
function spring({ damping, stiffness }, ms) {
|
||||
const naturalFreq = Math.sqrt(stiffness)
|
||||
const t = ms / 1000
|
||||
|
||||
if (damping > 1) {
|
||||
throw new Error(`Overdamped springs (damping ${damping}) are not in either M3 scheme.`)
|
||||
}
|
||||
|
||||
if (damping === 1) {
|
||||
return 1 - (1 + naturalFreq * t) * Math.exp(-naturalFreq * t)
|
||||
}
|
||||
|
||||
const r = -damping * naturalFreq
|
||||
const dampedFreq = naturalFreq * Math.sqrt(1 - damping ** 2)
|
||||
|
||||
return 1 + Math.exp(r * t) * (-Math.cos(dampedFreq * t) + (r / dampedFreq) * Math.sin(dampedFreq * t))
|
||||
}
|
||||
|
||||
/**
|
||||
* The first whole 10 ms after which the spring never leaves the 0.1 % band again.
|
||||
*
|
||||
* Scanned in 0.05 ms steps up to where the decay envelope alone is inside the band (for the
|
||||
* critically damped branch, √e times its half-life is a safe bound on the same idea), since the
|
||||
* band can be re-entered and left again while the spring rings down.
|
||||
*/
|
||||
function settleTime(constants) {
|
||||
const naturalFreq = Math.sqrt(constants.stiffness)
|
||||
const decay = constants.damping * naturalFreq
|
||||
const bound = ((Math.log(1 / THRESHOLD) + 10) / decay) * 1000
|
||||
let last = 0
|
||||
|
||||
for (let ms = 0; ms <= bound; ms += 0.05) {
|
||||
if (Math.abs(1 - spring(constants, ms)) >= THRESHOLD) {
|
||||
last = ms
|
||||
}
|
||||
}
|
||||
|
||||
return Math.ceil(last / 10) * 10
|
||||
}
|
||||
|
||||
/** Where an underdamped spring overshoots furthest: half a period of its damped oscillation. */
|
||||
function peak(constants) {
|
||||
if (constants.damping >= 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
const dampedFreq = Math.sqrt(constants.stiffness) * Math.sqrt(1 - constants.damping ** 2)
|
||||
const ms = (Math.PI / dampedFreq) * 1000
|
||||
|
||||
return { ms: Math.round(ms), value: spring(constants, ms) }
|
||||
}
|
||||
|
||||
/** The spring as a `linear()` easing of 49 points over `duration` ms. */
|
||||
function sample(constants, duration) {
|
||||
const points = Array.from({ length: POINTS }, (_, i) => {
|
||||
const value = spring(constants, (duration * i) / (POINTS - 1))
|
||||
|
||||
return Number(value.toFixed(4))
|
||||
})
|
||||
|
||||
points[POINTS - 1] = 1
|
||||
|
||||
return `linear(${points.join(', ')})`
|
||||
}
|
||||
|
||||
/** One spring as the CSS motion.css declares it: a derivation comment and the two tokens. */
|
||||
function declare(name, constants, duration) {
|
||||
const settles = settleTime(constants)
|
||||
const overshoot = peak(constants)
|
||||
const derivation = [
|
||||
`damping ${constants.damping.toFixed(1)}, stiffness ${constants.stiffness}`,
|
||||
`settles in ${settles}ms`,
|
||||
overshoot ? `peaks at ${overshoot.value.toFixed(3)} at ${overshoot.ms}ms` : null,
|
||||
`sampled over ${duration}ms`,
|
||||
].filter(Boolean)
|
||||
|
||||
return [
|
||||
` /* ${derivation.join('; ')} */`,
|
||||
` --md-sys-motion-${name}: ${sample(constants, duration)};`,
|
||||
` --md-sys-motion-${name}-duration: ${duration}ms;`,
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const toSettle = args.includes('--settle')
|
||||
const numbers = args.filter((argument) => !argument.startsWith('--')).map(Number)
|
||||
|
||||
if (numbers.length >= 2) {
|
||||
const constants = { damping: numbers[0], stiffness: numbers[1] }
|
||||
|
||||
process.stdout.write(`${declare('custom', constants, numbers[2] ?? settleTime(constants))}\n`)
|
||||
} else {
|
||||
const blocks = Object.entries(SCHEMES).map(([scheme, springs]) =>
|
||||
[
|
||||
`/* ${scheme} */`,
|
||||
...Object.entries(springs).map(([name, constants]) =>
|
||||
declare(name, constants, toSettle ? settleTime(constants) : DURATIONS[scheme][name]),
|
||||
),
|
||||
].join('\n'),
|
||||
)
|
||||
|
||||
process.stdout.write(`${blocks.join('\n\n')}\n`)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Builds resources/dist/, the stylesheets the package serves on its own, outside an application's
|
||||
* Vite build: the showcase's (`ShowcaseAssetController`) and the error page's
|
||||
* (`ErrorPage::layoutStyles()`, `ErrorPage::fallbackStyles()`).
|
||||
*
|
||||
* Rebuild with `npm run build:stylesheets` after changing anything under resources/css; the
|
||||
* output is committed so applications need no Node, and tests/Feature/StylesheetsBundleTest.php
|
||||
* fails while it is stale. `node bin/stylesheets.mjs <dir>` writes the same files elsewhere.
|
||||
*
|
||||
* The `@import`s are inlined by Vite's own `preprocessCSS()` — postcss-import, the same inlining an
|
||||
* application's build does, each file once, first occurrence kept — so these bundles cannot
|
||||
* order a rule differently from an application's. A relative `url()` is rebased onto
|
||||
* resources/dist/, the folder the showcase serves beside the fonts and SVGs. The fallback drops
|
||||
* every `@font-face`: without a build nothing serves the font, and the brand typeface's stack
|
||||
* (tokens/type.css) falls back to the system fonts on its own.
|
||||
*/
|
||||
import { mkdir, writeFile } from 'node:fs/promises'
|
||||
import { dirname, relative, resolve } from 'node:path'
|
||||
import { preprocessCSS, resolveConfig } from 'vite'
|
||||
|
||||
const css = resolve(import.meta.dirname, '../resources/css')
|
||||
const dist = resolve(import.meta.dirname, '../resources/dist')
|
||||
const out = resolve(process.argv[2] ?? dist)
|
||||
|
||||
const BUNDLES = {
|
||||
'showcase.css': ['all.css', 'showcase.css'],
|
||||
'error-page.css': ['components/error-page.css'],
|
||||
'error-page-fallback.css': ['foundation.css', 'components/error-page.css'],
|
||||
}
|
||||
|
||||
const rebase = {
|
||||
postcssPlugin: 'livewire-material',
|
||||
OnceExit(root, { result }) {
|
||||
// The prose lives in resources/css; the bundle only has to be served.
|
||||
root.walkComments((comment) => comment.remove())
|
||||
|
||||
root.walkDecls((decl) => {
|
||||
decl.value = decl.value.replace(/url\((['"]?)(?![a-z][a-z0-9+.-]*:|\/|#)([^'")]+)\1\)/gi, (_, quote, url) =>
|
||||
`url(${quote}${relative(dist, resolve(dirname(decl.source.input.file), url))}${quote})`)
|
||||
})
|
||||
|
||||
if (result.opts.from.endsWith('error-page-fallback.css')) {
|
||||
root.walkAtRules('font-face', (rule) => rule.remove())
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const config = await resolveConfig({ configFile: false, logLevel: 'error', css: { postcss: { plugins: [rebase] } } }, 'build')
|
||||
|
||||
await mkdir(out, { recursive: true })
|
||||
|
||||
for (const [name, files] of Object.entries(BUNDLES)) {
|
||||
const entry = files.map((file) => `@import './${file}';`).join('\n')
|
||||
// The entry is named after the bundle, beside the files it imports, but is never read from disk.
|
||||
const { code } = await preprocessCSS(entry, `${css}/bundle-${name}`, config)
|
||||
|
||||
await writeFile(`${out}/${name}`, `${code.trim()}\n`)
|
||||
}
|
||||
+3
-7
@@ -11,8 +11,7 @@
|
||||
"components",
|
||||
"material",
|
||||
"material-design",
|
||||
"material-3",
|
||||
"tailwindcss"
|
||||
"material-3"
|
||||
],
|
||||
"homepage": "https://gitea.nonameweb.ch/noNameWEB/livewire-material",
|
||||
"authors": [
|
||||
@@ -30,8 +29,7 @@
|
||||
"laravel/pint": "^1.32",
|
||||
"orchestra/testbench": "^11.2",
|
||||
"pestphp/pest": "^5.1",
|
||||
"pestphp/pest-plugin-browser": "^5.0",
|
||||
"pestphp/pest-plugin-laravel": "^5.0"
|
||||
"pestphp/pest-plugin-browser": "^5.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -63,9 +61,7 @@
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"@build",
|
||||
"@php vendor/bin/testbench serve --ansi"
|
||||
],
|
||||
"lint": "pint",
|
||||
"test": "pest"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true,
|
||||
|
||||
@@ -28,12 +28,47 @@ return [
|
||||
| localStorage under 'storage_key'; values found under 'legacy_keys' (an
|
||||
| earlier theme toggle's key) are adopted once and then removed.
|
||||
|
|
||||
| 'meta' keeps <meta name="theme-color"> (the colour an installed web app
|
||||
| or a mobile browser gives its bar) on the resolved theme's surface, and
|
||||
| the active colour profile's: the head script sets it before the first
|
||||
| paint, adds one when the page has none, and follows every later change,
|
||||
| wire:navigate included. A theme-color meta with a `media` attribute is
|
||||
| left alone.
|
||||
|
|
||||
| 'contrast' is M3's contrast level, the same three the scheme is generated
|
||||
| in: 'standard', 'medium' (3:1) or 'high' (7:1), or 'system' to follow the
|
||||
| operating system's own contrast setting until the visitor chooses. The
|
||||
| head script writes it to <html data-contrast> before the first paint —
|
||||
| standard, having the plain blocks, writes no attribute.
|
||||
|
|
||||
*/
|
||||
|
||||
'theme' => [
|
||||
'default' => 'system',
|
||||
'storage_key' => 'material-theme',
|
||||
'legacy_keys' => [],
|
||||
'meta' => false,
|
||||
'contrast' => [
|
||||
'default' => 'system',
|
||||
'storage_key' => 'material-contrast',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Motion
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| M3's two motion schemes. 'expressive' (the default) is the bouncy one the
|
||||
| library draws with; 'standard' is the restrained set, "minimal bounce,
|
||||
| for utilitarian products" — the head script writes it to
|
||||
| <html data-motion> before the first paint, and the spatial springs swap.
|
||||
| The effects springs are the same in both.
|
||||
|
|
||||
*/
|
||||
|
||||
'motion' => [
|
||||
'scheme' => 'expressive',
|
||||
],
|
||||
|
||||
/*
|
||||
@@ -72,10 +107,11 @@ return [
|
||||
| Pagination
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Draw Laravel's and Livewire's paginators in M3: the package's views are
|
||||
| put in front of `pagination::tailwind` and `livewire::tailwind` (and
|
||||
| their simple versions). An application's own published pagination views
|
||||
| still win.
|
||||
| Draw Laravel's and Livewire's paginators in M3: the package's views
|
||||
| become `Paginator::defaultView()`/`defaultSimpleView()`, and
|
||||
| `livewire.pagination_theme` is taken over while it is still Livewire's
|
||||
| own default. An application's own published pagination views still
|
||||
| win, and so does a theme it chose deliberately.
|
||||
|
|
||||
*/
|
||||
|
||||
@@ -113,7 +149,9 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Named schemes an installation can switch between. Each one is a 'label',
|
||||
| a 'seed' (#rrggbb), a 'variant' and an optional 'contrast'. Without a
|
||||
| a 'seed' (#rrggbb), a 'variant' and an optional 'contrast'; 'spec'
|
||||
| ('2025' or '2021') and the 'success', 'warning' and 'info' sources are
|
||||
| optional too, taken from the command's options when left out. Without a
|
||||
| seed, `php artisan material:scheme` generates every profile into one
|
||||
| stylesheet keyed by <html data-scheme>. 'profile' names the default one
|
||||
| (else the first); the application says which is active with
|
||||
@@ -154,9 +192,16 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Every component in every variant, rendered in the application's own
|
||||
| scheme. Off unless the application runs locally. 'vite' names the entry
|
||||
| points that import this package's CSS and JavaScript; the error pages
|
||||
| load them too, showcase or not.
|
||||
| scheme. Off unless the application runs locally. 'vite' names the
|
||||
| application's own entry points: its JavaScript (Livewire, Alpine) and
|
||||
| the CSS entry that imports this package's stylesheets. The showcase's
|
||||
| pages pass @vite() only the entries that are not a stylesheet: their
|
||||
| CSS is a bundle of their own (all.css, showcase.css and the scheme,
|
||||
| served by ShowcaseAssetController), so they need nothing from the
|
||||
| application's build. The error pages pass this whole list, CSS
|
||||
| included, to @vite() (ErrorPage::assets()) for the foundation, scheme
|
||||
| and font, and inline their own layout's rules beside it
|
||||
| (ErrorPage::layoutStyles()), so the entry needs no import for them.
|
||||
|
|
||||
*/
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Material Design 3 reference (extracted 2026-09-13)
|
||||
|
||||
What Google's documentation says, condensed for auditing this library against it. Every file
|
||||
keeps the wording of the site where a rule is stated, the tables verbatim, and a source URL per
|
||||
section. Numbers the site only draws in pictures were taken from androidx Compose Material 3's
|
||||
token files (`compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/`)
|
||||
and the Material Components Android component docs, and are marked with their source.
|
||||
|
||||
| File | Covers |
|
||||
|---|---|
|
||||
| `foundations.md`, `foundations-supplement.md` | accessibility, content design, customisation, design tokens, interaction (gestures, inputs, selection, states), layout (window size classes, scaffold, grids and spacing, canonical layouts, RTL), usability, glossary |
|
||||
| `styles.md`, `styles-supplement.md` | colour (roles, system, dynamic, custom colours, contrast), elevation, icons, motion (Expressive springs, easing and duration, transitions), shape (corner scale, shapes, morph), spacing, typography |
|
||||
| `components-actions-communication-containment.md` | buttons, icon buttons, button groups, split button, segmented buttons, FAB, extended FAB, FAB menu, badges, progress indicators, loading indicator, snackbar, tooltips, bottom sheets, cards, carousel, dialogs, divider, lists, side sheets, menus |
|
||||
| `components-navigation-selection-inputs.md` | app bars, navigation bar, navigation rail, navigation drawer, tabs, toolbars, search, checkbox, chips, radio button, sliders, switch, text fields, date pickers, time pickers, data tables |
|
||||
| `pages.txt` | the 238 pages under `/foundations`, `/styles` and `/components` in the site's sitemap (watches and XR left out) — the coverage checklist; every one was fetched |
|
||||
|
||||
## Refreshing
|
||||
|
||||
m3.material.io renders only in a browser. Read it through a reader proxy, one page at a time
|
||||
(it rate-limits with HTTP 429; retry after a few seconds; add `-H "x-no-cache: true"` for a
|
||||
stale snapshot):
|
||||
|
||||
```bash
|
||||
curl -sL "https://m3.material.io/sitemap.xml" | grep -o '<loc>[^<]*' | sed 's/<loc>//' # the page list
|
||||
curl -sL "https://r.jina.ai/https://m3.material.io/components/buttons/specs" # one page
|
||||
```
|
||||
|
||||
## Attribution
|
||||
|
||||
The text is Google's, from https://m3.material.io, reproduced here so the library can be
|
||||
checked against it; the token values are from the Android Open Source Project (Apache-2.0, see
|
||||
`NOTICE`). Google publishes its design documentation under the Creative Commons Attribution 4.0
|
||||
licence "except as otherwise noted"; confirm the statement in the site's footer before
|
||||
reproducing these files anywhere else. `docs/` is export-ignored, so they are not part of the
|
||||
Composer distribution.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,355 @@
|
||||
# Material Design 3 — Foundations reference (supplement)
|
||||
|
||||
Covers 27 sitemap URLs under `foundations/*` that `reference-foundations.md` did not fetch under their exact
|
||||
path (per-tab sub-pages the earlier pass reached only via a parent tab, plus a few pages the earlier pass
|
||||
never visited at all). Only content **not already in `reference-foundations.md`** is written out in full;
|
||||
pages that turned out to duplicate material already in the reference say so in one line instead of repeating
|
||||
it. Same structure as the reference: Principles / Rules / Values tables / M3 vs M3 Expressive /
|
||||
Accessibility / Source URLs, grouped by topic.
|
||||
|
||||
---
|
||||
|
||||
## Accessibility — assistive technology, designing, writing
|
||||
|
||||
### Duplicates (no new content)
|
||||
- `foundations/designing/overview` — restates the reference's "Do use native elements / Don't use
|
||||
non-standard elements" dialog example verbatim. One new framing sentence: the designing tabs
|
||||
(Accessibility markup, Implementing accessibility, ...) exist to **"translate a visual UI into a
|
||||
text-based, linear user experience that maps to code."** The page only fully rendered two of its stage
|
||||
tabs (Accessibility markup, Implementing accessibility); a third stage implied by "three stages" was not
|
||||
captured in this fetch.
|
||||
|
||||
### New — Assistive technology (foundations/overview/assistive-technology)
|
||||
Three assistive-technology categories, quoted:
|
||||
- **Keyboard**: "Hardware or software directional controllers such as keyboards, a D-pad, or trackball allow
|
||||
users to jump from selection to selection in a linear fashion."
|
||||
- **Screen readers**: "A screen reader is a software program that uses either a braille display or reads
|
||||
text aloud, such as Google's screen reader, TalkBack." Verbalizes visible content (paragraph/button text)
|
||||
**and** hidden content (alt text for icons, headings). Used by people with vision impairments, reading
|
||||
difficulty, or temporary reading limitations.
|
||||
- **Switch input**: "Switches scan the items on your screen, highlighting each item in turn, until you make
|
||||
a selection. Switch Access lets you interact with your Android device using one or more switches instead
|
||||
of the touchscreen."
|
||||
|
||||
### New — Writing best practices, beyond alt text (foundations/writing/best-practices)
|
||||
The reference already covers alt text length/rules; this page adds three more image-text mechanisms:
|
||||
- **Adjacent text**: text in/around an image should present key information about the image (distinct from
|
||||
alt text and captions).
|
||||
- **Captions**: text that appears below an image, explaining how it relates to the content; benefits both
|
||||
sighted and screen-reader users (screen readers **do** read captions, unlike embedded image text).
|
||||
- **Embedded text in images**: "Screen readers are unable to read text that is embedded in imagery." Any
|
||||
essential information embedded as image text must be duplicated into the alt text instead. Caution flagged
|
||||
explicitly for this case.
|
||||
- Alt text is also framed as an SEO benefit ("Keywords can also improve search engine optimization").
|
||||
|
||||
### New — Text resizing (foundations/writing/text-resizing)
|
||||
**Requirement**: UIs must support a minimum text increase of **200%**.
|
||||
|
||||
**Scaling rules** — most components:
|
||||
- Text and line height scale up proportionally, multiplied by the scale value.
|
||||
- Padding remains constant at **1x** the default size (e.g., a button keeps 8dp top/bottom and 24dp
|
||||
left/right padding at 1x, 1.3x, and 2x text scale).
|
||||
- Spacing between elements in a component remains constant at 1x.
|
||||
- Components with no text (progress indicators, checkboxes, radio buttons) are **not** affected by text
|
||||
resizing — don't resize them.
|
||||
|
||||
**Multiplier calculation** (when the OS doesn't drive resizing): offer multipliers (e.g. 1.5x, 2x);
|
||||
`font size = default font size (density 0) × scale value`. Example: 14pt at 1x → 28pt at 2x scale.
|
||||
|
||||
**Methods to handle large type** (avoids clipping, overlap, unwanted truncation):
|
||||
| Method | Rule |
|
||||
| --- | --- |
|
||||
| Increase container size | Let containers grow so text doesn't overlap/clip/truncate; design for line-to-line eye tracking |
|
||||
| Reflow the layout | Stack components (e.g. buttons) that were side-by-side, rather than keeping them fixed-width |
|
||||
| Enable content to scroll | Prefer vertical scrolling only — never ask users to scroll both vertically and horizontally |
|
||||
| Touch & hold tooltips | For space-constrained components — **top app bar, navigation bar, navigation rail, and fixed (non-scrolling) tabs** — keep the component's own text at 1x and show the full scaled-up label in a tooltip on touch & hold |
|
||||
|
||||
### New — Text truncation (foundations/writing/text-truncation)
|
||||
**Requirement**: "Content, understandability, and functionality must not be lost when users modify their
|
||||
type settings." (Exceptions may apply for non-Latin-alphabet languages.)
|
||||
|
||||
- **Text wrapping**: wrap text when it's critical, needed for understandability, or space allows. **Don't**
|
||||
cut off wrapped text without a way to see the rest (e.g., an expand affordance).
|
||||
- **Height/width of components**: prefer flexible containers that grow to fit content over fixed text-size
|
||||
limits that don't fit the available space.
|
||||
- **Ellipsis + hover/link**: truncating to an ellipsis is acceptable **only** if the full text is reachable
|
||||
via a tooltip or a link that reveals what was truncated. An ellipsis with no reveal mechanism "is not
|
||||
accessible." Note: this pattern still adds difficulty for some users, so use it deliberately.
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/overview/assistive-technology
|
||||
https://m3.material.io/foundations/designing/overview
|
||||
https://m3.material.io/foundations/writing/best-practices
|
||||
https://m3.material.io/foundations/writing/text-resizing
|
||||
https://m3.material.io/foundations/writing/text-truncation
|
||||
|
||||
---
|
||||
|
||||
## Content design — global writing, style guide
|
||||
|
||||
### Duplicates (no new content)
|
||||
- `foundations/content-design/global-writing/overview` — same definition of global writing already quoted
|
||||
in the reference. One new line: **"This guidance is for all American English that is localized."**
|
||||
- `foundations/content-design/style-guide/ux-writing-best-practices` — this is the exact content the earlier
|
||||
pass already captured at `foundations/content-design/style-guide` (explain consequences, scannable
|
||||
words/formats, sentence case, abbreviations sparingly) — fully identical, word for word.
|
||||
|
||||
### New — Grammar and punctuation (foundations/content-design/style-guide/grammar-and-punctuation)
|
||||
Not covered anywhere in the reference. Rules, quoted/paraphrased:
|
||||
- **Skip periods on single sentences** — omit in labels, tooltip text, bulleted lists, dialog body text,
|
||||
hyperlinked text. Use periods for multiple sentences, or long/complex sentences.
|
||||
- **Use contractions** — easier to scan; exception: spelled-out "do not" can read as more emphatic than
|
||||
"don't" when caution is needed.
|
||||
- **Serial (Oxford) comma** — always use it, except before an ampersand. Commas always go inside quotation
|
||||
marks.
|
||||
- **Commas in numbers**: use commas for numbers **1,000–1,000,000**. Don't use commas when identifying
|
||||
something (street address, radio frequency, year). Above 1 million, comma use depends on context;
|
||||
"million" can be abbreviated "M" with rounding when giving a sense of volume rather than an exact count
|
||||
(e.g. "23M views").
|
||||
- **Skip colons in headings** — no colon on a heading introducing a list; do use a colon for a list
|
||||
introduced within body text.
|
||||
- **Exclamation points sparingly** — fine for greetings/congratulatory moments; avoid for empty states and
|
||||
routine tasks ("save it for bigger accomplishments").
|
||||
- **Ellipses sparingly** — indicate an in-progress action or incomplete text (check with engineering, since
|
||||
truncation ellipses are often automatic). No space before an ellipsis. Omit ellipses from menu items or
|
||||
buttons that open a dialog or start a process.
|
||||
- **Parentheses** — only to define acronyms/jargon or cite a source; never for an aside/afterthought (fold
|
||||
the info into the sentence instead).
|
||||
- **Ampersands**: "&" is acceptable in headlines, column headers, table headers, navigation labels, and
|
||||
buttons (space-constrained contexts) — but spelling out "and" improves readability where there's room.
|
||||
Always spell out "and" in sentences/paragraphs, before the final item in a 3+ item list, and in email
|
||||
subject lines.
|
||||
- **Dashes** — three kinds: em dash (—), en dash (–), hyphen (-). Avoid em dashes in UX writing (replace with
|
||||
a comma, period, or new sentence). Use an en dash with no surrounding spaces for a range (e.g. "9 AM–Noon").
|
||||
- **Hyphens** — bind closely related words; also represent negative numbers (e.g. "-100"); never surrounded
|
||||
by spaces. Defer to AP style when unsure whether a phrase needs one. Hyphenation table:
|
||||
| Rule | Examples | Why |
|
||||
| --- | --- | --- |
|
||||
| Hyphenate adjective phrases | case-by-case basis, best-in-class performance, once-in-a-lifetime opportunity | Multiple words used together as one adjective are hyphenated — but not proper nouns or common phrases ("cell phone number", "chocolate chip cookie") |
|
||||
| Hyphenate noun phrases | sign-off, drive-through, go-ahead (verb+adverb); higher-up, most-read (adjective-as-noun); jack-of-all-trades, stick-in-the-mud (fixed idioms) | Certain multi-word nouns are always hyphenated |
|
||||
| Don't hyphenate verb phrases | "look out for falling rocks," "drop off your car," "check in to the room" | A verb+adverb/preposition acting as a verb isn't hyphenated (contrast the noun "check-in"); note "check into" ≠ "check in to" |
|
||||
- **Italics sparingly** — hard to read; use bold for emphasis instead. Italics are acceptable for a single
|
||||
word/phrase (a name or title), never a full sentence.
|
||||
- **No caps blocks** — "Avoid using caps blocks altogether; they're not accessible." Use sentence case for
|
||||
all product text.
|
||||
|
||||
### New — Word choice: pronouns (foundations/content-design/style-guide/word-choice)
|
||||
Note: this is a **different** page from the already-covered `global-writing/word-choice` (which is about
|
||||
localization word choice). This one is about person/pronoun choice:
|
||||
- **Use second person ("you"/"your")** — makes the UI feel like it's speaking to the user about their own
|
||||
actions; avoid impersonal/robotic phrasing.
|
||||
- **Don't mix first and second person** in the same context (e.g. "My photos" title next to "Your photos"
|
||||
description) — pick one address form, or drop the pronoun.
|
||||
- **Use caution with "I"/"we"** — for a large company like Google, first person can read as robotic or
|
||||
disconcerting; prefer the user's point of view or remove the pronoun. **Exception**: legal text may use
|
||||
"I"/"my" to emphasize ownership in an agreement (e.g. "I agree to the terms of service").
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/content-design/global-writing/overview
|
||||
https://m3.material.io/foundations/content-design/style-guide/grammar-and-punctuation
|
||||
https://m3.material.io/foundations/content-design/style-guide/ux-writing-best-practices
|
||||
https://m3.material.io/foundations/content-design/style-guide/word-choice
|
||||
|
||||
---
|
||||
|
||||
## Design tokens — usage workflow
|
||||
|
||||
### Duplicates (no new content)
|
||||
- `foundations/design-tokens/overview` — restates the reference's token definition, naming scheme,
|
||||
three-class model (ref/sys/comp), and contexts verbatim (this is the site's single `design-tokens` page,
|
||||
now also reachable at `/overview`).
|
||||
|
||||
### New — Reading a component's token spec table (foundations/design-tokens/overview)
|
||||
Not in the reference: a documented procedure for finding a token in a component's Specs tab. Tokens there
|
||||
are grouped first by **state** (enabled, disabled, hover, etc.), then by **element** (the part the token
|
||||
applies to, e.g. container or label text). Table columns:
|
||||
| Column | Meaning |
|
||||
| --- | --- |
|
||||
| Name | The style aspect the token applies to (e.g. color, font) |
|
||||
| Token ID | The token defining that style aspect |
|
||||
| Description | Optional extra info |
|
||||
| Context/value | The value stored for a given context |
|
||||
|
||||
Worked example given on the page: to verify a filled button's label-text color, go to *Common buttons* →
|
||||
*Specs*, find the filled-button token module, locate the "label text" row under elements, and copy the color
|
||||
token into code (or compare it to the Figma color role).
|
||||
|
||||
### New — How to use tokens, a Figma workflow (foundations/design-tokens/how-to-use-tokens)
|
||||
Entirely new — a step-by-step production workflow, not covered anywhere in the reference:
|
||||
1. **Install** the Material Theme Builder Figma plugin.
|
||||
2. **Generate tokens**: Plugins → Material Theme Builder → Open Plugin → Get started. Creates a
|
||||
`material-theme` artboard with baseline tonal palettes (light/dark) and a default type scale, populated
|
||||
as Figma styles.
|
||||
3. **Update token values** — two ways:
|
||||
- Via the plugin (color only): re-open the plugin, choose new colors; color/text styles update in place.
|
||||
- Via Figma styles directly: right-click a style → "Go to style definition," hover the style → the
|
||||
adjust icon (or right-click → Edit style) to change name/description/properties.
|
||||
4. **Use tokens in mockups**: apply the generated Figma styles to layout elements instead of hand-setting
|
||||
color/typography, so engineers can trust the design intent transfers.
|
||||
5. **Use tokens with the Material Design Kit**: duplicate the kit, open the plugin with kit components
|
||||
selected, and choose **Swap** to replace baseline token values with the custom generated ones.
|
||||
6. **Export tokens**: plugin's Export tab → choose a target format (Android, Jetpack Compose) → save as a
|
||||
named `.zip`.
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/design-tokens/overview
|
||||
https://m3.material.io/foundations/design-tokens/how-to-use-tokens
|
||||
|
||||
---
|
||||
|
||||
## Interaction: states (per-state one-line definitions)
|
||||
|
||||
### Duplicate, with one addition
|
||||
`foundations/interaction/states/overview` restates the "two visual indicators," "states can combine," and
|
||||
six-state list already in the reference. New here: the site's own one-line definition for each state
|
||||
(not quoted in the reference), verbatim:
|
||||
| State | Definition |
|
||||
| --- | --- |
|
||||
| Enabled | "communicates an interactive component or element" |
|
||||
| Disabled | "communicates an inoperable component or element" |
|
||||
| Hover | "communicates when a user has placed a cursor above an interactive element" |
|
||||
| Focused | "communicates when a user has highlighted an element, using an input method such as a keyboard or voice" |
|
||||
| Pressed | "communicates a user tap" |
|
||||
| Dragged | "communicates when a user presses and moves an element" |
|
||||
|
||||
### Source URL
|
||||
https://m3.material.io/foundations/interaction/states/overview
|
||||
|
||||
---
|
||||
|
||||
## Layout — adaptive design, breakpoints per class, canonical layouts
|
||||
|
||||
### Duplicates (no new content)
|
||||
- `foundations/layout/layout-overview/overview` — same 7-parts-of-layout figure and Layout terms glossary
|
||||
already quoted in the reference (this is the site's `layout-overview` tab, now also at `/overview`).
|
||||
- `foundations/layout/scaffold/overview` — same Bars/Rails/Panes summary already in the reference.
|
||||
- `foundations/layout/grids-spacing/overview` — same "grids adapt across breakpoints / spacing groups &
|
||||
directs attention / density for data-heavy views" summary already in the reference.
|
||||
- `foundations/layout/breakpoints/overview` — same breakpoint width table, pane/navigation/communication/
|
||||
action table, five adaptation questions, and common-swappable-components table already fully quoted in
|
||||
the reference.
|
||||
- `foundations/usability/overview` and `foundations/usability/applying-m3-expressive` — **both render the
|
||||
same content** as the reference's Usability section (five NN/g usability aspects, usability-vs-
|
||||
accessibility distinction, the M3-Expressive design-tactics list — color/contrast, containment, motion,
|
||||
shape & shape morph, size, typography — and "design based on primary goals"). No new facts on either page.
|
||||
|
||||
### New — Adaptive design, defined (foundations/layout/layout-overview/adaptive-design)
|
||||
Not covered in the reference at all — this is the site's actual definition of the "adaptive design" term
|
||||
the reference only flagged as a May-2026 rename target:
|
||||
- **Adaptive vs. responsive**: "While responsive design scales a single layout to fit any screen, adaptive
|
||||
design customizes a product to optimize the experience on each device." Adapts to **People** (preferences/
|
||||
settings), **Devices** (watch, phone, foldable, tablet, desktop, XR), and **Usage** (window resize,
|
||||
orientation, device switch).
|
||||
- **Three primary experience types** — design starts at mobile and scales up to spatial; all three should
|
||||
account for touch, pointer, and physical-keyboard input regardless of the device's default input:
|
||||
| Experience | Notes |
|
||||
| --- | --- |
|
||||
| Mobile | Phones, foldables, tablets. Window modes: **full-screen** (default), **split-screen** (2+ apps sharing the screen), **bubbles** (floating multitask windows) |
|
||||
| Desktop | Free-form windows adapting across breakpoints; split-screen/floating/free-form multitasking. A tablet becomes a desktop experience once a keyboard+mouse connect; an Android phone can become desktop-like via an external monitor |
|
||||
| Spatial (XR) | Multiple free-form windows in a "virtually limitless" screen. Android XR's immersive **"full space"** mode positions components freely in 3D. A navigation rail can become an **Orbiter**, floating beside the main pane (ties to the glossary's XR "Orbiter" term already in the reference, now with a concrete use case) |
|
||||
- **Adaptive strategies** (named mechanism, not in the reference): as a pane/window resizes or someone
|
||||
navigates, panes may change size, enter/exit, or reorganize via three strategies: **show and hide**,
|
||||
**levitate**, and **reflow**. Panes are "the primary segments of a layout" and can change based on context.
|
||||
- **Pane display styles**, with sharper definitions/examples than the reference's terse list:
|
||||
- **Co-planar**: panes displayed side by side.
|
||||
- **Floating**: a pane displayed above other panes/content, "like a dialog."
|
||||
- **Docked**: a pane displayed above other content where one edge extends beyond a screen edge, "like a
|
||||
bottom sheet."
|
||||
- Compose's **Navigation 3** library lets multiple destinations show on screen simultaneously and adapts
|
||||
layouts across window sizes/screens automatically.
|
||||
- **Adapting components** — three component-level adaptation strategies:
|
||||
| Strategy | Example given |
|
||||
| --- | --- |
|
||||
| Resizing | A button may scale with its parent container, or "hug" its content and stay left/right-aligned |
|
||||
| Showing & hiding | A list item can reveal a description/extra info as its container scales up |
|
||||
| Presentation changes | A FAB can become an extended FAB; a navigation rail can auto-expand as the window grows |
|
||||
|
||||
### New — Breakpoints, per window-size class (foundations/layout/breakpoints/{compact,medium,expanded,large-extra-large})
|
||||
The reference only has the aggregate breakpoint table; these per-class pages add concrete margins, pane
|
||||
widths, and navigation placement the reference doesn't state:
|
||||
|
||||
| Breakpoint | Width | Margins/spacer | Panes | Navigation | Notes |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Compact | <600dp | **16dp** margins | 1 (always) | Navigation bar or modal expanded navigation rail, placed close to the screen edge for reach | Must transition dynamically on: foldable unfold, portrait→landscape rotation, exiting split-screen, multi-window resize larger, free-form resize |
|
||||
| Medium | 600–839dp | **24dp** margins and spacer | 1 recommended; 2 only for low-density content (e.g. settings) — each pane defaults to 50% width, don't set custom widths; a drag handle can expand either pane to 100% | Single-pane: navigation rail (can hide on secondary destinations if a back button still reaches the primary destination); two-pane: navigation bar (to keep panes at full width) | Reachability: top 25% of screen is hard to reach on horizontal tablets/unfolded foldables — limit interactions there; page defines 3 ergonomic regions (inconvenient reach / comfortable / challenging near bottom edge). Transition triggers: foldable fold, landscape→portrait rotation, full-screen→split-screen, multi-window start, free-form resize |
|
||||
| Expanded | 840–1199dp | **24dp** margins and spacer | 1 (only for dense content like video) or 2 (recommended); fixed-and-flexible layout's fixed pane defaults to **360dp**; a split-pane layout's two flexible panes visually center the spacer, with nav rail + first pane totaling 50% | Navigation rail (collapsed or expanded); hideable on secondary destinations with a back button; use tabs/other in-pane components for secondary/sort/filter nav | Transition triggers: foldable fold, landscape→portrait rotation, full-screen→split-screen, multi-window start, free-form resize |
|
||||
| Large (1200–1599dp) & Extra-large (1600dp+) | — | **24dp** margins and spacer | 2 recommended (1 only for dense/media content); fixed-and-flexible fixed pane defaults to **412dp**; extra-large only can add a **standard side sheet as a 3rd pane** (side sheet max width 400dp even though 412dp is the general fixed-pane recommendation) — never exceed 3 panes | Navigation rail, collapsed or expanded depending on content volume; expanded rail suits extra-large best, collapse when space is needed or deep in the hierarchy | Large/extra-large are "most useful for...laptop and desktop devices"; some products may skip them entirely. Watch line length/typography at this size. Transition triggers: full-screen→split-screen, multi-window start, free-form resize |
|
||||
|
||||
### New — Canonical layout examples, in detail (foundations/layout/canonical-examples/{overview,feed,list-detail,supporting-pane})
|
||||
The reference only had a stub (three names + "not fetched, outside foundations/*"); these are the real
|
||||
`foundations/layout/canonical-examples/*` pages (distinct from the `m3/pages/canonical-examples/*` URLs the
|
||||
reference declined to follow), each with compact/medium/expanded configurations:
|
||||
|
||||
**Overview**
|
||||
- Three canonical layouts exist: **feed, list-detail, supporting pane** — each with compact/medium/expanded
|
||||
configurations, meant as a starting point rather than a fixed template.
|
||||
- Supporting-pane split defined precisely: primary/focus area "typically about two-thirds" of the window;
|
||||
secondary/supporting area takes the remaining third.
|
||||
- **Advanced custom layouts**: build on a canonical example or layer scaffold elements directly. The
|
||||
**"layering"** technique uses the **levitate** adaptive strategy — a pane floats above other content for a
|
||||
focused, task-oriented moment (examples given: reviewing a shopping basket, responding to comments,
|
||||
creating a calendar event).
|
||||
- Availability: Figma M3 Design Kit, Jetpack Compose canonical-layouts sample, Android Views (MDC-Android)
|
||||
canonical-layouts sample.
|
||||
|
||||
**Feed**
|
||||
- Use case: grid of cards/lists for quick browsing/discovery of large content volumes (news, photos, social
|
||||
media); grids adapt from single- to multi-column at any size.
|
||||
- Dividing space: flexible enough to mix proportions/sizes (e.g. small + large cards together) to signal
|
||||
relationships between items; items reflow on rotation/unfold/multi-window, and item **order is determined
|
||||
by position**.
|
||||
- Across breakpoints: **Compact** — stack vertically, one card per row, full pane width. **Medium** — split
|
||||
into multiple equal-width columns (example shown: 4 columns). **Expanded/large/extra-large** — more
|
||||
columns than medium, and column width should usually increase as the breakpoint grows.
|
||||
|
||||
**List-detail**
|
||||
- Use case: parent-child content pairings — text message + conversation, file browser + open folder, artist
|
||||
+ album, settings + category detail, inbox + selected email.
|
||||
- Visible-panes table (breakpoint dp ranges as stated on this page):
|
||||
| Breakpoint (dp) | Visible panes |
|
||||
| --- | --- |
|
||||
| Compact (0–599) | 1 pane |
|
||||
| Medium (600–839) | 1 (recommended) or 2 |
|
||||
| Expanded (840+) | 2 |
|
||||
| Large (1200–1599) | 2 |
|
||||
| Extra-large (1600+) | 2 |
|
||||
- **Compact**: single pane only, either list or detail view at a time.
|
||||
- **Medium**: single pane for information-dense content/deep focus, **or** two-pane to browse and switch
|
||||
quickly — use a bottom navigation bar or modal navigation rail in two-pane mode to maximize horizontal
|
||||
space.
|
||||
- **Expanded/large/extra-large**: two-pane.
|
||||
- **Behavior**: a **Back button appears in the detail view only for single-pane layouts**; a **selected
|
||||
state appears in the list view only for two-pane layouts**; use explicit/implicit grouping to direct
|
||||
visual focus in two-pane layouts. Layouts transition dynamically as available space changes (e.g. rotating
|
||||
from expanded to medium collapses two panes back to one).
|
||||
|
||||
**Supporting pane**
|
||||
- Use case: secondary content that is "only meaningful in relation to" the primary content (contrast with
|
||||
list-detail's parent-child relationship — use list-detail instead if that's the actual relationship).
|
||||
Examples: productivity, document editing + commenting, content/media browsing.
|
||||
- Placement/width table:
|
||||
| Supporting-pane placement | Pane width | Breakpoint |
|
||||
| --- | --- | --- |
|
||||
| Below the focus pane | Flexible | Compact or Medium |
|
||||
| Leading or trailing side of the focus pane | Fixed, **360dp** | Expanded |
|
||||
- **Compact**: supporting pane appears below the focus pane; a bottom sheet is a good way to keep focus on
|
||||
the primary pane while still giving access to supporting info.
|
||||
- **Medium**: supporting pane appears below the focus pane (same as compact).
|
||||
- **Expanded**: supporting pane moves to the leading or trailing side of the focus pane.
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/layout/layout-overview/overview
|
||||
https://m3.material.io/foundations/layout/layout-overview/adaptive-design
|
||||
https://m3.material.io/foundations/layout/scaffold/overview
|
||||
https://m3.material.io/foundations/layout/grids-spacing/overview
|
||||
https://m3.material.io/foundations/layout/breakpoints/overview
|
||||
https://m3.material.io/foundations/layout/breakpoints/compact
|
||||
https://m3.material.io/foundations/layout/breakpoints/medium
|
||||
https://m3.material.io/foundations/layout/breakpoints/expanded
|
||||
https://m3.material.io/foundations/layout/breakpoints/large-extra-large
|
||||
https://m3.material.io/foundations/layout/canonical-examples/overview
|
||||
https://m3.material.io/foundations/layout/canonical-examples/feed
|
||||
https://m3.material.io/foundations/layout/canonical-examples/list-detail
|
||||
https://m3.material.io/foundations/layout/canonical-examples/supporting-pane
|
||||
https://m3.material.io/foundations/usability/overview
|
||||
https://m3.material.io/foundations/usability/applying-m3-expressive
|
||||
@@ -0,0 +1,748 @@
|
||||
# Material Design 3 — Foundations reference
|
||||
|
||||
Extracted verbatim (with light reformatting) from m3.material.io/foundations for auditing an M3 Expressive
|
||||
component library. Facts not present on the fetched pages are marked "(not on the site; from ...)".
|
||||
Where the site's own text is ambiguous or inconsistent, both readings are quoted rather than resolved.
|
||||
|
||||
---
|
||||
|
||||
## Accessibility (foundations/overview, /overview/principles, /designing*, /writing, /building-for-all*)
|
||||
|
||||
### Principles
|
||||
- "Accessibility by default is a core design value for Material." Accessibility requirements and goals are
|
||||
documented across component pages and guidelines.
|
||||
- Three principles for accessible design:
|
||||
- **Honor individuals** — universal defaults rarely meet everyone's needs; build in customizable features
|
||||
since a person's needs change over time.
|
||||
- **Learn before, not after** — invest in research across a wide range of abilities before defining
|
||||
solutions, rather than reacting to problems after launch.
|
||||
- **Requirements as a starting point** — treat WCAG minimums as a floor for creative solutions, not a
|
||||
ceiling (dark mode, text-to-speech, speech-to-text all originated as accommodations).
|
||||
- "Usability focuses on making products intuitive and easy to understand for everyone" while "Accessibility
|
||||
focuses on making products accessible for people with disabilities. Accessible experiences are
|
||||
perceivable, operable, understandable and robust, and support people who use assistive technology."
|
||||
(from foundations/usability)
|
||||
|
||||
### Rules (do / don't)
|
||||
- **Do** use native platform elements/semantic HTML so components automatically carry the right
|
||||
accessibility markup (e.g., a native `dialog`).
|
||||
- **Don't** use non-standard elements to fake a standard pattern (e.g., a custom div styled as a dialog) —
|
||||
it requires extra testing to work with assistive tech.
|
||||
- Disabled states **do not** need to meet contrast requirements.
|
||||
- **Landmarks (web only)** — classify page regions with ARIA landmark roles so visual structure is also
|
||||
conveyed in code. The eight landmark roles: **navigation, search, main, banner, complementary,
|
||||
contentinfo, region, form**.
|
||||
- Only one **main**, one **banner**, one **contentinfo** per page.
|
||||
- Label any landmark role that repeats (e.g., multiple `navigation` regions) with a unique, specific
|
||||
label; **all regions** should be labeled.
|
||||
- **Don't repeat the landmark role inside its own label** (e.g., a nav labeled "Navigation").
|
||||
- **Headings (web only)**:
|
||||
- Identify headings by content hierarchy, not visual styling.
|
||||
- "Headings should not skip a level, for example, don't go from H2 to H4 without using an H3."
|
||||
- Map content to H1–H6 in sequential order.
|
||||
- "A single H1 for the page title is recommended."
|
||||
- Heading level and visual prominence don't have to match.
|
||||
- **Focus order / keyboard**:
|
||||
- Default tab order should follow the DOM and "generally flows from left to right; top to bottom." Use
|
||||
this default unless a custom pattern requires otherwise.
|
||||
- Define **initial focus** for a screen and for multi-element components (dialogs, complex cards).
|
||||
- When a dialog opens, focus moves into it (to a specific interactive element); on close/cancel, focus
|
||||
returns to the element that triggered it.
|
||||
- Tab / Shift+Tab move between interactive elements (Shift+Tab reverses); arrow keys navigate within a
|
||||
component (menu items, grid cells); Enter activates a link/button or submits a focused form.
|
||||
- Grouped sub-elements can be one tab stop, navigated internally with arrow keys.
|
||||
- **Keyboard shortcuts**: use two-or-more-key combinations by default. Document all shortcuts (help page
|
||||
or tutorial). If a shortcut uses a single key, provide at least one of, in preference order:
|
||||
1. (most preferred) remap to include a non-printable key,
|
||||
2. (preferred) only activate when the relevant component is focused,
|
||||
3. (temporary only) allow turning the shortcut off.
|
||||
- **Labeling elements**:
|
||||
- Needs a label: interactive icons/buttons with no/insufficient visible text, interactive images,
|
||||
meaningful visual cues (progress bars, error states), meaningful icons/images, generic links
|
||||
("Learn more"), buttons with ambiguous/duplicate text ("Save").
|
||||
- Doesn't need a label: non-interactive UI text (read automatically), buttons with sufficient text
|
||||
("Download image").
|
||||
- **Don't include the element's role (button, menu, etc.) in the label text** — the role is added
|
||||
automatically when the proper role is assigned.
|
||||
- Decorative icons/images that add nothing for a screen-reader user should be marked decorative/hidden
|
||||
(e.g., `alt=""`).
|
||||
|
||||
### Values
|
||||
| Item | Value | Source |
|
||||
| --- | --- | --- |
|
||||
| Large text/graphics contrast (≥14pt bold / ≥18pt regular) | ≥ 3:1 against background | designing/color-contrast, writing |
|
||||
| Small text contrast | ≥ 4.5:1 against background | designing/color-contrast, writing |
|
||||
| Clustered non-text elements (e.g. a group of buttons) vs. background | ≥ 3:1 recommended | designing/color-contrast |
|
||||
| Standalone elements (e.g. a single FAB) vs. background | not required to meet 3:1 (prominence already differentiates them) | designing/color-contrast |
|
||||
| Disabled-state contrast | not required to meet contrast requirements | designing/color-contrast, applying-states |
|
||||
| Touch target minimum | **48 × 48dp** (≈9mm physical; recommended range 7–10mm) | designing/structure |
|
||||
| iOS touch target note | iOS recommends 44 × 44dp | designing/structure |
|
||||
| Pointer (mouse/stylus) target minimum | **44 × 44dp** | designing/structure |
|
||||
| Target spacing | ≥ 8dp between adjacent targets | designing/structure |
|
||||
| Web default target size | at least 48 × 48 **CSS pixels** | grids-spacing/density |
|
||||
| Alt text length | "up to 125 characters" (foundations/writing) **vs.** "recommended length ... is 140 characters" (content-design/alt-text) — site is internally inconsistent; both figures are quoted verbatim | writing, content-design/alt-text |
|
||||
|
||||
### Alt text rules (content-design/alt-text, foundations/writing)
|
||||
- Alt text is read in place of an image by screen readers and shown if an image fails to load.
|
||||
- **Do** write alt text that conveys context/meaning; **don't** leave the auto-generated filename as alt
|
||||
text.
|
||||
- Mark an image `alt=""` (decorative) if removing it loses no information.
|
||||
- Focus on the meaning/purpose of the image, not exhaustive visual detail.
|
||||
- **Don't start alt text with "image of"** — screen readers already announce "image".
|
||||
- Essential vs. non-essential image content: essential info must meet the 3:1 (large) / 4.5:1 (small) contrast
|
||||
above; decorative/non-essential elements are exempt.
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
- No explicit M3-vs-Expressive distinction is drawn on the Accessibility pages themselves; the WCAG-based
|
||||
contrast and target-size rules are presented as constant across both.
|
||||
|
||||
### Accessibility requirements (recap)
|
||||
- Two-indicator principle for states is asserted only as a bullet: **"States have two visual indicators to
|
||||
ensure accessibility"** (foundations/interaction/states) — the page does not elaborate further on this
|
||||
Foundations tier (see Interaction States section below for the gap this leaves).
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/overview
|
||||
https://m3.material.io/foundations/overview/principles
|
||||
https://m3.material.io/foundations/designing (tabs: Overview, Color contrast, Structure, Flow, Elements)
|
||||
https://m3.material.io/foundations/designing/color-contrast
|
||||
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/writing
|
||||
https://m3.material.io/foundations/building-for-all
|
||||
https://m3.material.io/foundations/building-for-all/user-needs (identical rendered content to /building-for-all)
|
||||
https://m3.material.io/foundations/building-for-all/co-design
|
||||
|
||||
---
|
||||
|
||||
## Content design (foundations/content-design/*)
|
||||
|
||||
### Principles
|
||||
- "UI text should be clear to anyone." Follow **Associated Press (AP) Style** unless Material notes
|
||||
otherwise.
|
||||
|
||||
### Rules (do / don't) — Style guide (content-design/style-guide)
|
||||
- **Explain consequences** in neutral, direct language; avoid alarming/condescending warnings. Do tell
|
||||
users what will happen and how to undo it; don't misrepresent consequences to influence a decision.
|
||||
- **Use scannable words and formats** — specific titles/headings people can skim.
|
||||
- **Use sentence case.** "Unless otherwise specified, use sentence-style capitalization... All text,
|
||||
including titles, headings, labels, menu items, navigation components, app bars, and buttons should use
|
||||
sentence-style capitalization." Products/branded terms may still be capitalized. **Don't use title case.**
|
||||
- **Use abbreviations sparingly.** Spell out words where possible; avoid Latin abbreviations ("e.g.",
|
||||
"etc.") — use "for example," "and more" instead.
|
||||
|
||||
### Rules — Alt text (see also Accessibility above)
|
||||
- Recommended alt text length: 140 characters (content-design/alt-text); don't start with "image of".
|
||||
|
||||
### Rules — Global writing (content-design/global-writing, /global-writing/word-choice)
|
||||
- Global writing = writing inclusively for many cultures and optimized for localization/translation.
|
||||
- **Use global examples; explain local references** — call out (in a message description for translators)
|
||||
locations, names/nicknames, currencies, temperatures, date formats, and providers (internet/cable) that
|
||||
don't translate globally.
|
||||
- Use short, simple sentences; break into bullets/sections with headings.
|
||||
- Avoid abbreviations; clarify ambiguous pronouns and "this"/"that" references.
|
||||
- Avoid idiomatic, colloquial, or overly polite expressions (translation risk).
|
||||
- Reduce technical jargon; clarify ambiguities.
|
||||
|
||||
### Rules — Notifications (content-design/notifications)
|
||||
- Notifications should: be about the user (not the product); be precise, timely, actionable, contextual,
|
||||
relevant; give easy opt-out controls; never be unsolicited ads.
|
||||
- Put the most important info first (people skim in an F-shape).
|
||||
- CTAs should be concise, specific, actionable.
|
||||
- Make notifications relevant/personal — don't broadcast generically.
|
||||
- **Avoid dynamic text**, especially in headlines (breaks character limits when translated, doesn't
|
||||
expand if truncated). If used, pair with no more than one additional word, and prepare a fallback
|
||||
notification that fits the character budget.
|
||||
- **Don't use relative day words** ("today"/"tomorrow"/"tonight") — about 20% of users don't see a
|
||||
notification the day it's sent; use the specific day of the week instead (exception: auto-dismissing
|
||||
notifications tied to a timestamp).
|
||||
- Use emoji sparingly; never to accentuate bad news (negative emoji tested poorly); don't replace words
|
||||
with emoji.
|
||||
- Don't repeat the app name/logo (already shown by the OS).
|
||||
- Don't interrupt onboarding with unrelated notifications/emails.
|
||||
- Give users in-context, clear opt-in/opt-out controls.
|
||||
|
||||
### Values
|
||||
| Element | Character limit |
|
||||
| --- | --- |
|
||||
| Notification title | < 29 characters |
|
||||
| Notification collapsed body | < 40 characters |
|
||||
| Notification expanded body | < 80 characters (start with the collapsed body and extend it) |
|
||||
| Notification buttons | 1–2 buttons, 1–2 words each |
|
||||
| SMS, Latin-script languages | < 160 characters |
|
||||
| SMS, non-Latin-script languages | < 134 characters |
|
||||
| Alt text | 140 characters recommended (content-design/alt-text) / "up to 125 characters" (foundations/writing) |
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
- Not called out on these pages; content-design guidance is presented as version-agnostic.
|
||||
|
||||
### Accessibility requirements
|
||||
- Alt text and captions are the accessibility mechanism this section owns (see Accessibility section for
|
||||
full alt-text rules); captions benefit both sighted and screen-reader users.
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/content-design/overview
|
||||
https://m3.material.io/foundations/content-design/alt-text
|
||||
https://m3.material.io/foundations/content-design/global-writing
|
||||
https://m3.material.io/foundations/content-design/global-writing/word-choice
|
||||
https://m3.material.io/foundations/content-design/notifications
|
||||
https://m3.material.io/foundations/content-design/style-guide
|
||||
|
||||
---
|
||||
|
||||
## Customization (foundations/customization)
|
||||
|
||||
### Principles
|
||||
- "M3 opens up new possibilities for both brand colors and individual color preferences to converge in
|
||||
one-of-a-kind experiences."
|
||||
- Dynamic color lets an app retain brand-defining colors while also honoring a user's device/personal color
|
||||
preference; works for custom (non-Material) components too.
|
||||
|
||||
### Rules
|
||||
- Build a **custom color scheme** with the M3 color system so the app can respect device/app-level dynamic
|
||||
color settings; always keep a fallback (non-dynamic) color scheme for users who don't enable dynamic
|
||||
color.
|
||||
- Apply dynamic color selectively if desired (e.g., only on a profile screen) rather than app-wide.
|
||||
|
||||
### Values
|
||||
- **Five essential color groups / roles**: Primary, Secondary, Tertiary, Neutral, Neutral Variant. An input
|
||||
(source) color generates a tonal palette that fills roles such as primary / on-primary / primary
|
||||
container.
|
||||
- The Material Theme Builder (Figma plugin) generates **color and type tokens**, exportable to multiple
|
||||
code formats, including Design System Package (DSP) format.
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
- Not explicitly distinguished on this page; dynamic color and the 5-role model are presented as the
|
||||
current (M3) system without an Expressive-specific callout.
|
||||
|
||||
### Accessibility requirements
|
||||
- "The color system automatically handles critical adjustments that provide accessible color contrast,
|
||||
legibility, interaction states, and component structure."
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/customization
|
||||
|
||||
---
|
||||
|
||||
## Design tokens (foundations/design-tokens)
|
||||
|
||||
Note: the task brief anticipated separate `/overview`, `/how-to-read-tokens`, `/how-to-use-tokens`
|
||||
sub-pages; the live site has a **single page** at `foundations/design-tokens` covering all of this content
|
||||
(confirmed via the site's own top-nav, which links only to `foundations/design-tokens`).
|
||||
|
||||
### Principles
|
||||
- "Design tokens are small, reusable design decisions that make up a design system's visual style. Tokens
|
||||
replace static values with self-explanatory names."
|
||||
- A token = a code-like name (e.g. `md.ref.palette.secondary90`) + an associated value (e.g. `#E8DEF8`);
|
||||
the value can be a color, typeface, measurement, or another token.
|
||||
- Tokens let design and code stay in sync: "if a designer's mock-ups and an engineer's implementation both
|
||||
reference the same token... they can be confident that the same [value] is being used in both places,"
|
||||
even after the underlying value changes.
|
||||
- Tokens should be used instead of hardcoded values whenever a design choice recurs across components.
|
||||
|
||||
### Rules
|
||||
- **Do** use design tokens instead of hardcoded values.
|
||||
- Name each token for how/where it's used (e.g. `md.comp.fab.primary.container.color` sets an FAB's
|
||||
container color).
|
||||
- **System tokens should point to reference tokens**, not static values, whenever possible.
|
||||
- **Component tokens should point to a system or reference token**, not hardcoded values, whenever
|
||||
possible.
|
||||
- Tokens are recommended when: building a design system from scratch or updating one broadly, applying a
|
||||
system across a suite of products, wanting to maintain/update styles easily, or wanting dynamic color.
|
||||
Tokens are **less** helpful for an existing app with values unlikely to change and no design system.
|
||||
|
||||
### Values — naming scheme
|
||||
| Part | Meaning | Example |
|
||||
| --- | --- | --- |
|
||||
| System prefix | Always starts with the system name | `md` (Material Design) |
|
||||
| Class | `ref` = reference, `sys` = system, `comp` = component | `md.ref…`, `md.sys…`, `md.comp…` |
|
||||
| Role/descriptor | Descriptive words for the token's purpose | `…palette.secondary90`, `…color.secondary-container`, `…fab.primary.container.color` |
|
||||
|
||||
### Values — the three token classes
|
||||
| Class | Prefix | Definition | Example |
|
||||
| --- | --- | --- | --- |
|
||||
| Reference tokens | `ref` | "All available tokens with associated values." Point to static values (or other reference tokens); don't change based on context. | `md.ref.palette.secondary90` → `#E8DEF8`; `md.ref.typeface.plain-medium` → Roboto Medium |
|
||||
| System tokens | `sys` | "Decisions and roles that give the design system its character" — theming happens here; can point to different ref tokens by context (e.g. light/dark theme). | `md.sys.color.secondary-container` → `md.ref.palette.secondary90`; `md.sys.typescale.label-medium.font` |
|
||||
| Component tokens | `comp` | "(in development)" — the design properties assigned to a specific component's elements (container, label, icon, state, size, shape, color, elevation). | Extended FAB container color / label text tokens |
|
||||
|
||||
- **Contexts**: conditions under which a token points to a non-default value — "device form factors, dark
|
||||
theme, dense layouts, and right-to-left writing systems" are the examples given. A context acts like a
|
||||
tag overriding the default token value.
|
||||
- Glossary cross-references (foundations/glossary): **Design tokens: Role** = "short nickname describing
|
||||
the purpose of a design token... Also known as slots" (e.g. "On surface", "Body 1"); **Design tokens:
|
||||
Value** = "the information defining a design attribute, either stored in a token or hard-coded."
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
- Component tokens are explicitly flagged as **"(in development)"** — i.e., not every component's tokens
|
||||
are finalized/complete as of this fetch.
|
||||
|
||||
### Accessibility requirements
|
||||
- Not directly addressed on this page; contexts (e.g. dark theme, dense layout) are the token mechanism
|
||||
used elsewhere to satisfy accessibility/adaptation requirements.
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/design-tokens
|
||||
|
||||
---
|
||||
|
||||
## Interaction: gestures, inputs, selection, states
|
||||
|
||||
### Gestures (foundations/interaction/gestures)
|
||||
|
||||
**Principles**
|
||||
- "Gestures help people navigate, take action, or transform content." UI elements should respond to
|
||||
gestures in real time. Tap, scroll, and swipe are the most common.
|
||||
|
||||
**Types (do/don't-style catalogue, no numeric specs given)**
|
||||
- Tap (navigate/interact), Double tap (zoom in/out), Long press (extra functionality / selection), Scroll
|
||||
and pan, Swipe (switch peer views like tabs, complete actions, or reveal actions on a list item), Drag,
|
||||
Pick up and move (long-press + drag to reorder), Pinch (scale), Compound gestures (e.g. pan + pinch on a
|
||||
map).
|
||||
- **Predictive back** (Android): swipe left/right to preview going back before committing. Compatible
|
||||
components: **bottom sheet, navigation bar, navigation rail, search bar, side sheet**.
|
||||
|
||||
**Source**: https://m3.material.io/foundations/interaction/gestures
|
||||
|
||||
### Inputs (foundations/interaction/inputs)
|
||||
|
||||
**Principles**
|
||||
- "Design for touch, keyboard, and mouse interactions." Embrace multiple input methods within one app.
|
||||
|
||||
**Rules / values**
|
||||
- External input devices: mouse (left/right click, wheel, extra buttons), trackpad (click, gestures,
|
||||
haptics), physical keyboard (replaces virtual keyboard; media keys; modifier keys), stylus.
|
||||
- **Hover**: rest a cursor on an interactive element to trigger its hover state (see States below) and, if
|
||||
applicable, a tooltip.
|
||||
- **Cursor shapes**: Pointer (default), Hand (links/linked images), Resize arrows (resizable element
|
||||
edges), I-beam (editable/selectable text). Text-selection click semantics: single click places cursor /
|
||||
deselects & repositions; double click selects a word; triple click selects a paragraph.
|
||||
- **Physical keyboard**: show/hide the on-screen (virtual) keyboard based on whether a physical keyboard is
|
||||
attached (do hide it when a physical keyboard connects; do show it again when disconnected). Enter
|
||||
typically sends/submits; Spacebar (or media keys) typically play/pause media.
|
||||
|
||||
**Source**: https://m3.material.io/foundations/interaction/inputs
|
||||
|
||||
### Selection (foundations/interaction/selection)
|
||||
|
||||
**Principles**
|
||||
- Selection can be shown via a check icon, a checkbox, or a surface-color change (or a combination).
|
||||
- An entire component, or only certain parts of it, can be selected. Selection can be performed by tap,
|
||||
cursor, keyboard, or voice.
|
||||
|
||||
**Values**
|
||||
- Components that **inherit selection styling**: cards, checkboxes, chips, data tables, icon buttons, list
|
||||
items, menu items, pickers, radio buttons, segmented buttons, sliders, switch.
|
||||
- Components that use an **active indicator** instead (only one selected at a time): navigation bar,
|
||||
navigation drawer, navigation rail, tabs.
|
||||
|
||||
**Rules**
|
||||
- Touch: long-press or two-finger touch (or a shortcut, e.g. tapping an avatar) enters selection mode; tap
|
||||
additional items to add them; tap each selected item (or a toolbar action) to exit selection mode.
|
||||
- Long-press + drag can batch-select — **don't** reuse this gesture combo for selection if it's already
|
||||
used to pick up/move items (e.g. reordering cards).
|
||||
- Desktop/click: when selection is the primary activity, checkboxes stay visible; when secondary, show a
|
||||
checkbox on hover for a single item, and for all items once one is selected.
|
||||
|
||||
**Source**: https://m3.material.io/foundations/interaction/selection
|
||||
|
||||
### States (foundations/interaction/states, /states/state-layers, /states/applying-states)
|
||||
|
||||
**Principles**
|
||||
- "States are visual indicators used to communicate the status of a component or interactive element."
|
||||
- **"States have two visual indicators to ensure accessibility"** — stated as a bullet on the States
|
||||
overview page; the site does **not** elaborate on this rule anywhere in Foundations (no page defines
|
||||
what the two indicators must be, e.g. color + shape/outline). This is a documentation gap to flag when
|
||||
auditing — Foundations only asserts the principle, not the mechanism.
|
||||
- States can combine (e.g. selected + hover); apply states consistently across components.
|
||||
- Six states: **Enabled, Disabled, Hover, Focused, Pressed, Dragged.**
|
||||
|
||||
**State layer mechanics (states/state-layers)**
|
||||
- "A state layer is a semi-transparent covering on an element that indicates its state," providing "a
|
||||
systematic approach to visualizing states by using opacity." Only one state layer can be applied at a
|
||||
given time; it can cover the whole element or a circular sub-region.
|
||||
- The state layer's color equals the **content's "on" color** (e.g. if the container uses **secondary
|
||||
container** and content uses **on secondary container**, the state layer uses **on secondary
|
||||
container**).
|
||||
- **"The size of state layers is 40dp while the interactive target size is 48dp."**
|
||||
|
||||
**Values — state layer / disabled opacities** (from the states/state-layers interactive module, exact
|
||||
figures)
|
||||
| State | Opacity |
|
||||
| --- | --- |
|
||||
| Hover | 0.08 (+8%) |
|
||||
| Focus | 0.10 (+10%) |
|
||||
| Pressed | 0.10 (+10%) |
|
||||
| Dragged | 0.16 (+16%) |
|
||||
| Disabled | 0.38 |
|
||||
|
||||
**Rules — per-state inheritance (applying-states)**
|
||||
| State | Inherited by | NOT inherited by |
|
||||
| --- | --- | --- |
|
||||
| Disabled | Buttons, Cards, Checkboxes, Chips, List items, Radio buttons, Switches, Text fields | App bars, Badges, Dialogs, FABs, Menus, Navigation bar/drawer/rail, Sheets, Tabs, Tooltips |
|
||||
| Hover | Buttons, Cards, Checkbox, Chips, Date/time pickers, List items, Slider, Switch, Text fields | App bars, Badges, Dialogs, Menus, Navigation bar/drawer/rail, Sheets, Tabs |
|
||||
| Focused | Buttons, Cards, Checkbox, Chips, Date/time pickers, List items, Selection controls, Text fields | App bars, Badges, Banner, Card, Dialogs, Navigation bar/drawer/rail, Sheets |
|
||||
| Pressed | Buttons, Cards, Checkbox, Chips, List items, Text fields | App bars, Badges, Bottom navigation, Dialogs, Menus, Sheets, Tabs |
|
||||
| Dragged | Cards, Chips, List items, Sliders | App bars, Badges, Buttons, Dialogs, Menus, Navigation bar/drawer/rail |
|
||||
|
||||
- **Disabled**: communicated via color change + reduced elevation; not focusable, draggable, or pressable;
|
||||
no hover/other state layer; exempt from contrast requirements; "if the action... is unavailable, the FAB
|
||||
shouldn't appear" (don't just disable it) — a Don't example specifically about FABs.
|
||||
- **Hover**: lower-emphasis overlay; appears/disappears with a "low-emphasis animated fade"; only one hover
|
||||
state active in a layout at a time; can combine with focused/activated/selected/pressed.
|
||||
- **Focused / keyboard focus indicator**: "When an element is tabbed to, it appears in its focused state
|
||||
with a ring-like keyboard focus indicator." **No thickness, offset, or color token is specified anywhere
|
||||
in Foundations** — this page only calls it "ring-like." (Exact focus-ring specs, if they exist, live in
|
||||
per-component spec pages outside the Foundations section — flag as out-of-scope-for-this-file / a gap
|
||||
for the audit.) Only one focus state active at a time; initiated by Tab (or equivalent).
|
||||
- **Pressed**: higher-emphasis than hover; a **ripple overlay** signifies press (can also/alternatively use
|
||||
an elevation change); only one pressed state active at a time; initiated by tap/click/keyboard/voice
|
||||
activation.
|
||||
- **Dragged**: lower-emphasis overlay than pressed, "to avoid distracting users from their task"; can also
|
||||
use elevation; only one dragged state active at a time; initiated by touch-and-hold + move.
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
- Not explicitly called out for interaction/states; the state-layer opacity table and inheritance lists are
|
||||
presented as the current, single spec (this appears to be the M3 Expressive-era rewrite of the classic M2
|
||||
state-layer table, but the site does not itself label old vs. new values here).
|
||||
|
||||
### Accessibility requirements
|
||||
- The "two visual indicators" principle (see above) is the accessibility hook for state changes but is left
|
||||
unelaborated in Foundations.
|
||||
- Disabled components are fully exempted from contrast requirements.
|
||||
- Keyboard operability requirements live in Accessibility → Flow (focus order, keyboard shortcuts) above,
|
||||
and cross-reference into States for the focus indicator.
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/interaction/gestures
|
||||
https://m3.material.io/foundations/interaction/inputs
|
||||
https://m3.material.io/foundations/interaction/selection
|
||||
https://m3.material.io/foundations/interaction/states/overview
|
||||
https://m3.material.io/foundations/interaction/states/state-layers
|
||||
https://m3.material.io/foundations/interaction/states/applying-states
|
||||
|
||||
---
|
||||
|
||||
## Layout (foundations/layout/*)
|
||||
|
||||
Note on the task brief's guessed slugs: the live IA does **not** use `/understanding-layout/*` or
|
||||
`/applying-layout/*`. Real structure discovered via the site's own nav/tab links:
|
||||
- `layout/layout-overview` (tabs: Overview, **Overview: Parts of layout** → `/layout-overview/parts-of-layout`)
|
||||
- `layout/scaffold` (tabs: Overview, Bars, Rails, Panes)
|
||||
- `layout/grids-spacing` (tabs: Overview, Grids, Spacing, Density)
|
||||
- `layout/breakpoints`
|
||||
- `layout/bidirectionality-rtl`
|
||||
- `layout/canonical-examples`
|
||||
|
||||
### Principles
|
||||
- "Layout is the visual and strategic arrangement of elements on a screen," used to organize elements,
|
||||
signal hierarchy, and draw attention to key actions.
|
||||
- Adapt layouts to the five breakpoints; build from an established canonical layout example; design for
|
||||
bidirectionality (LTR + RTL); apply consistent arrangement/sizing/spacing.
|
||||
- Material's components are built with **"leading"/"trailing"** (not left/right) naming so they support RTL
|
||||
by construction — "extra configuration may be needed to achieve specific RTL situations," though.
|
||||
- **May-2026 renames** (explicitly called out on the site as new terminology): **"Window size classes
|
||||
renamed to breakpoints"**; **"Responsive layout renamed to adaptive design."**
|
||||
|
||||
### Values — Layout terms glossary (layout-overview)
|
||||
- **Adaptive design**: techniques letting an interface dynamically respond to user preference, device
|
||||
type, state, and breakpoints.
|
||||
- **Bars**: frame the page (app bar, bottom navigation bar).
|
||||
- **Bidirectionality**: a writing system displaying content RTL.
|
||||
- **Breakpoints**: "opinionated window sizes where a layout changes to match available space, device
|
||||
conventions, and ergonomics (previously window size classes)."
|
||||
- **Column**: one or more vertical blocks of content within a pane.
|
||||
- **Drag handle**: resizes panes.
|
||||
- **Fold**: the flexible/hinge area separating two displays on a foldable.
|
||||
- **Gap**: space between components/elements within a container.
|
||||
- **Margin**: space between the screen edge and elements inside it.
|
||||
- **Multi-window mode**: multiple apps sharing one screen simultaneously.
|
||||
- **Pane**: "a layout container that houses other components and elements within a single app... fixed,
|
||||
flexible, floating, or semi-permanent."
|
||||
- **Rails**: perimeter space around panes holding navigation rails, toolbars, pane controls.
|
||||
- **RTL language**: e.g. Arabic, Hebrew, Farsi — used by "over 2 billion people."
|
||||
- **Rulers**: opinionated global alignment lines.
|
||||
- **Safety region**: zones reserved for system UI (status bar, gesture bar) — "shouldn't contain primary
|
||||
content."
|
||||
- **Scaffold**: "a fundamental UI design structure that provides a standard platform for assembling key
|
||||
screen components."
|
||||
- **Spacer**: space between two panes on a foldable.
|
||||
- **7 parts of layout** (numbered figure caption): Column, Fold, Margin, Bar, Drag handle, Pane, Rail.
|
||||
|
||||
### Values — Breakpoints (layout/breakpoints)
|
||||
| Breakpoint | Width (dp) | Common devices |
|
||||
| --- | --- | --- |
|
||||
| Compact | Under 600dp | Phone in portrait |
|
||||
| Medium | 600–839dp | Tablet in portrait; foldable in portrait (unfolded) |
|
||||
| Expanded | 840–1199dp | Phone/tablet in landscape; foldable in landscape (unfolded); desktop |
|
||||
| Large | 1200–1599dp | Desktop |
|
||||
| Extra-large | 1600dp+ | Desktop; ultra-wide monitors |
|
||||
|
||||
- Height breakpoints (compact/medium/expanded) also exist on Android but are rarely needed since most
|
||||
layouts scroll vertically.
|
||||
|
||||
**Breakpoint → recommended panes / navigation / communication / action** (layout/breakpoints)
|
||||
| Breakpoint | Panes | Navigation | Communication | Action |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Compact | 1 | Navigation bar, modal expanded navigation rail | Simple dialog, full-screen dialog | Bottom sheet |
|
||||
| Medium | 1 (recommended) or 2 | Navigation bar, modal expanded navigation rail | Simple dialog | Menu |
|
||||
| Expanded | 1 or 2 (recommended) | Modal or standard expanded navigation rail | Simple dialog | Menu |
|
||||
| Large | 1 or 2 (recommended) | Modal or standard expanded navigation rail | Simple dialog | Menu |
|
||||
| Extra-large | 1 to 3 (recommended) | Modal or standard expanded navigation rail | Simple dialog | Menu |
|
||||
|
||||
**Common swappable components by breakpoint** (layout/breakpoints)
|
||||
| Component type | Compact | Medium | Expanded |
|
||||
| --- | --- | --- | --- |
|
||||
| Navigation | Navigation bar | Collapsed navigation rail | Collapsed navigation rail |
|
||||
| Navigation (secondary row) | Modal expanded navigation rail | Modal expanded navigation rail | Standard expanded navigation rail |
|
||||
| Communication | Basic or full-screen dialog | Basic dialog | Basic dialog |
|
||||
| Supplemental selection | Bottom sheet | Menu | Menu |
|
||||
|
||||
- Pane-count guidance restated: **compact/medium → 1 pane; expanded/large → 2 panes recommended;
|
||||
extra-large → consider 3 panes.**
|
||||
- Five questions to ask moving up a breakpoint: what should be **revealed, divided (paned), resized,
|
||||
repositioned, swapped**. Across breakpoints, keep text to **40–60 characters per line**.
|
||||
- Component-swap example: compact navigation bar → medium/expanded navigation rail; collapsed rail
|
||||
(medium/expanded) → expanded rail (large/extra-large). **Don't** swap functionally non-equivalent
|
||||
components (e.g. a button for a menu).
|
||||
|
||||
### Values — Scaffold (layout/scaffold, /bars, /rails, /panes)
|
||||
- Scaffold = **Bars + Rails + Panes**, arranged around a **safety region** that must stay clear of primary
|
||||
content.
|
||||
- **Bars**: frame the page; contain app bar (top) or navigation bar (bottom). App bars: title + 1–2
|
||||
essential actions (search, back). "**Navigation bars let people switch between 3–5 primary UI views**"
|
||||
at compact/medium breakpoints.
|
||||
- **Rails**: the perimeter layer after bars; hold navigation rails, toolbars, chat inputs, FABs, other
|
||||
primary controls; on desktop, a leading/trailing rail; on mobile, a toolbar can float in the rail region.
|
||||
- **Panes**: all content must live in a pane; a layout has **1–3 panes**, adapting to breakpoint and
|
||||
language direction (RTL flips navigation to the right).
|
||||
- **Single-pane** layouts: any breakpoint, recommended at compact/medium.
|
||||
- **Two-pane** layouts: common at expanded/large/extra-large; fixed-and-flexible panes can appear in
|
||||
either order.
|
||||
- **Three-pane** layouts: "Fixed panes at this breakpoint are recommended to be **412dp**, but side
|
||||
sheets have a default maximum width of **400dp**."
|
||||
- **Snap widths** when resizing a two-pane layout at expanded/large/extra-large: **360dp**, **412dp**, or
|
||||
a centered split-pane with spacer.
|
||||
- **Persistent resizing** (remembers width across sessions/breakpoint changes) is the default
|
||||
recommendation for most resizable layouts; **temporary resizing** (reverts to default on
|
||||
close/reopen) is for supporting-pane layouts where resizing is uncommon.
|
||||
- Multi-pane display styles: **co-planar** (side by side — used for persistent utilities so they stay
|
||||
accessible), **floating**, **docked**.
|
||||
- **Accessibility**: coplanar-pane focus order must match visual arrangement; modal floating panes trap
|
||||
focus (move focus in on open, return it to the trigger on close) and close on outside interaction;
|
||||
non-modal floating panes let focus move freely in/out and must sit in logical reading order; docked
|
||||
panes follow the same focus rules as floating panes, plus the coplanar focus-order requirement.
|
||||
|
||||
### Values — Grids & spacing (layout/grids-spacing, /grids, /spacing, /density)
|
||||
- Grids: column count/width/spacing scale with breakpoint (compact fewer columns → expanded/foldable more
|
||||
columns); **no explicit numeric column-count or gutter table is given on the current site** (this is a
|
||||
gap versus the classic M2 12-column grid spec — flag if the component library assumes fixed column
|
||||
counts).
|
||||
- **Rulers**: Bar & safety rulers (reserve system-UI space), Title ruler (aligns app-bar title), Content
|
||||
rulers (first ruler = major blocks like hero/headline; secondary rulers = supplementary text/actions).
|
||||
Margin rulers can be widened/narrowed for expressive effect (e.g., full-bleed photo grid vs. a
|
||||
wide-margin search bar).
|
||||
- Spacing is used to **group** (explicit grouping = outlines/dividers/shadows; implicit grouping =
|
||||
proximity/whitespace only), to **direct attention** (rhythm, similarity, proximity, continuity), and as
|
||||
**expression** (generous spacing + bright surfaces on the most important content; negative space for
|
||||
emphasis). No fixed numeric spacing scale (e.g. 4/8/16dp increments) is stated on this page beyond the
|
||||
8dp target-spacing rule already listed under Accessibility.
|
||||
- **Density** (component scaling):
|
||||
- Density scale is numbered starting at **0** (default) and moves negative (**-1, -2, -3**) as space
|
||||
decreases.
|
||||
- "Higher density is typically applied by decreasing the top and bottom padding or overall height by
|
||||
**4dp**" per step.
|
||||
- Text size should not change as a container scales with density.
|
||||
- Example figures from the page: 20dp gap between a text field's label and input; a "don't" example shows
|
||||
a dropdown item shrunk to 38dp of selectable height (too dense — reduces usability).
|
||||
- **Density must be opt-in, never default**: "People should be able to **opt in** to dense layouts and
|
||||
components." Settings interactions themselves must keep default target sizes (**48×48 CSS pixels**).
|
||||
"Don't scale layouts below 48×48dp by default."
|
||||
- **Interaction targets must stay ≥48×48dp even when the visible element is scaled down** — e.g. a
|
||||
settings icon can be 24×24dp visually but must keep a 48×48dp target; a 36dp-tall button can keep a
|
||||
48dp target.
|
||||
- Don't increase density in focused/high-stakes UI (menus, snackbars, dialogs).
|
||||
- Pixel density formula: `pixel density = screen width (or height) in px / screen width (or height) in
|
||||
inches`.
|
||||
- **Density-independent pixel (dp)**: "A dp is equal to one physical pixel on a screen with a density of
|
||||
160." Formula: `dp = (width in pixels × 160) / screen density`.
|
||||
|
||||
### Values — Bidirectionality / RTL (layout/bidirectionality-rtl)
|
||||
- Over 2 billion people read/write RTL languages (Arabic, Hebrew, Farsi, Urdu).
|
||||
- **Mirroring**: LTR ⇄ RTL flips element alignment/order; reading starts top-right in RTL. **Exception**:
|
||||
graphs/charts stay LTR even in Persian and Urdu.
|
||||
- **Text rendering** has two parts: alignment (box placement) and directionality (text/element flow).
|
||||
Common RTL bugs: text entry, cursor position, punctuation, phone numbers, URLs. **Don't** reverse an
|
||||
email's username/domain order (domain always stays to the right of the username, even though the
|
||||
username itself can render RTL). **Don't** apply LTR directionality to RTL content (scrambles word
|
||||
order) — content needs both RTL alignment *and* RTL directionality.
|
||||
- **Icons**: directional icons (back/forward, send) mirror in RTL. **Exception**: in Hebrew, timelines and
|
||||
media controls keep LTR directionality; help icons mirror only in some RTL languages (e.g. Urdu,
|
||||
Persian).
|
||||
- **Time / progress**: linear progress indicators fill right-to-left in most RTL languages, **except
|
||||
Hebrew, which stays LTR**. Circular progress indicators always move clockwise regardless of direction.
|
||||
Media player controls (video/audio) are **always LTR**. Clocks always turn clockwise; on a 12-hour clock
|
||||
in RTL, the AM/PM symbol moves to the left; the 24-hour clock is common outside English-primary
|
||||
locales.
|
||||
- **Canonical layouts in RTL**: list-detail, feed, and supporting-pane layouts are all explicitly "mirrored
|
||||
in RTL."
|
||||
- **Component specifics**: badges reposition; toolbars mirror tool order; app bars mirror layout and flip
|
||||
directional icons; **navigation rail sits on the leading edge — right side in RTL, left in LTR**
|
||||
(expanded nav rail too); text field leading/trailing icons swap sides; a chip's trailing icon "is always
|
||||
aligned to the end side of the container" (right for LTR, left for RTL).
|
||||
- **Gestures**: swipe-to-reveal actions and Android predictive back must mirror their LTR counterparts
|
||||
(e.g. a delete action revealed by a right-swipe in LTR should be revealed by a left-swipe in RTL).
|
||||
|
||||
### Values — Canonical layout examples (layout/canonical-examples)
|
||||
- Named canonical layouts referenced: **feed**, **list-detail**, **supporting-pane** (each links out to a
|
||||
dedicated `m3.material.io/m3/pages/canonical-examples/...` page — a legacy/alternate URL namespace not
|
||||
fetched here since it falls outside `foundations/*`). An "Advanced custom layouts" section exists on the
|
||||
page but only points onward, with no further numeric content on this page itself.
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
- The May-2026 rename ("window size class" → "breakpoint"; "responsive layout" → "adaptive design") is the
|
||||
only explicit terminology-version marker found in Layout.
|
||||
- Density (opt-in component scaling) and the layout scaffold (bars/rails/panes model) are presented as the
|
||||
current single system, introduced/updated as part of the same May-2026 refresh per layout-overview's
|
||||
"What's new" log ("Introduced layout scaffold... Updated canonical layout examples... Spacing system").
|
||||
|
||||
### Accessibility requirements
|
||||
- Safety regions must stay clear of primary content (keeps content from being obscured by system UI).
|
||||
- Pane focus-order/trap rules above (coplanar order must match visual order; modal panes trap and restore
|
||||
focus).
|
||||
- RTL is treated as an accessibility/global-reach requirement throughout, not an optional locale nicety.
|
||||
- Minimum 48×48dp interaction targets carry through from Accessibility into density/scaling rules here.
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/layout/layout-overview
|
||||
https://m3.material.io/foundations/layout/layout-overview/parts-of-layout
|
||||
https://m3.material.io/foundations/layout/scaffold/overview
|
||||
https://m3.material.io/foundations/layout/scaffold/bars
|
||||
https://m3.material.io/foundations/layout/scaffold/rails
|
||||
https://m3.material.io/foundations/layout/scaffold/panes
|
||||
https://m3.material.io/foundations/layout/grids-spacing/overview
|
||||
https://m3.material.io/foundations/layout/grids-spacing/grids
|
||||
https://m3.material.io/foundations/layout/grids-spacing/spacing
|
||||
https://m3.material.io/foundations/layout/grids-spacing/density
|
||||
https://m3.material.io/foundations/layout/breakpoints
|
||||
https://m3.material.io/foundations/layout/bidirectionality-rtl
|
||||
https://m3.material.io/foundations/layout/canonical-examples
|
||||
|
||||
---
|
||||
|
||||
## Usability (foundations/usability)
|
||||
|
||||
### Principles
|
||||
- "Usability focuses on making products intuitive and easy to understand for everyone" — distinct from
|
||||
Accessibility (see Accessibility section for the exact wording of the distinction).
|
||||
- Key takeaways: emphasize key actions for visual hierarchy; leverage M3 Expressive design tactics; don't
|
||||
overwhelm users with visual information; test and iterate.
|
||||
|
||||
### Values — Nielsen Norman Group's five usability aspects (quoted directly)
|
||||
| Aspect | Definition |
|
||||
| --- | --- |
|
||||
| Efficiency | Users can efficiently complete tasks and goals |
|
||||
| Errors | Proper design reduces the likelihood of mistakes, and users can easily correct any errors that do occur |
|
||||
| Learnability | New users learn to use the product and complete tasks easily, even the first time |
|
||||
| Memorability | Returning users remember how to use the product |
|
||||
| Satisfaction | Users are satisfied with the designed experience |
|
||||
|
||||
### Rules — Usability design tactics (explicitly framed as **M3 Expressive** tactics)
|
||||
- **Color & contrast**: use eye-catching primary/secondary colors and contrasting pairs (e.g. purple +
|
||||
green) for hierarchy; always follow the accessibility contrast rules above.
|
||||
- **Containment & grouping**: group related elements in subtle containers; break content into sections with
|
||||
containment/spacing/headings.
|
||||
- **Motion**: use sparingly to emphasize key moments — "motion can be distracting" if overused.
|
||||
- **Shape & shape morph**: "The Material shape library has **35 shapes**." Shape adds emphasis/delight,
|
||||
guides focus, differentiates containers/buttons/animations, signals interaction, sets emotional tone.
|
||||
Every shape can morph into any other in the set; shape morph communicates interaction states (selected,
|
||||
tap, swipe, scroll, release, long press) and emphasizes in-progress actions.
|
||||
- **Size**: the most important action/CTA should be the largest element; larger key actions measurably
|
||||
improve efficiency, error rate, satisfaction, learnability (per the doc's own claim, no citation given).
|
||||
- **Typography**: use type scale/weight to separate information hierarchies (largest/most legible text =
|
||||
primary action; smaller text = secondary/tertiary); group similar content with the same font style.
|
||||
- Design around **primary / secondary / tertiary goals**: give primary goals the strongest emphasis;
|
||||
simplify to one primary task per page; make core actions large/reachable; don't stack too many expressive
|
||||
tactics at once (distracting).
|
||||
- **Iterate**: test and gather feedback early/often from a range of users/contexts.
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
- This entire page's "design tactics" section is explicitly attributed to **M3 Expressive** ("Leverage
|
||||
[expressive design tactics]... containment, size, shape, color, and typography"), including the 35-shape
|
||||
library and shape-morph mechanic, which are Expressive-era additions.
|
||||
|
||||
### Accessibility requirements
|
||||
- "Accessibility focuses on making products accessible for people with disabilities... perceivable,
|
||||
operable, understandable and robust... support people who use assistive technology" (contrasted directly
|
||||
with usability's broader "intuitive for everyone" framing).
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/usability
|
||||
|
||||
---
|
||||
|
||||
## Glossary terms that matter for a web component library (foundations/glossary)
|
||||
|
||||
Only terms relevant to auditing a component library are excerpted; the full glossary runs A–Z (~20KB) and
|
||||
also defines many component names already covered under Components (out of scope here). Quoted verbatim.
|
||||
|
||||
| Term | Definition (verbatim) |
|
||||
| --- | --- |
|
||||
| Adaptive design | "A design approach in which the interface changes based on known user, device, or environmental conditions. Adaptive design in Material includes layout and component adaptations." |
|
||||
| Condition | "A signal that determines when and how an adaptive layout or component should adapt." |
|
||||
| Contrast | "Difference between colors. For accessibility, contrast refers strictly to the difference in tone. **A difference of 40 in tone guarantees a WCAG contrast ratio ≥ 3.0; a difference of 50 in tone guarantees a contrast ratio ≥ 4.5.**" |
|
||||
| Customization | "A modification made to a UI reflecting an app, OEM, or individual user's visual preferences and brand." Can be per-element or global (a theme). |
|
||||
| Dark theme | "A dark theme is a low-light UI that displays mostly dark surfaces." |
|
||||
| Design tokens | "A design token represents a small, reusable design decision that's part of a design system's visual style. Tokens replace static values with self-explanatory names." |
|
||||
| Design tokens: Context | "The set of conditions in which tokens can point to non-default values (for example, dark theme, dense layout)." |
|
||||
| Design tokens: Role | "A shortened version of the system token name (for example, Secondary container color; Headline 1)." |
|
||||
| Design tokens: Types | Reference tokens (e.g. `md.ref.palette.secondary200`); Component tokens (define a component element's design attributes); System tokens (e.g. `md.sys.color.secondary-container`) |
|
||||
| Design tokens: Value | "The information defining a design attribute, either stored in a token or hard-coded." |
|
||||
| Element | (page continues past excerpt boundary — not fully captured; component name only) |
|
||||
| HCT | "HCT is an abbreviation of hue, chroma, tone. It's the name of the color space that enables dynamic color. HCT is based on CAM16 hue and chroma; the L* construct for luminance from L*a*b* (CIELAB, 1976) is denoted as T for tone." |
|
||||
| Mode | (component/theming term; not fully excerpted) |
|
||||
| Orbiter | "Floating UI elements that control the content within spatial panels." (XR-adjacent; likely out of scope) |
|
||||
| Pane | "The building blocks of a layout. Content and actions are grouped into panes, which adapt the content to best fit the screen." |
|
||||
| Role | "Short nickname describing the purpose of a design token within a design system. Also known as slots. Examples: On surface; Body 1." |
|
||||
| Spatial | (XR-adjacent term; out of scope per task instructions) |
|
||||
| Style | (design-spec term; not fully excerpted) |
|
||||
| Theme | (not fully excerpted — see "Note: A light scheme is not the same as a light theme" under Color: Scheme) |
|
||||
|
||||
### Color-system glossary terms (used throughout Customization/Design tokens)
|
||||
| Term | Definition (verbatim) |
|
||||
| --- | --- |
|
||||
| Color: Baseline scheme | "the group of selected tones that make up the default colors values used for light and dark themes." |
|
||||
| Color: Dynamic color | "A customization feature in which a user-generated color scheme is mapped to an app's color scheme." Not simply Theme-Builder output or an algorithmic scheme — it's the *presence of a changeable color role*. |
|
||||
| Color: Extended color | "A color specified (in addition to key colors) in order to fill color roles for custom schemes... brand expression or conventional (semantic) meanings." |
|
||||
| Color: Key color | "not an extracted color — it's a derivation of the source color... the basis for a tonal palette." |
|
||||
| Color: Scheme | "Any mapping of color roles to specific tones from a tonal palette." A **dark scheme** ≠ a **dark theme** (scheme vs. theme distinction explicitly noted). |
|
||||
| Color: Source color | "The single color that's extracted to define all five key colors." |
|
||||
| Color: Tonal palette | "a 13-tone range that serves as the basis for mapping tones to specific roles." |
|
||||
| Color: Tone | "colors with the same hue and chroma... degrees of lightness." Code: `Tone.of(hex).get(tone)` or `Tone.of(hue, chroma).get(tone)`. |
|
||||
| Color: User-generated schemes | colors derived from a user's wallpaper selection or Android preset colors. |
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/glossary
|
||||
|
||||
---
|
||||
|
||||
## Cross-cutting gaps found (for the auditing agents to be aware of)
|
||||
|
||||
1. **Focus indicator specifics** (thickness / offset / color token) are **not defined anywhere in
|
||||
Foundations** — only "ring-like keyboard focus indicator" is stated (states/applying-states). If the
|
||||
component library asserts a specific focus-ring spec, that spec's source is not this section of the
|
||||
site.
|
||||
2. **The "two visual indicators" accessibility rule for states** is asserted once, as a bullet, with zero
|
||||
elaboration anywhere in Foundations on what the two indicators must be.
|
||||
3. **Alt text length is inconsistent across the site itself**: 125 characters (foundations/writing) vs. 140
|
||||
characters (foundations/content-design/alt-text).
|
||||
4. **No explicit numeric grid/column/gutter table** exists on the current (M3 Expressive-era) Layout pages,
|
||||
unlike the classic Material 2 12-column-grid spec; grids/spacing guidance here is qualitative except for
|
||||
the 8dp target-spacing and pane-width (360/400/412dp) numbers captured above.
|
||||
5. **Component tokens are explicitly flagged "(in development)"** on the design-tokens page — treat
|
||||
component-token completeness claims cautiously.
|
||||
6. Two task-provided sub-page slugs did not exist as separate URLs: `design-tokens/overview`,
|
||||
`/how-to-read-tokens`, `/how-to-use-tokens` all collapse into the single `foundations/design-tokens`
|
||||
page; `layout/understanding-layout/*` and `layout/applying-layout/*` do not exist — the real nesting is
|
||||
`layout/layout-overview/parts-of-layout`, `layout/scaffold/{bars,rails,panes}`, and
|
||||
`layout/grids-spacing/{grids,spacing,density}` (all fetched and covered above).
|
||||
@@ -0,0 +1,238 @@
|
||||
https://m3.material.io/components
|
||||
https://m3.material.io/components/all-buttons
|
||||
https://m3.material.io/components/app-bars/accessibility
|
||||
https://m3.material.io/components/app-bars/guidelines
|
||||
https://m3.material.io/components/app-bars/overview
|
||||
https://m3.material.io/components/app-bars/specs
|
||||
https://m3.material.io/components/badges/accessibility
|
||||
https://m3.material.io/components/badges/guidelines
|
||||
https://m3.material.io/components/badges/overview
|
||||
https://m3.material.io/components/badges/specs
|
||||
https://m3.material.io/components/bottom-sheets/accessibility
|
||||
https://m3.material.io/components/bottom-sheets/guidelines
|
||||
https://m3.material.io/components/bottom-sheets/overview
|
||||
https://m3.material.io/components/bottom-sheets/specs
|
||||
https://m3.material.io/components/button-groups/accessibility
|
||||
https://m3.material.io/components/button-groups/guidelines
|
||||
https://m3.material.io/components/button-groups/overview
|
||||
https://m3.material.io/components/button-groups/specs
|
||||
https://m3.material.io/components/buttons/accessibility
|
||||
https://m3.material.io/components/buttons/guidelines
|
||||
https://m3.material.io/components/buttons/overview
|
||||
https://m3.material.io/components/buttons/specs
|
||||
https://m3.material.io/components/cards/accessibility
|
||||
https://m3.material.io/components/cards/guidelines
|
||||
https://m3.material.io/components/cards/overview
|
||||
https://m3.material.io/components/cards/specs
|
||||
https://m3.material.io/components/carousel/accessibility
|
||||
https://m3.material.io/components/carousel/guidelines
|
||||
https://m3.material.io/components/carousel/overview
|
||||
https://m3.material.io/components/carousel/specs
|
||||
https://m3.material.io/components/checkbox/accessibility
|
||||
https://m3.material.io/components/checkbox/guidelines
|
||||
https://m3.material.io/components/checkbox/overview
|
||||
https://m3.material.io/components/checkbox/specs
|
||||
https://m3.material.io/components/chips/accessibility
|
||||
https://m3.material.io/components/chips/guidelines
|
||||
https://m3.material.io/components/chips/overview
|
||||
https://m3.material.io/components/chips/specs
|
||||
https://m3.material.io/components/date-pickers/accessibility
|
||||
https://m3.material.io/components/date-pickers/guidelines
|
||||
https://m3.material.io/components/date-pickers/overview
|
||||
https://m3.material.io/components/date-pickers/specs
|
||||
https://m3.material.io/components/dialogs/accessibility
|
||||
https://m3.material.io/components/dialogs/guidelines
|
||||
https://m3.material.io/components/dialogs/overview
|
||||
https://m3.material.io/components/dialogs/specs
|
||||
https://m3.material.io/components/divider/accessibility
|
||||
https://m3.material.io/components/divider/guidelines
|
||||
https://m3.material.io/components/divider/overview
|
||||
https://m3.material.io/components/divider/specs
|
||||
https://m3.material.io/components/extended-fab/accessibility
|
||||
https://m3.material.io/components/extended-fab/guidelines
|
||||
https://m3.material.io/components/extended-fab/overview
|
||||
https://m3.material.io/components/extended-fab/specs
|
||||
https://m3.material.io/components/fab-menu/accessibility
|
||||
https://m3.material.io/components/fab-menu/guidelines
|
||||
https://m3.material.io/components/fab-menu/overview
|
||||
https://m3.material.io/components/fab-menu/specs
|
||||
https://m3.material.io/components/floating-action-button/accessibility
|
||||
https://m3.material.io/components/floating-action-button/guidelines
|
||||
https://m3.material.io/components/floating-action-button/overview
|
||||
https://m3.material.io/components/floating-action-button/specs
|
||||
https://m3.material.io/components/icon-buttons/accessibility
|
||||
https://m3.material.io/components/icon-buttons/guidelines
|
||||
https://m3.material.io/components/icon-buttons/overview
|
||||
https://m3.material.io/components/icon-buttons/specs
|
||||
https://m3.material.io/components/lists/accessibility
|
||||
https://m3.material.io/components/lists/guidelines
|
||||
https://m3.material.io/components/lists/overview
|
||||
https://m3.material.io/components/lists/specs
|
||||
https://m3.material.io/components/loading-indicator/accessibility
|
||||
https://m3.material.io/components/loading-indicator/guidelines
|
||||
https://m3.material.io/components/loading-indicator/overview
|
||||
https://m3.material.io/components/loading-indicator/specs
|
||||
https://m3.material.io/components/menus/accessibility
|
||||
https://m3.material.io/components/menus/guidelines
|
||||
https://m3.material.io/components/menus/overview
|
||||
https://m3.material.io/components/menus/specs
|
||||
https://m3.material.io/components/navigation-bar/accessibility
|
||||
https://m3.material.io/components/navigation-bar/guidelines
|
||||
https://m3.material.io/components/navigation-bar/overview
|
||||
https://m3.material.io/components/navigation-bar/specs
|
||||
https://m3.material.io/components/navigation-drawer/accessibility
|
||||
https://m3.material.io/components/navigation-drawer/guidelines
|
||||
https://m3.material.io/components/navigation-drawer/overview
|
||||
https://m3.material.io/components/navigation-drawer/specs
|
||||
https://m3.material.io/components/navigation-rail/accessibility
|
||||
https://m3.material.io/components/navigation-rail/guidelines
|
||||
https://m3.material.io/components/navigation-rail/overview
|
||||
https://m3.material.io/components/navigation-rail/specs
|
||||
https://m3.material.io/components/progress-indicators/accessibility
|
||||
https://m3.material.io/components/progress-indicators/guidelines
|
||||
https://m3.material.io/components/progress-indicators/overview
|
||||
https://m3.material.io/components/progress-indicators/specs
|
||||
https://m3.material.io/components/radio-button/accessibility
|
||||
https://m3.material.io/components/radio-button/guidelines
|
||||
https://m3.material.io/components/radio-button/overview
|
||||
https://m3.material.io/components/radio-button/specs
|
||||
https://m3.material.io/components/search/accessibility
|
||||
https://m3.material.io/components/search/guidelines
|
||||
https://m3.material.io/components/search/overview
|
||||
https://m3.material.io/components/search/specs
|
||||
https://m3.material.io/components/segmented-buttons/accessibility
|
||||
https://m3.material.io/components/segmented-buttons/guidelines
|
||||
https://m3.material.io/components/segmented-buttons/overview
|
||||
https://m3.material.io/components/segmented-buttons/specs
|
||||
https://m3.material.io/components/side-sheets/accessibility
|
||||
https://m3.material.io/components/side-sheets/guidelines
|
||||
https://m3.material.io/components/side-sheets/overview
|
||||
https://m3.material.io/components/side-sheets/specs
|
||||
https://m3.material.io/components/sliders/accessibility
|
||||
https://m3.material.io/components/sliders/guidelines
|
||||
https://m3.material.io/components/sliders/overview
|
||||
https://m3.material.io/components/sliders/specs
|
||||
https://m3.material.io/components/snackbar/accessibility
|
||||
https://m3.material.io/components/snackbar/guidelines
|
||||
https://m3.material.io/components/snackbar/overview
|
||||
https://m3.material.io/components/snackbar/specs
|
||||
https://m3.material.io/components/split-button/accessibility
|
||||
https://m3.material.io/components/split-button/guidelines
|
||||
https://m3.material.io/components/split-button/overview
|
||||
https://m3.material.io/components/split-button/specs
|
||||
https://m3.material.io/components/switch/accessibility
|
||||
https://m3.material.io/components/switch/guidelines
|
||||
https://m3.material.io/components/switch/overview
|
||||
https://m3.material.io/components/switch/specs
|
||||
https://m3.material.io/components/tabs/accessibility
|
||||
https://m3.material.io/components/tabs/guidelines
|
||||
https://m3.material.io/components/tabs/overview
|
||||
https://m3.material.io/components/tabs/specs
|
||||
https://m3.material.io/components/text-fields/accessibility
|
||||
https://m3.material.io/components/text-fields/guidelines
|
||||
https://m3.material.io/components/text-fields/overview
|
||||
https://m3.material.io/components/text-fields/specs
|
||||
https://m3.material.io/components/time-pickers/accessibility
|
||||
https://m3.material.io/components/time-pickers/guidelines
|
||||
https://m3.material.io/components/time-pickers/overview
|
||||
https://m3.material.io/components/time-pickers/specs
|
||||
https://m3.material.io/components/toolbars/accessibility
|
||||
https://m3.material.io/components/toolbars/guidelines
|
||||
https://m3.material.io/components/toolbars/overview
|
||||
https://m3.material.io/components/toolbars/specs
|
||||
https://m3.material.io/components/tooltips/accessibility
|
||||
https://m3.material.io/components/tooltips/guidelines
|
||||
https://m3.material.io/components/tooltips/overview
|
||||
https://m3.material.io/components/tooltips/specs
|
||||
https://m3.material.io/foundations
|
||||
https://m3.material.io/foundations/building-for-all/co-design
|
||||
https://m3.material.io/foundations/building-for-all/user-needs
|
||||
https://m3.material.io/foundations/content-design/alt-text
|
||||
https://m3.material.io/foundations/content-design/global-writing/overview
|
||||
https://m3.material.io/foundations/content-design/global-writing/word-choice
|
||||
https://m3.material.io/foundations/content-design/notifications
|
||||
https://m3.material.io/foundations/content-design/overview
|
||||
https://m3.material.io/foundations/content-design/style-guide/grammar-and-punctuation
|
||||
https://m3.material.io/foundations/content-design/style-guide/ux-writing-best-practices
|
||||
https://m3.material.io/foundations/content-design/style-guide/word-choice
|
||||
https://m3.material.io/foundations/customization
|
||||
https://m3.material.io/foundations/design-tokens/how-to-use-tokens
|
||||
https://m3.material.io/foundations/design-tokens/overview
|
||||
https://m3.material.io/foundations/designing/color-contrast
|
||||
https://m3.material.io/foundations/designing/elements
|
||||
https://m3.material.io/foundations/designing/flow
|
||||
https://m3.material.io/foundations/designing/overview
|
||||
https://m3.material.io/foundations/designing/structure
|
||||
https://m3.material.io/foundations/glossary
|
||||
https://m3.material.io/foundations/interaction/gestures
|
||||
https://m3.material.io/foundations/interaction/inputs
|
||||
https://m3.material.io/foundations/interaction/selection
|
||||
https://m3.material.io/foundations/interaction/states/applying-states
|
||||
https://m3.material.io/foundations/interaction/states/overview
|
||||
https://m3.material.io/foundations/interaction/states/state-layers
|
||||
https://m3.material.io/foundations/layout/bidirectionality-rtl
|
||||
https://m3.material.io/foundations/layout/breakpoints/compact
|
||||
https://m3.material.io/foundations/layout/breakpoints/expanded
|
||||
https://m3.material.io/foundations/layout/breakpoints/large-extra-large
|
||||
https://m3.material.io/foundations/layout/breakpoints/medium
|
||||
https://m3.material.io/foundations/layout/breakpoints/overview
|
||||
https://m3.material.io/foundations/layout/canonical-examples/feed
|
||||
https://m3.material.io/foundations/layout/canonical-examples/list-detail
|
||||
https://m3.material.io/foundations/layout/canonical-examples/overview
|
||||
https://m3.material.io/foundations/layout/canonical-examples/supporting-pane
|
||||
https://m3.material.io/foundations/layout/grids-spacing/density
|
||||
https://m3.material.io/foundations/layout/grids-spacing/grids
|
||||
https://m3.material.io/foundations/layout/grids-spacing/overview
|
||||
https://m3.material.io/foundations/layout/grids-spacing/spacing
|
||||
https://m3.material.io/foundations/layout/layout-overview/adaptive-design
|
||||
https://m3.material.io/foundations/layout/layout-overview/overview
|
||||
https://m3.material.io/foundations/layout/layout-overview/parts-of-layout
|
||||
https://m3.material.io/foundations/layout/scaffold/bars
|
||||
https://m3.material.io/foundations/layout/scaffold/overview
|
||||
https://m3.material.io/foundations/layout/scaffold/panes
|
||||
https://m3.material.io/foundations/layout/scaffold/rails
|
||||
https://m3.material.io/foundations/overview/assistive-technology
|
||||
https://m3.material.io/foundations/overview/principles
|
||||
https://m3.material.io/foundations/usability/applying-m3-expressive
|
||||
https://m3.material.io/foundations/usability/overview
|
||||
https://m3.material.io/foundations/writing/best-practices
|
||||
https://m3.material.io/foundations/writing/text-resizing
|
||||
https://m3.material.io/foundations/writing/text-truncation
|
||||
https://m3.material.io/styles
|
||||
https://m3.material.io/styles/color/advanced/adjust-existing-colors
|
||||
https://m3.material.io/styles/color/advanced/apply-colors
|
||||
https://m3.material.io/styles/color/advanced/define-new-colors
|
||||
https://m3.material.io/styles/color/advanced/overview
|
||||
https://m3.material.io/styles/color/choosing-a-scheme
|
||||
https://m3.material.io/styles/color/dynamic/choosing-a-source
|
||||
https://m3.material.io/styles/color/dynamic/content-based-source
|
||||
https://m3.material.io/styles/color/dynamic/user-generated-source
|
||||
https://m3.material.io/styles/color/resources
|
||||
https://m3.material.io/styles/color/roles
|
||||
https://m3.material.io/styles/color/static/baseline
|
||||
https://m3.material.io/styles/color/static/custom-brand
|
||||
https://m3.material.io/styles/color/system/how-the-system-works
|
||||
https://m3.material.io/styles/color/system/overview
|
||||
https://m3.material.io/styles/elevation/applying-elevation
|
||||
https://m3.material.io/styles/elevation/overview
|
||||
https://m3.material.io/styles/elevation/tokens
|
||||
https://m3.material.io/styles/icons/applying-icons
|
||||
https://m3.material.io/styles/icons/designing-icons
|
||||
https://m3.material.io/styles/icons/overview
|
||||
https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration
|
||||
https://m3.material.io/styles/motion/easing-and-duration/tokens-specs
|
||||
https://m3.material.io/styles/motion/overview/how-it-works
|
||||
https://m3.material.io/styles/motion/overview/specs
|
||||
https://m3.material.io/styles/motion/transitions/applying-transitions
|
||||
https://m3.material.io/styles/motion/transitions/transition-patterns
|
||||
https://m3.material.io/styles/shape/corner-radius-scale
|
||||
https://m3.material.io/styles/shape/overview-principles
|
||||
https://m3.material.io/styles/shape/shape-morph
|
||||
https://m3.material.io/styles/spacing/applying-spacing
|
||||
https://m3.material.io/styles/spacing/overview
|
||||
https://m3.material.io/styles/spacing/tokens
|
||||
https://m3.material.io/styles/typography/applying-type
|
||||
https://m3.material.io/styles/typography/editorial-treatments
|
||||
https://m3.material.io/styles/typography/fonts
|
||||
https://m3.material.io/styles/typography/overview
|
||||
https://m3.material.io/styles/typography/type-scale-tokens
|
||||
@@ -0,0 +1,318 @@
|
||||
# Material Design 3 — Styles Supplement (Spacing; Color additions; Motion transition patterns)
|
||||
|
||||
Extraction from sitemap pages `reference-styles.md` did not cover (fetched directly, not via in-site
|
||||
navigation). Same structure as the reference. Only material **not** already in `reference-styles.md` is
|
||||
included; where a fetched page turned out to be a duplicate of an already-covered page, that is noted
|
||||
instead of repeating it.
|
||||
|
||||
**Duplicate pages found (no new content beyond what's already in the reference):**
|
||||
- `styles/color/system/overview` — byte-identical to `styles/color/system` (already in the reference's
|
||||
Color section / Source URLs), *except* it also carries a Resources table not previously captured — see
|
||||
Color § Values below.
|
||||
- `styles/color/advanced/overview` — byte-identical to `styles/color/advanced` (already in the reference's
|
||||
Color § Source URLs; its linked sub-pages apply-colors/define-new-colors/adjust-existing-colors are
|
||||
already the basis of the reference's "Advanced customization best practices" rules).
|
||||
- `styles/color/dynamic/choosing-a-source` — byte-identical to `styles/color/dynamic` (already in the
|
||||
reference's Color § Source URLs), *except* it's the canonical location for the source-selection
|
||||
decision criteria — see Color § Dynamic sources below (the reference didn't extract these criteria).
|
||||
- `styles/motion/easing-and-duration/applying-easing-and-duration` — its "Suggested easing and duration
|
||||
pairs" table, "Choosing an easing set", "Choosing an easing type" (by transition direction), and
|
||||
"Choosing a duration" (by size / enter vs. exit) sections are already reproduced verbatim in the
|
||||
reference's Motion § Rules and § Values. The only new fact is an M2-vs-M3 style comparison — see
|
||||
Motion § Principles below.
|
||||
|
||||
---
|
||||
|
||||
## Spacing
|
||||
|
||||
Absent from `reference-styles.md` entirely — no Spacing section existed. This is a full addition.
|
||||
|
||||
### Principles
|
||||
|
||||
- Spacing is applied to the margins, padding, and gaps of a component, UI element, or layout.
|
||||
- Spacing adapts to different values based on context, like mobile vs. desktop, or density settings.
|
||||
- The spacing system is measured on an **8dp scale**, where **space100 = 8dp**.
|
||||
- Spacing is applied to the flow of elements (horizontal, vertical) or in relation to elements (leading,
|
||||
trailing, top, bottom, gap).
|
||||
- Unlike the color system (which adjusts light/dark theme logic across all components at once), spacing
|
||||
logic is tailored and built within each component individually — there's no single global spacing
|
||||
transform.
|
||||
- Components have padding/margin/gap tokens that map to system spacing tokens, which resolve to final dp
|
||||
values (component token → system token → value).
|
||||
|
||||
### Rules (Google's wording)
|
||||
|
||||
- Spacing has three categories: **padding** (space inside an element), **gap** (space between elements in
|
||||
a grid or container), and **margin** (space outside an element). Position can be vertical, top, bottom,
|
||||
horizontal, leading, or trailing. **Leading**/**trailing** swap sides in RTL languages.
|
||||
- **Do** define padding and gaps on the parent container to organize all elements inside.
|
||||
- **Don't** define margins on child elements — they usually aren't uniform and require more tokens.
|
||||
- **Use padding & gaps before using margins**: Material rarely uses margins in components; padding/gaps
|
||||
apply spacing more uniformly. Only use margins for spacing beyond the parent container's padding, or in
|
||||
layouts.
|
||||
- Use a horizontal/vertical gap for simple components where gaps are always the same size; complex
|
||||
components with many different gaps should name them by the elements on each side (e.g. "icon-label
|
||||
gap") rather than a single generic gap token.
|
||||
- When text is scaled up to 200%, the same spacing should be preserved by default (spacing does not scale
|
||||
with text).
|
||||
- What to use, in priority order:
|
||||
1. **Pre-tokenized components** — some Material components map to spacing system tokens out of the box
|
||||
(work is ongoing to hook up all components; this mapping can be customized for form factor/density).
|
||||
2. **System tokens** — apply these to custom components/layouts, replacing hardcoded values.
|
||||
3. If the right system token doesn't exist, **customize the system** (see below) and add your own.
|
||||
- Customizing the system (three approaches, by scope):
|
||||
- **Customize Material's existing component spacing** — remap a component attribute to a different
|
||||
system token product-wide (e.g. change "button top padding" from **space125** to **space200** for a
|
||||
taller default button).
|
||||
- **Add custom system spacing & patterns** — when you need units beyond what Material provides, or have
|
||||
a recurring adaptive pattern. Follow the multiplier convention for new tokens (e.g. **space225 = 18dp**
|
||||
= 8dp × 2.25). Product-specific recurring patterns can get their own pattern token (e.g. if cards and
|
||||
sheets always adapt horizontal content padding the same way, create a **surface-content padding
|
||||
horizontal** token for that pattern).
|
||||
- **Add adaptive layout & density** — map the same component to different system tokens per device type
|
||||
(mobile vs. desktop), and/or adapt vertical padding to different values per density setting, while
|
||||
keeping the same core component.
|
||||
- **Note:** system spacing tokens are currently only used on **Jetpack Compose** (see Availability below)
|
||||
— this is a stronger platform restriction than color/elevation/shape, which have broader (if uneven)
|
||||
support.
|
||||
|
||||
### Values
|
||||
|
||||
#### Availability
|
||||
|
||||
| Type | Resource | Status |
|
||||
| --- | --- | --- |
|
||||
| Implementation | Android Views (MDC-Android) | Unavailable |
|
||||
| Implementation | Jetpack Compose | Available |
|
||||
| Implementation | Web | Unavailable |
|
||||
|
||||
#### Spacing scale
|
||||
|
||||
The site does not render the token→dp table as text (it's an image diagram); the following is
|
||||
reconstructed from the images' alt-text and from dp values cited inline elsewhere on the page, and should
|
||||
be treated as inferred, not a verbatim site table:
|
||||
|
||||
| Token | Multiplier | Value |
|
||||
| --- | --- | --- |
|
||||
| space25 | 0.25× | 2dp |
|
||||
| space50 | 0.5× | 4dp |
|
||||
| space75 | 0.75× | 6dp |
|
||||
| **space100** | **1×** | **8dp (base unit, `md.sys.measurement.space100`)** |
|
||||
| space125 | 1.25× | 10dp |
|
||||
| space200 | 2× | 16dp |
|
||||
| space300 | 3× | 24dp |
|
||||
| space400 | 4× | 32dp |
|
||||
| space500 | 5× | 40dp |
|
||||
| space600 | 6× | 48dp |
|
||||
| space700 | 7× | 56dp |
|
||||
| space800 | 8× | 64dp |
|
||||
| space900 | 9× | 72dp |
|
||||
|
||||
Basis for this reconstruction: the overview page's scale diagram alt-text reads "a spacing scale shows 2,
|
||||
4, 6, and 8 at the bottom range and 48, 56, 64, and 72 at the top of the range"; the tokens page states the
|
||||
main range "covers 0x to 9x" of the 8dp base unit and separately defines nested sub-1x units at "0.25x,
|
||||
0.5x, 0.75x, 1.25x"; the overview page separately lists the same nested dp values ("2dp, 4dp, 6dp, and
|
||||
10dp"). Values beyond space900 (e.g. the 18dp/space225 example) are explicitly presented as **custom**,
|
||||
not part of the default set. Material only defines the nested/nonstandard units it actively uses — the
|
||||
scale is not a complete arithmetic progression at every step.
|
||||
|
||||
#### Component spacing — naming convention
|
||||
|
||||
| | Going forward | Legacy |
|
||||
| --- | --- | --- |
|
||||
| Prefix | `padding`, `margin`, `gap` | `space` |
|
||||
| Positional modifiers | horizontal, vertical, leading, trailing, top, bottom | leading-space, trailing-space, top-space, bottom-space, between-space |
|
||||
| Example | "Medium button: leading padding" | "Medium button: leading space" |
|
||||
|
||||
#### Parts of spacing — example (Search)
|
||||
|
||||
The search container: 8dp vertical padding, 8dp horizontal gaps, 24dp horizontal margins by default
|
||||
(narrows to 12dp when focused, while padding/gaps stay the same). The 24dp default margin exists
|
||||
specifically "to ensure accurate placement from the screen edge."
|
||||
|
||||
#### Spacing concepts diagrammed on the site (numbered image captions — anatomy, not values)
|
||||
|
||||
- Component layout: (1) vertical padding top & bottom, (2) vertical gap, (3) horizontal padding leading &
|
||||
trailing, (4) horizontal gap.
|
||||
- Page layout: (1) margin, (2) top padding, (3) horizontal padding leading & trailing, (4) spacer/gap,
|
||||
(5) vertical gap. Layouts additionally use **panes** (layout containers, which can be fixed, flexible,
|
||||
floating, or semi-permanent) and **spacers** (space between two panes; contains a drag handle if the
|
||||
panes are resizable).
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
|
||||
No Expressive-specific spacing changes are called out on these pages (unlike shape/motion/typography,
|
||||
spacing carries no "M3 Expressive" labelled update section). The system is presented as a single,
|
||||
version-agnostic linear scale.
|
||||
|
||||
### Accessibility
|
||||
|
||||
- Spacing must be preserved (not scaled) when text is scaled up to 200%, so layouts stay stable and
|
||||
legible as text grows — spacing is a support mechanism for text-resizing accessibility, not something
|
||||
that competes with it.
|
||||
|
||||
### Source URLs
|
||||
|
||||
- https://m3.material.io/styles/spacing/overview
|
||||
- https://m3.material.io/styles/spacing/applying-spacing
|
||||
- https://m3.material.io/styles/spacing/tokens
|
||||
|
||||
---
|
||||
|
||||
## Color — additions
|
||||
|
||||
### Static: Custom brand color scheme
|
||||
|
||||
Not previously fetched. Supplements the reference's Color section (which covers baseline/static generally
|
||||
but not the brand-specific workflow).
|
||||
|
||||
**Principles**
|
||||
- A **brand-based static scheme** is hand-picked by the product team to align with the product's brand
|
||||
color(s), as opposed to the **baseline** static scheme (Material's own default) or a **dynamic** scheme.
|
||||
It requires greater ongoing investment since the team creates and maintains it entirely.
|
||||
- End-users see: an accessible UI with static colors, and "a product that looks like its brand."
|
||||
|
||||
**Rules (Google's wording)**
|
||||
- Brand schemes are still generated through the same tonal-palette machinery as dynamic color: you can
|
||||
set a **custom source color independently for each of the 6 palette groups** — Primary, Secondary,
|
||||
Tertiary, Error, Neutral, and Neutral Variant — not just Primary. Setting only Primary and leaving the
|
||||
rest defaulted is also valid ("use the scheme as-is").
|
||||
- Workflow (Material Theme Builder Figma plugin): add a new theme, name it (the name becomes the color
|
||||
role prefix in Figma), set a custom Hex source color for Primary (and optionally Secondary/Tertiary/
|
||||
Error/Neutral/Neutral Variant) → generates a full custom color scheme with the same role set as baseline.
|
||||
- Enabling "Generate State Layers" in the plugin settings when creating the theme is called out as
|
||||
necessary groundwork for interaction-state design (cross-references state layers).
|
||||
- Point of escalation: "want to further adjust your brand color scheme?" and "need to make adjustments?"
|
||||
both route to **Advanced customizations** (apply/define/adjust colors) — i.e. custom-brand is the
|
||||
starting point, advanced customization is the refinement path.
|
||||
- Develop: export the branded scheme from Material Theme Builder for Jetpack Compose, Android Views,
|
||||
Flutter, Web, or as a JSON file.
|
||||
|
||||
**Source URLs**
|
||||
- https://m3.material.io/styles/color/static/custom-brand
|
||||
|
||||
### Dynamic: choosing and using a source
|
||||
|
||||
Two new sub-pages (content-based-source, user-generated-source) plus decision criteria from
|
||||
choosing-a-source that the reference's Color section states only as a single pipeline description, not as
|
||||
selection criteria.
|
||||
|
||||
**Principles**
|
||||
- Both user-generated and content-based color go through the same pipeline: the image is digitally
|
||||
analyzed through **quantization**, a single color is selected as the source color, and tones are chosen
|
||||
and assigned to each color role (this is the concrete mechanism behind the "dynamic color pipeline"
|
||||
already summarized in the reference).
|
||||
- **User-generated color**: sourced from the user's wallpaper.
|
||||
- **Content-based color**: sourced from in-app content — an album thumbnail, a logo, a video preview.
|
||||
|
||||
**Rules (Google's wording) — decision criteria (not previously captured)**
|
||||
|
||||
| Source type | Choose it if… |
|
||||
| --- | --- |
|
||||
| User-generated | Users would benefit from a personalized experience that's well-tested; you want to showcase the latest Material features |
|
||||
| Content-based | Content is front-and-center in the product; the team can do some advanced customization; it supports usability of content-centric features like media players; best applied to contained screen elements adjacent to the source image (the source image itself need not stay visible) |
|
||||
| Multiple sources | Product requirements meet more than one of the above criteria and the team doesn't mind extra customization work; get started with user-generated color first, then customize |
|
||||
|
||||
- Example of multiple sources coexisting: a photo app derives content-based color (green) for edit buttons
|
||||
from the in-app photo, while wallpaper-derived user-generated color (red) drives share buttons elsewhere
|
||||
in the same screen.
|
||||
- Content-based color guidance: apply it where it enhances brand identity and personalization (e.g. a
|
||||
music app deriving color from album artwork; a news app differentiating publications by their content
|
||||
color).
|
||||
- QA/testing step for user-generated color (Figma workflow): use the Material Theme Builder's "Dynamic"
|
||||
mode, add a real image or use the Shuffle icon for a random source color, swap it onto frames, and
|
||||
**repeat across a range of colors** to get a sense of how the product will appear across different
|
||||
users' devices/wallpapers — i.e. dynamic-color design review should be done across many source colors,
|
||||
not just one or two.
|
||||
- Android implementation reference specifically for content-based color: MDC-Android's
|
||||
"Content-based dynamic color" doc (separate from the general dynamic-color doc already cited).
|
||||
|
||||
**Source URLs**
|
||||
- https://m3.material.io/styles/color/dynamic/choosing-a-source
|
||||
- https://m3.material.io/styles/color/dynamic/content-based-source
|
||||
- https://m3.material.io/styles/color/dynamic/user-generated-source
|
||||
|
||||
---
|
||||
|
||||
## Motion — Transitions (supplement)
|
||||
|
||||
The reference's Motion section already reproduces the easing/duration Rules and Values from
|
||||
`applying-easing-and-duration` verbatim, and names the six transition patterns in one summary line without
|
||||
detail. This section supplies the missing detail: what each pattern is for, what it's commonly used with,
|
||||
platform (Android/iOS) differences, and the accessibility/consistency criteria for transitions overall —
|
||||
which also fills the gap the reference explicitly flagged ("No general reduced-motion policy was found
|
||||
stated on `styles/motion` itself").
|
||||
|
||||
**Naming note:** the current M3 site does **not** use the older M2 terms "shared axis / fade through /
|
||||
fade." The current (2026) taxonomy is: **Container transform**, **Forward and backward** (≈ old shared
|
||||
axis), **Lateral**, **Top level** (≈ old fade through), **Enter and exit** (subsumes old "fade" cases), and
|
||||
**Skeleton loaders** (new, not an M2 concept). Anyone looking for "shared axis"/"fade through"/"fade" by
|
||||
name should map to this table.
|
||||
|
||||
### Principles — what makes a good transition
|
||||
|
||||
- **Follows accessibility settings**: when a platform's reduced-motion setting is on, transitions should
|
||||
use subtle fades instead of intense sliding/scaling, and disable decorative effects like parallax or
|
||||
shape morphing. *(This is the missing general reduced-motion policy for `styles/motion` — previously
|
||||
only found at the component level, e.g. carousel/menus.)*
|
||||
- **Consistent**: applying the same transition type consistently for the same kind of navigation makes
|
||||
apps feel cohesive and predictable (e.g. multiple Android apps sharing one forward/backward transition).
|
||||
- **Stable layouts**: use skeleton loaders (subtle pulsing) so content doesn't shift position or pop in
|
||||
abruptly as it loads.
|
||||
- **No jarring jump cuts**: avoid instant, uncued transitions by default — they're disorienting. Exception:
|
||||
a jump cut may be preferred when pure efficiency is the top priority (e.g. opening a menu in a
|
||||
productivity app).
|
||||
- **Coherent spatial model**: transitions should help users understand an app's physical/spatial layout
|
||||
(e.g. keep a consistent axis when moving between collapsed/expanded carousel views; don't flip between
|
||||
horizontal and vertical layouts).
|
||||
- **Unified direction**: elements are grouped and move along one primary axis rather than independently;
|
||||
only genuinely persistent elements (e.g. a hero image) should remain visible throughout — animating many
|
||||
persistent elements independently is distracting.
|
||||
- **Clean fades**: fully fade out old content before fading in new content; avoid overlapping
|
||||
partially-transparent frames. If a cross-fade is unavoidable, keep it quick and hide it during the
|
||||
fastest part of the transition (e.g. a dialog entering mid-screen should not slowly fade on top of
|
||||
content — use a short fade to hide the overlap).
|
||||
- **Simple style**: transitions are frequent and utility-driven, not a showcase for stylized motion — avoid
|
||||
overt effects like bouncy springs on common transitions.
|
||||
|
||||
### Rules — per-pattern guidance (Google's wording)
|
||||
|
||||
| Pattern | Use for | Commonly used with | Do / Don't / Caution |
|
||||
| --- | --- | --- | --- |
|
||||
| **Container transform** | Seamlessly transforming an element to show more detail (e.g. a card expanding into a details page); hero moments that should be expressive; shallow hierarchies (expand for detail, then collapse); creating a seamless connection between elements. Most dramatic/expressive pattern — reserve for the right context. | Cards, lists, image galleries, search boxes, sheets, FABs, chips | **Do** use it for hero moments instead of a forward/backward transition. **Don't** use it in apps with deep hierarchies (motion becomes excessive) or in utility-focused navigation (style mismatch). |
|
||||
| **Forward and backward** | Navigating between screens at consecutive hierarchy levels (e.g. inbox → message thread) | Lists, cards, buttons, links | **Do** use platform defaults — easy to implement, stays current with platform updates. Android fades content as screens slide (reduces motion vs. a full-width slide); iOS uses parallax (background slides slower than foreground). **Caution**: container transform requires custom implementation and can feel excessive if overused for this role. |
|
||||
| **Lateral** | Navigating between peer content at the same hierarchy level (e.g. swiping tabs in a content library) | Tabs, carousels, image galleries | Elements are grouped and slide in unison with **no fade or parallax**, creating a strong peer relationship and hinting at swipe-ability. **Caution**: fading content while it slides weakens the peer/swipe cue and can be confused with forward/backward. **Don't** use for hierarchical navigation — full-width sliding is excessive and wrongly implies a peer (non-hierarchical) relationship. |
|
||||
| **Top level** | Navigating between top-level destinations (e.g. tapping a nav bar/rail/drawer item) | Navigation bar, navigation rail, navigation drawer | Exiting screen quickly fades out, then the entering screen fades in — intentionally **no** grouping or persistent elements, since top-level destinations aren't necessarily related. **Don't** use a lateral transition here — it wrongly implies you can swipe between top-level destinations, conflicting with carousel/list swipe gestures. |
|
||||
| **Enter and exit — within screen bounds** | Introducing/removing a component in the context of the main UI (modal, e.g. a dialog; or non-modal, e.g. a bottom sheet coexisting with content) | FABs, dialogs, menus, snackbars, time pickers, tooltips | Android: expand/collapse along x or y axis only (scale and z-axis motion are avoided — they'd imply an elevation change, which conflicts with M3's reduced-elevation model). iOS: uniform scale to enter, fade to exit. Enter direction is informed by on-screen location, expanding away from the device edge (e.g. a top menu expands downward; a bottom snackbar expands upward). **Don't** use this pattern for navigating hierarchical screens — full-height sliding is excessive and creates an unclear screen-to-screen relationship. |
|
||||
| **Enter and exit — beyond screen bounds** | Components that slide fully on/off screen | App bars, banners, navigation bar, navigation rail, navigation drawer, sheets | Android: expand/collapse along x or y axis while sliding on/off (adds shape emphasis). iOS: slides on/off without changing shape. Coplanar side sheets shrink the available content area rather than overlaying it. Can be scroll-driven (e.g. a top app bar or nav bar sliding off/on during scroll, to reclaim screen space). Entry/exit location establishes spatial memory: notifications enter from the top (matches the pull-down drawer), a nav drawer enters from the left (matches its off-screen position), a bottom sheet/keyboard enters from the bottom (easiest to reach). |
|
||||
| **Skeleton loaders** | Transitioning from a temporary loading state to the fully loaded UI | Used in combination with other transition patterns | Subtle pulsing animation indicates indeterminate progress, animating from top-left to bottom-right. Once content loads, it quickly fades in **on top of** the skeleton loader (not instantly popping in). |
|
||||
|
||||
### Values
|
||||
|
||||
Durations and easing for these patterns are governed by the already-referenced tables (Suggested
|
||||
easing/duration pairs; legacy duration tokens) — no separate duration table is given per pattern on these
|
||||
pages. The pattern choice itself is the "value" documented here; see the Rules table above for the
|
||||
authoritative per-pattern mapping (this *is* the durations/patterns cross-reference the task asked for —
|
||||
the site does not tie a specific numeric duration to "container transform" etc. beyond the general
|
||||
begin/end-on-screen vs. enter vs. exit rules already in the reference).
|
||||
|
||||
### M2 vs M3 easing note (from `applying-easing-and-duration`; not an M3-vs-Expressive distinction)
|
||||
|
||||
- Compared to M2's more utilitarian easing style, **M3 easing is more expressive**: transitions have
|
||||
"snappy take-offs and very soft landings."
|
||||
- **M3 durations are slightly longer than M2's**, deliberately, to give transitions time to come to a
|
||||
gentle rest without feeling abrupt.
|
||||
|
||||
### Accessibility requirements
|
||||
|
||||
- General reduced-motion policy for transitions (see Principles above): substitute subtle fades for
|
||||
intense sliding/scaling, and disable decorative effects (parallax, shape morphing) when a platform's
|
||||
reduced-motion setting is on. This generalizes — and predates in priority — the component-level
|
||||
reduced-motion notes already in the reference (carousel, menus).
|
||||
|
||||
### Source URLs
|
||||
|
||||
- https://m3.material.io/styles/motion/transitions/applying-transitions
|
||||
- https://m3.material.io/styles/motion/transitions/transition-patterns
|
||||
- https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration (mostly duplicate
|
||||
of already-referenced content; see M2-vs-M3 note above for the one new fact)
|
||||
@@ -0,0 +1,896 @@
|
||||
# Material Design 3 — Styles Reference (Color, Elevation, Icons, Motion, Shape, Typography)
|
||||
|
||||
Ground-truth extraction from m3.material.io/styles and its sub-pages, plus androidx Compose Material3
|
||||
token source files where the site only renders a value inside an unrendered interactive widget.
|
||||
Anything not from the live site text is explicitly marked "(not on the site; from <source>)".
|
||||
|
||||
---
|
||||
|
||||
## Color
|
||||
|
||||
### Principles
|
||||
|
||||
- Color is used to express style and communicate meaning. With dynamic color, Material puts personal
|
||||
color preferences and individual needs at the forefront of systematic color application.
|
||||
- The Material color system includes: a built-in set of accessible color relationships (e.g. a dark
|
||||
surface color is algorithmically paired with a light text-label color so the UI automatically meets
|
||||
contrast requirements); 26+ color roles mapped to components; a built-in dark theme; a static baseline
|
||||
color scheme; and dynamic color (user-generated from wallpaper, or content-based from in-app content).
|
||||
- Color roles are "like the numbers in a paint-by-number canvas" — the connective tissue between UI
|
||||
elements and what color goes where.
|
||||
- **Color roles are mapped to Material Components.** Custom components must be mapped to this same
|
||||
role set.
|
||||
- **Color roles ensure accessibility.** The system is built on accessible color pairings; color pairs
|
||||
provide an accessible minimum 3:1 contrast.
|
||||
- **Color roles are tokenized.**
|
||||
- General vocabulary used in role names:
|
||||
- **Surface** — backgrounds and large, low-emphasis areas.
|
||||
- **Primary / Secondary / Tertiary** — accent roles used to emphasize or de-emphasize foreground elements.
|
||||
- **Container** — fill color for foreground elements like buttons; not for text or icons.
|
||||
- **On** — a color for text/icons *on top of* its paired parent color (e.g. "on primary" on "primary").
|
||||
- **Variant** — a lower-emphasis alternative to its non-variant pair (e.g. "outline variant" vs "outline").
|
||||
- The system uses a color space called **HCT** (Hue, Chroma, Tone), not HSL/RGB. HCT lets you manipulate
|
||||
hue and chroma without affecting tone.
|
||||
- **Hue**: 0–360, circular (perception as red/orange/yellow/…).
|
||||
- **Chroma**: 0 (grey/black/white) up to roughly 120 in HCT (colorfulness); max chroma varies by hue/tone.
|
||||
- **Tone**: 0 (pure black) to 100 (pure white); determines contrast. HCT tone is not interchangeable
|
||||
with HSL lightness — two colors can share an HCT tone (perceived brightness) while having different
|
||||
HSL lightness values.
|
||||
- Dynamic color pipeline: (1) start with a source color (from wallpaper, in-app content, or hand-picked);
|
||||
(2) feed it into an algorithm; (3) the algorithm generates key colors; (4) it creates a **tonal palette**
|
||||
per key color (tones 0–100 in steps of 10, plus 95/98/99, some palettes have more); (5) it assigns tones
|
||||
to the ~26 standard color roles for both light and dark theme; (6) the new colors are applied to the UI.
|
||||
|
||||
### Rules (Google's wording)
|
||||
|
||||
- Pair and layer color roles only as intended (e.g. primary + on primary + secondary container + on
|
||||
secondary container) to keep them legible as contrast level changes. **Don't** mix roles improperly
|
||||
(e.g. primary + primary container + secondary container + on surface) — this can become illegible as
|
||||
contrast changes.
|
||||
- **Don't** use the **outline** color for dividers (different contrast requirements) — use **outline
|
||||
variant** instead.
|
||||
- **Don't** use **outline** for components containing multiple elements (e.g. cards) — use **outline
|
||||
variant**.
|
||||
- **Don't** use **outline variant** to create visual hierarchy or define a target's visual boundary —
|
||||
use **outline** or another color giving 3:1 contrast with the surface.
|
||||
- **Caution**: **outline variant** can border targets like chips/buttons only if elements inside them
|
||||
(icons/text) already meet 4.5:1 contrast.
|
||||
- Most products won't need the **add-on** color roles (fixed/fixed-dim, on-fixed/on-fixed-variant,
|
||||
surface-dim/surface-bright). "If you aren't sure whether your product should use the add-on roles, it
|
||||
probably shouldn't."
|
||||
- **Don't** use **fixed** colors where contrast is necessary — they don't adapt to light/dark theme and
|
||||
are likely to cause contrast issues (e.g. a button fill on a permissions screen). **Do** use primary/
|
||||
secondary/tertiary where contrast is needed.
|
||||
- All color mappings — especially surface colors — should remain the same for a given layout region
|
||||
across breakpoints (e.g. body always **surface**, navigation always **surface container**, on both
|
||||
mobile and tablet).
|
||||
- Error is a **static** color by default (doesn't change with dynamic color schemes) but still adapts to
|
||||
light/dark theme.
|
||||
- Advanced customization best practices:
|
||||
- Any role starting with "on-" is guaranteed sufficient contrast with its paired role; other role pairs
|
||||
may not meet the 4.5:1 (small text) / 3:1 (large text) Material contrast requirements — check before
|
||||
reusing a pairing.
|
||||
- Always apply **color roles**, never static hex values or raw tonal-palette values, for anything that
|
||||
should respond to theme, contrast level, or dynamic color.
|
||||
- Test a dynamic-color component under multiple source-color themes (light/dark; red/yellow/green/blue).
|
||||
- Limit a screen to two color schemes from different source types (e.g. baseline/user-generated combined
|
||||
with one content-based source) to avoid visual disarray.
|
||||
- Don't replace semantic colors (e.g. red error, green success) with content-based dynamic color.
|
||||
- Don't harmonize colors whose appearance should stay absolutely consistent (e.g. brand colors).
|
||||
- Defining custom color roles should be considered only if the existing Material roles can't achieve the
|
||||
desired result.
|
||||
- Choosing a scheme: use **static (baseline)** if not ready for dynamic color, migrating from M2, building
|
||||
for enterprise users, or building for iOS. Use **dynamic** if the product should personalize, react to
|
||||
wallpaper/content, or offer user-controlled contrast.
|
||||
|
||||
### Values
|
||||
|
||||
#### Color roles (Compose `ColorSchemeKeyTokens`, 47 tokens; tone source `ColorLightTokens.kt` / `ColorDarkTokens.kt`)
|
||||
|
||||
Each role resolves to a tone from one of 5 tonal palettes (Primary, Secondary, Tertiary, Neutral,
|
||||
Neutral Variant) or Error. Format: role — light theme tone — dark theme tone.
|
||||
|
||||
| Role | Light tone | Dark tone | Purpose (site wording) |
|
||||
| --- | --- | --- | --- |
|
||||
| Primary | Primary40 | Primary80 | High-emphasis fills, text, icons against surface |
|
||||
| On primary | Primary100 | Primary20 | Text/icons against primary |
|
||||
| Primary container | Primary90 | Primary30 | Standout fill against surface, key components like FAB |
|
||||
| On primary container | Primary10 | Primary90 | Text/icons against primary container |
|
||||
| Secondary | Secondary40 | Secondary80 | Less prominent fills/text/icons against surface |
|
||||
| On secondary | Secondary100 | Secondary20 | Text/icons against secondary |
|
||||
| Secondary container | Secondary90 | Secondary30 | Recessive fill, e.g. tonal buttons |
|
||||
| On secondary container | Secondary10 | Secondary90 | Text/icons against secondary container |
|
||||
| Tertiary | Tertiary40 | Tertiary80 | Complementary accent, e.g. input fields |
|
||||
| On tertiary | Tertiary100 | Tertiary20 | Text/icons against tertiary |
|
||||
| Tertiary container | Tertiary90 | Tertiary30 | Complementary container fill |
|
||||
| On tertiary container | Tertiary10 | Tertiary90 | Text/icons against tertiary container |
|
||||
| Error | Error40 | Error80 | Attention-grabbing color for urgency |
|
||||
| On error | Error100 | Error20 | Text/icons against error |
|
||||
| Error container | Error90 | Error30 | Attention-grabbing fill |
|
||||
| On error container | Error10 | Error90 | Text/icons against error container |
|
||||
| Surface | Neutral98 | Neutral6 | Default background |
|
||||
| On surface | Neutral10 | Neutral90 | Text/icons on surface / surface container |
|
||||
| On surface variant | NeutralVariant30 | NeutralVariant80 | Lower-emphasis text/icons on surface |
|
||||
| Surface variant | NeutralVariant90 | NeutralVariant30 | (legacy; superseded by surface container roles Feb 2023) |
|
||||
| Background / On background | Neutral98 / Neutral10 | Neutral6 / Neutral90 | Legacy roles, same values as surface/on surface |
|
||||
| Outline | NeutralVariant50 | NeutralVariant60 | Important boundaries, e.g. text-field outline |
|
||||
| Outline variant | NeutralVariant80 | NeutralVariant30 | Decorative elements, e.g. dividers |
|
||||
| Inverse surface | Neutral20 | Neutral90 | Background contrasting against surrounding surface |
|
||||
| Inverse on surface | Neutral95 | Neutral20 | Text/icons against inverse surface |
|
||||
| Inverse primary | Primary80 | Primary40 | Actionable elements (e.g. text buttons) against inverse surface |
|
||||
| Scrim | Neutral0 | Neutral0 | Scrim behind modals, at 32% opacity |
|
||||
| Surface tint | (=Primary) | (=Primary) | Deprecated — use elevation level tokens instead |
|
||||
|
||||
Surface container family (5 levels, named by emphasis — not tied to elevation, replaced "surface +1..+5"
|
||||
in Feb 2023):
|
||||
|
||||
| Role | Light tone | Dark tone |
|
||||
| --- | --- | --- |
|
||||
| Surface container lowest | Neutral100 | Neutral4 |
|
||||
| Surface container low | Neutral96 | Neutral10 |
|
||||
| Surface container | Neutral94 | Neutral12 |
|
||||
| Surface container high | Neutral92 | Neutral17 |
|
||||
| Surface container highest | Neutral90 | Neutral22 |
|
||||
|
||||
Bright/dim add-on surface roles (keep relative brightness across both themes, unlike default surface
|
||||
which inverts):
|
||||
|
||||
| Role | Light tone | Dark tone |
|
||||
| --- | --- | --- |
|
||||
| Surface dim | Neutral87 | Neutral6 |
|
||||
| Surface bright | Neutral98 | Neutral24 |
|
||||
|
||||
Fixed accent add-on roles (same tone in light **and** dark theme; added Feb 2023):
|
||||
|
||||
| Role | Tone (both themes) | Purpose |
|
||||
| --- | --- | --- |
|
||||
| Primary fixed | Primary90 | Fill color that stays constant across themes |
|
||||
| Primary fixed dim | Primary80 | Stronger/more emphasized fixed tone |
|
||||
| On primary fixed | Primary10 | Text/icons on primary fixed |
|
||||
| On primary fixed variant | Primary30 | Lower-emphasis text/icons on primary fixed |
|
||||
| Secondary fixed | Secondary90 | ” |
|
||||
| Secondary fixed dim | Secondary80 | ” |
|
||||
| On secondary fixed | Secondary10 | ” |
|
||||
| On secondary fixed variant | Secondary30 | ” |
|
||||
| Tertiary fixed | Tertiary90 | ” |
|
||||
| Tertiary fixed dim | Tertiary80 | ” |
|
||||
| On tertiary fixed | Tertiary10 | ” |
|
||||
| On tertiary fixed variant | Tertiary30 | ” |
|
||||
|
||||
#### Baseline tonal palettes (hex; from androidx `PaletteTokens.kt`, since the site renders these only in
|
||||
an interactive, non-text widget)
|
||||
|
||||
| Tone | Primary | Secondary | Tertiary | Neutral | Neutral Variant | Error |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 0 | #000000 | #000000 | #000000 | #000000 | #000000 | #000000 |
|
||||
| 4 | — | — | — | #0F0D13 | — | — |
|
||||
| 6 | — | — | — | #141218 | — | — |
|
||||
| 10 | #21005D | #1D192B | #31111D | #1D1B20 | #1D1A22 | #410E0B |
|
||||
| 12 | — | — | — | #211F26 | — | — |
|
||||
| 17 | — | — | — | #2B2930 | — | — |
|
||||
| 20 | #381E72 | #332D41 | #492532 | #322F35 | #322F37 | #601410 |
|
||||
| 22 | — | — | — | #36343B | — | — |
|
||||
| 24 | — | — | — | #3B383E | — | — |
|
||||
| 30 | #4F378B | #4A4458 | #633B48 | #48464C | #49454F | #8C1D18 |
|
||||
| 40 | #6750A4 | #625B71 | #7D5260 | #605D64 | #605D66 | #B3261E |
|
||||
| 50 | #7F67BE | #7A7289 | #986977 | #79767D | #79747E | #DC362E |
|
||||
| 60 | #9A82DB | #958DA5 | #B58392 | #938F96 | #938F99 | #E46962 |
|
||||
| 70 | #B69DF8 | #B0A7C0 | #D29DAC | #AEA9B1 | #AEA9B4 | #EC928E |
|
||||
| 80 | #D0BCFF | #CCC2DC | #EFB8C8 | #CAC5CD | #CAC4D0 | #F2B8B5 |
|
||||
| 87 | — | — | — | #DED8E1 | — | — |
|
||||
| 90 | #EADDFF | #E8DEF8 | #FFD8E4 | #E6E0E9 | #E7E0EC | #F9DEDC |
|
||||
| 92 | — | — | — | #ECE6F0 | — | — |
|
||||
| 94 | — | — | — | #F3EDF7 | — | — |
|
||||
| 95 | #F6EDFF | #F6EDFF | #FFECF1 | #F5EFF7 | #F5EEFA | #FCEEEE |
|
||||
| 96 | — | — | — | #F7F2FA | — | — |
|
||||
| 98 | — | — | — | #FEF7FF | — | — |
|
||||
| 99 | #FFFBFE | #FFFBFE | #FFFBFA | #FFFBFF | #FFFBFE | #FFFBF9 |
|
||||
| 100 | #FFFFFF | #FFFFFF | #FFFFFF | #FFFFFF | #FFFFFF | #FFFFFF |
|
||||
|
||||
(Source: androidx Compose Material3 `PaletteTokens.kt` — the purple/violet "baseline" scheme. Any product
|
||||
using a different source color will generate a different palette via the HCT algorithm, but role→tone
|
||||
assignments are the same.)
|
||||
|
||||
#### Contrast levels
|
||||
|
||||
| Level | Ratio (approx.) | Notes |
|
||||
| --- | --- | --- |
|
||||
| Standard (default) | mixed | Emphasizes hierarchy using high- and low-contrast elements together |
|
||||
| Medium | 3:1 minimum | For users needing more contrast without high-contrast side effects like halation |
|
||||
| High | 7:1 | Further emphasizes essential elements (e.g. card content, not the card container) |
|
||||
|
||||
Contrast settings apply automatically in both light and dark theme. Custom components support contrast
|
||||
levels simply by using Material color roles (e.g. primary container / on primary container) — the role's
|
||||
resolved value changes per contrast level automatically.
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
|
||||
The color pages do not explicitly label any change "M3 Expressive" the way shape/motion/typography pages
|
||||
do, but the "What's new" timeline on `styles/color` lists (with the May 2025 entry coinciding with the
|
||||
Expressive launch):
|
||||
|
||||
- **May 2025 — Three levels of contrast.** Color roles now support standard/medium/high contrast, each
|
||||
tokenized (see Values above).
|
||||
- **Aug 2024 — More colorful text & icons.** On-primary-container, on-secondary-container,
|
||||
on-tertiary-container, on-error-container were updated in light theme to be more colorful while
|
||||
remaining accessible; affects badges, buttons (all variants), chips, lists, menus, navigation bar,
|
||||
navigation drawer, navigation rail, switches, toolbars.
|
||||
- **Feb 2023 — Tone-based surface colors.** Replaced the old "surface +1 to +5 elevation overlay" model
|
||||
with the surface/surface-container role family (not tied to elevation). Also: default light-theme
|
||||
surface moved from tone 99 to tone 98; neutral palette chroma increased 4→6; dark-theme surfaces
|
||||
slightly darkened (alignment with Android SysUI).
|
||||
- **Feb 2023 — Additional accent colors.** Added fixed / fixed-dim roles for primary, secondary, tertiary.
|
||||
|
||||
### Accessibility requirements
|
||||
|
||||
- Color pairs provide an accessible **minimum 3:1 contrast** by design (color-role pairing system).
|
||||
- Text contrast targets (also stated on `foundations/designing/color-contrast`, W3C-based):
|
||||
large text (≥14pt bold / ≥18pt regular) and graphics: **≥3:1** against background; small text: **≥4.5:1**.
|
||||
Disabled states do not need to meet contrast requirements.
|
||||
- Non-text clustered elements (e.g. a group of buttons) should meet **3:1** contrast between container
|
||||
color and background; a standalone prominent element (e.g. a FAB) does not need this because its
|
||||
prominence already distinguishes it.
|
||||
- High contrast mode targets **7:1**.
|
||||
- Default typography color is **on surface** (or **on surface variant** as a strong alternative).
|
||||
- **Color-alone rule**: hyperlinked text must use primary (or tertiary, for a less prominent link) *and*
|
||||
must be underlined — color alone must not be the only signal.
|
||||
|
||||
### Source URLs
|
||||
|
||||
- https://m3.material.io/styles
|
||||
- https://m3.material.io/styles/color
|
||||
- https://m3.material.io/styles/color/system (identical render to /styles/color)
|
||||
- https://m3.material.io/styles/color/roles
|
||||
- https://m3.material.io/styles/color/system/how-the-system-works
|
||||
- https://m3.material.io/styles/color/choosing-a-scheme
|
||||
- https://m3.material.io/styles/color/static (= /styles/color/static/baseline)
|
||||
- https://m3.material.io/styles/color/dynamic
|
||||
- https://m3.material.io/styles/color/advanced
|
||||
- https://m3.material.io/styles/color/advanced/apply-colors
|
||||
- https://m3.material.io/styles/color/advanced/define-new-colors
|
||||
- https://m3.material.io/styles/color/advanced/adjust-existing-colors
|
||||
- https://m3.material.io/styles/color/resources
|
||||
- https://m3.material.io/foundations/designing/color-contrast (contrast-ratio table)
|
||||
- https://raw.githubusercontent.com/androidx/androidx/androidx-main/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorSchemeKeyTokens.kt
|
||||
- .../tokens/ColorLightTokens.kt
|
||||
- .../tokens/ColorDarkTokens.kt
|
||||
- .../tokens/PaletteTokens.kt
|
||||
|
||||
---
|
||||
|
||||
## Elevation
|
||||
|
||||
### Principles
|
||||
|
||||
- Elevation is applied to all surfaces and components.
|
||||
- Tokens codify the distance on the z-axis so components appear consistently relative to each other.
|
||||
- Tokens carry no shadow or color themselves — each platform determines the specific shadow/value used
|
||||
at each elevation level.
|
||||
- Elevation can be shown as tonal surface colors **or** shadows.
|
||||
- Avoid changing the default elevation of Material 3 components.
|
||||
- Stick to using a small number of elevation levels.
|
||||
- Elevation is measured as the distance between components along the z-axis in density-independent
|
||||
pixels (dp).
|
||||
- To successfully depict elevation, a surface must show: surface edges (contrast with surroundings),
|
||||
overlap with other surfaces (at rest or in motion), and distance from other surfaces.
|
||||
|
||||
### Rules (do/don't)
|
||||
|
||||
- **Do** ensure floating elements (e.g. a FAB) have sufficient contrast with the surface beneath them.
|
||||
**Don't** use colors with insufficient contrast — the relationship between surfaces must stay clear.
|
||||
- By default, Material 3 surfaces use **tonal difference** to indicate separation; shadows and scrims
|
||||
are alternative/additional methods.
|
||||
- Any overlapping containment areas or components should use **different surface/surface-container color
|
||||
roles** to communicate separation — these color roles are not tied to elevation.
|
||||
- Shadow size and softness both express distance: small, sharp shadows = close proximity; larger, softer
|
||||
shadows = more distance.
|
||||
- "When it comes to applying shadows, less is more. The fewer levels in your UI, the more power they have
|
||||
to direct attention and action."
|
||||
- Use **visible shadows** to: (1) protect elements against a patterned/busy background (cards, chips,
|
||||
buttons), or (2) encourage interaction (elements can temporarily lift on focus/selection/hover; a
|
||||
raised element can lower when a higher element appears).
|
||||
- Use a **scrim** to bring focus to elements over a large layered surface (e.g. modals, expanded
|
||||
navigation menus).
|
||||
|
||||
### Values
|
||||
|
||||
#### Elevation levels (`ElevationTokens.kt`; matches the site's dp values)
|
||||
|
||||
| Level | dp |
|
||||
| --- | --- |
|
||||
| 0 | 0dp |
|
||||
| 1 | 1dp |
|
||||
| 2 | 3dp |
|
||||
| 3 | 6dp |
|
||||
| 4 | 8dp |
|
||||
| 5 | 12dp |
|
||||
|
||||
An element's **resting state** is on levels 0–3; levels 4–5 are reserved for user-interacted states
|
||||
(hover, dragged) — e.g. hovering a FAB increases elevation by 1 level (level 3 → 4), consistently across
|
||||
all Material buttons.
|
||||
|
||||
#### Component resting elevation (from `styles/elevation/tokens`)
|
||||
|
||||
| Resting level | dp | Components |
|
||||
| --- | --- | --- |
|
||||
| 5 | 12dp | (not assigned as a resting level) |
|
||||
| 4 | 8dp | (not assigned as a resting level) |
|
||||
| 3 | 6dp | Date pickers, Dialogs (modal), Extended FAB, FAB, FAB menu (close button), Search, Time pickers |
|
||||
| 2 | 3dp | App bar (scrolled), Menu, Navigation bar, Rich tooltip, Toolbar |
|
||||
| 1 | 1dp | Banner, Bottom sheet (modal), Button (elevated), Card (elevated), Chips (elevated), Navigation drawer (modal), Side sheet (modal) |
|
||||
| 0 | 0dp | App bar (not scrolled), Buttons (filled/tonal/outlined), Button groups, Cards (filled/outlined), Carousel, Chips, Dialog (full-screen), Extended FAB (in nav rail), FAB (in nav rail), FAB menu (list items), Icon buttons, List, Navigation rail, Segmented button, Side sheet (docked), Slider, Split button, Tabs |
|
||||
|
||||
#### Scrim
|
||||
|
||||
Scrims use the **scrim** color role at **32% opacity**.
|
||||
|
||||
### Differences from M2
|
||||
|
||||
- **Shadows**: instead of applying shadows by default at all levels, use shadows only when needed for
|
||||
protection against a background or to encourage interaction.
|
||||
- **Color**: new color mappings and compatibility with dynamic color.
|
||||
- **Levels**: elevation is now described in terms of discrete levels (0–5) rather than continuous dp.
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
|
||||
No "M3 Expressive" update is documented on the fetched elevation pages (`styles/elevation`,
|
||||
`/applying-elevation`, `/tokens`) — the elevation model appears unchanged by the Expressive update.
|
||||
|
||||
### Accessibility requirements
|
||||
|
||||
- Edges (surface separation) must create sufficient contrast between surfaces — meeting or exceeding
|
||||
accessible contrast ratios — to be perceived as separate. Elevation/shadow alone is treated as a visual
|
||||
cue that must be reinforced with adequate color/tonal contrast, not relied on in isolation.
|
||||
|
||||
### Source URLs
|
||||
|
||||
- https://m3.material.io/styles/elevation (= /styles/elevation/overview)
|
||||
- https://m3.material.io/styles/elevation/applying-elevation
|
||||
- https://m3.material.io/styles/elevation/tokens
|
||||
- https://raw.githubusercontent.com/androidx/androidx/androidx-main/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ElevationTokens.kt
|
||||
|
||||
---
|
||||
|
||||
## Icons
|
||||
|
||||
### Principles
|
||||
|
||||
- Icons are small symbols to easily identify actions and categories.
|
||||
- Material Symbols are the new default: a variable icon font set in three styles — **outlined, rounded,
|
||||
sharp** — across seven weights. (Legacy "Material Icons" remain available but lack the variable-font axes.)
|
||||
- Design principles (do/don't): simplify icons for clarity/legibility, don't be overly literal or complex;
|
||||
use geometric, consistent, bold shapes, don't use delicate/loose organic shapes; maintain one consistent
|
||||
visual style per icon set, don't mix styles within a set.
|
||||
|
||||
### Rules (do/don't)
|
||||
|
||||
- **Don't** use the lightest weight (100) for standard-size (24dp) icons — minimum weight for that size
|
||||
should be **200**. Use caution with excessive weight at 24dp too.
|
||||
- **Do** apply weight consistently across a group (e.g. a navigation rail); **don't** mix different weights.
|
||||
- **Do** use the same size for a Material Symbol and adjacent text; **don't** mix symbol/text sizes.
|
||||
- **Do** use the same optical weight for a symbol and adjacent text; **don't** mismatch optical weights.
|
||||
- **Do** shift a symbol's baseline down ~11.5% of the text size when pairing with text; **don't** use the
|
||||
same baseline for symbol and text.
|
||||
- **Do** position icons "on pixel" within the icon grid; **don't** place on non-integer (sub-pixel)
|
||||
coordinates.
|
||||
- Corners: default corner radius is **2dp**. For the **outlined** style, interior corners are square (not
|
||||
rounded); for shapes ≤2dp wide, stroke corners shouldn't be rounded. **Rounded** style: both exterior
|
||||
and interior corners are rounded. **Sharp** style: both exterior and interior corners reduce from 2dp
|
||||
to **0dp**. **Caution**: overly round corners reduce legibility. **Don't** use inconsistent corner radii.
|
||||
- Stroke: recommended stroke weight is **2dp** (regular weight, 400) with consistent weights and squared
|
||||
stroke terminals; **don't** use inconsistent stroke weights or rounded stroke terminals. Complex icons
|
||||
may use an optically-corrected **1.5dp** stroke to fit multiple curves in the 24×24dp space.
|
||||
- **Do** make icons face forward (flat); **don't** tilt, rotate, or render icons as dimensional/isometric.
|
||||
- Icon content must stay inside the **live area**; it may extend into padding toward the **trim area** if
|
||||
extra visual weight is needed, but **no part may extend past the trim area**.
|
||||
|
||||
### Values
|
||||
|
||||
#### Sizes
|
||||
|
||||
| Size | Use |
|
||||
| --- | --- |
|
||||
| 20dp | Desktop, dense layouts, small-scale visuals |
|
||||
| 24dp | Standard (baseline) size |
|
||||
| 40dp | Larger — pairs with display/headline type, larger screens |
|
||||
| 48dp | Larger — pairs with display/headline type, larger screens |
|
||||
|
||||
#### Layout (24dp standard icon)
|
||||
|
||||
| Region | Size |
|
||||
| --- | --- |
|
||||
| Trim area (total canvas) | 24dp × 24dp |
|
||||
| Live area | 20dp × 20dp |
|
||||
| Padding (live→trim) | 2dp around the perimeter |
|
||||
|
||||
#### Grid and keyline shapes (within the 24dp grid)
|
||||
|
||||
| Keyline | Dimensions |
|
||||
| --- | --- |
|
||||
| Square | 18dp × 18dp |
|
||||
| Circle | 20dp diameter |
|
||||
| Vertical rectangle | 20dp height × 16dp width |
|
||||
| Horizontal rectangle | 16dp height × 20dp width |
|
||||
|
||||
#### Corner radius by style
|
||||
|
||||
| Style | Exterior corner | Interior corner |
|
||||
| --- | --- | --- |
|
||||
| Outlined | 2dp rounded | Square (not rounded) |
|
||||
| Rounded | 2dp rounded | Rounded |
|
||||
| Sharp | 0dp | 0dp |
|
||||
|
||||
#### Material Symbols variable-font axes
|
||||
|
||||
| Axis | Range | Notes |
|
||||
| --- | --- | --- |
|
||||
| Weight | 100 (thin) – 700 (bold) | Also affects overall symbol size; min 200 recommended at 24dp |
|
||||
| Fill | 0 – 1 | 0 = unfilled/outlined, 1 = fully filled; communicates state transitions |
|
||||
| Grade | negative / 0 / positive (site examples: −25, 0) | Finer-grained than weight; doesn't change width or line breaks. Default grade is 0 for a dark icon on light background, **−25** for a light icon on dark background (compensates for visual bleed/halation). Positive grade for emphasis (e.g. active state). |
|
||||
| Optical size | 20dp – 48dp | Auto-adjusts stroke weight so the icon "looks the same" as size scales; use 20dp for dense desktop layouts, 40–48dp to highlight primary actions |
|
||||
|
||||
#### Icon styles (3): Outlined, Rounded, Sharp — see corner-radius table above.
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
|
||||
No "M3 Expressive" update is documented on the fetched icon pages (`styles/icons`,
|
||||
`/designing-icons`, `/applying-icons`). Material Symbols (three styles + four variable axes) predate the
|
||||
Expressive update; no Expressive-specific icon changes were found on the site.
|
||||
|
||||
### Accessibility requirements
|
||||
|
||||
- Icon meaning must always be unambiguous and accessible; use caution displaying icons without labels.
|
||||
Label text (short, meaningful) should be used especially for navigation.
|
||||
- Below **20dp**, symbols that are complex/highly-detailed, have multiple parts, or represent a key
|
||||
action essential to the product should have an accompanying text label. Simple symbols (e.g. a star for
|
||||
ratings) can be used alone at any size if they remain identifiable.
|
||||
- **Target size**: a 24dp symbol should have a default **target size of 48dp**.
|
||||
|
||||
### Source URLs
|
||||
|
||||
- https://m3.material.io/styles/icons (= /styles/icons/overview)
|
||||
- https://m3.material.io/styles/icons/designing-icons
|
||||
- https://m3.material.io/styles/icons/applying-icons
|
||||
|
||||
---
|
||||
|
||||
## Motion
|
||||
|
||||
### Principles
|
||||
|
||||
- May 2025: Material introduced the **motion physics system** with M3 Expressive — a spring-based system
|
||||
replacing the previous easing-and-duration system, intended to feel "more alive, fluid, and natural,"
|
||||
and to be easier to implement/customize.
|
||||
- Two preset **motion schemes**: **Expressive** (Material's opinionated default; overshoots final values
|
||||
to add bounce; use for most situations, hero moments, key interactions) and **Standard** (minimal
|
||||
bounce, eases into final values; for utilitarian products). Custom schemes can also be created.
|
||||
- A **spring** is defined by three attributes: **stiffness** (hardness — higher = faster resolve),
|
||||
**damping** (how fast bounce wears out — higher = faster stop; damping = 1 removes bounce entirely),
|
||||
and **initial velocity** (starting speed, combines with stiffness/damping to affect total duration).
|
||||
- Springs are versatile (one spring can drive transitions, button effects, gestures — consistent feel)
|
||||
and feel natural/predictable, handling gesture interruption and retargeting seamlessly.
|
||||
- Two spring **styles**: **Spatial** (movement — x/y position, rotation, size, rounded corners; overshoots
|
||||
and bounces into place) and **Effects** (color, opacity; no overshoot).
|
||||
- Three **speeds** per style: **default**, **fast**, **slow**. Most motion uses default; smaller elements
|
||||
may use fast; larger elements may use slow. The exact values differ by device class (wearable/phone/
|
||||
tablet) even though the *relative* speed ordering (fast < default < slow) is constant.
|
||||
- Token naming: e.g. `md.sys.motion.spring.fast.spatial` — the scheme (expressive/standard) is applied at
|
||||
the product level, not baked into the token name, so schemes can be swapped without re-tokenizing.
|
||||
- On Jetpack Compose, **21 Material components** use the motion physics system by default (Android Views/
|
||||
MDC-Android: available but not yet wired into components; Web: compatible via curve approximations).
|
||||
- Legacy **easing and duration** tokens remain available as a fallback and are still used for transition
|
||||
animation on platforms/paths not yet migrated to springs.
|
||||
|
||||
### Rules (do/don't; easing & duration choice)
|
||||
|
||||
- Choose the **Emphasized** easing set for most transitions (captures the M3 style); use **Standard**
|
||||
only for small, utility-focused, quick transitions, and as the fallback on platforms without Emphasized
|
||||
support (iOS, Web).
|
||||
- Easing by transition type:
|
||||
- **Begin and end on screen** → **Emphasized** (speeds up quickly, gentle rest, emphasizes the end).
|
||||
- **Enter the screen** → **Emphasized decelerate** (starts at peak velocity, gentle rest).
|
||||
- **Exit the screen permanently** → **Emphasized accelerate** (starts at rest, ends at peak velocity —
|
||||
implies it can't be retrieved).
|
||||
- **Exit the screen temporarily** → **Emphasized** (ends at rest just off-screen — implies it can be
|
||||
retrieved, e.g. a drawer).
|
||||
- Duration by transition size: small-area transitions get short durations; large-area transitions get
|
||||
long durations (consistent sense of speed).
|
||||
- Duration by direction: **exit/dismiss/collapse** → shorter durations (less attention needed); **enter/
|
||||
persist** → longer durations (focus attention on what's new).
|
||||
- Suggested easing+duration pairs (defaults good for most transitions):
|
||||
|
||||
| Easing | Duration | Transition type |
|
||||
| --- | --- | --- |
|
||||
| Emphasized | 500ms | Begin and end on screen |
|
||||
| Emphasized decelerate | 400ms | Enter the screen |
|
||||
| Emphasized accelerate | 200ms | Exit the screen |
|
||||
| Standard | 300ms | Begin and end on screen |
|
||||
| Standard decelerate | 250ms | Enter the screen |
|
||||
| Standard accelerate | 200ms | Exit the screen |
|
||||
|
||||
- Motion customization levels: **Level 1** — use a default scheme (expressive/standard) as-is; **Level 2**
|
||||
— create a custom `MotionScheme` (custom `AnimationSpec` per property) applied product-wide; **Level 3**
|
||||
— swap the scheme per element/screen (override the `CompositionLocal`) while most of the product uses
|
||||
the primary scheme.
|
||||
- Transition patterns documented: container transform (full-screen and within-screen), forward/backward,
|
||||
lateral, top-level, enter/exit (within screen bounds and beyond screen bounds), skeleton loaders.
|
||||
|
||||
### Values
|
||||
|
||||
#### Expressive motion scheme — spring tokens (`ExpressiveMotionTokens.kt`)
|
||||
|
||||
| Speed | Style | Damping | Stiffness |
|
||||
| --- | --- | --- | --- |
|
||||
| Fast | Spatial | 0.6 | 800.0 |
|
||||
| Fast | Effects | 1.0 | 3800.0 |
|
||||
| Default | Spatial | 0.8 | 380.0 |
|
||||
| Default | Effects | 1.0 | 1600.0 |
|
||||
| Slow | Spatial | 0.8 | 200.0 |
|
||||
| Slow | Effects | 1.0 | 800.0 |
|
||||
|
||||
#### Standard motion scheme — spring tokens (`StandardMotionTokens.kt`)
|
||||
|
||||
| Speed | Style | Damping | Stiffness |
|
||||
| --- | --- | --- | --- |
|
||||
| Fast | Spatial | 0.9 | 1400.0 |
|
||||
| Fast | Effects | 1.0 | 3800.0 |
|
||||
| Default | Spatial | 0.9 | 700.0 |
|
||||
| Default | Effects | 1.0 | 1600.0 |
|
||||
| Slow | Spatial | 0.9 | 300.0 |
|
||||
| Slow | Effects | 1.0 | 800.0 |
|
||||
|
||||
#### Web curve equivalents for springs (from `styles/motion/overview/specs`; springs aren't natively
|
||||
supported on the web, so Web should use these approximated cubic-bezier curves with matching duration)
|
||||
|
||||
| Spring | Cubic-bezier | Duration |
|
||||
| --- | --- | --- |
|
||||
| Expressive fast spatial | 0.42, 1.67, 0.21, 0.90 | 350ms |
|
||||
| Expressive default spatial | 0.38, 1.21, 0.22, 1.00 | 500ms |
|
||||
| Expressive slow spatial | 0.39, 1.29, 0.35, 0.98 | 650ms |
|
||||
| Expressive fast effects | 0.31, 0.94, 0.34, 1.00 | 150ms |
|
||||
| Expressive default effects | 0.34, 0.80, 0.34, 1.00 | 200ms |
|
||||
| Expressive slow effects | 0.34, 0.88, 0.34, 1.00 | 300ms |
|
||||
| Standard fast spatial | 0.27, 1.06, 0.18, 1.00 | 350ms |
|
||||
| Standard default spatial | 0.27, 1.06, 0.18, 1.00 | 500ms |
|
||||
| Standard slow spatial | 0.27, 1.06, 0.18, 1.00 | 750ms |
|
||||
| Standard fast effects | 0.31, 0.94, 0.34, 1.00 | 150ms |
|
||||
| Standard default effects | 0.34, 0.80, 0.34, 1.00 | 200ms |
|
||||
| Standard slow effects | 0.34, 0.88, 0.34, 1.00 | 300ms |
|
||||
|
||||
#### Legacy easing tokens (`styles/motion/easing-and-duration/tokens-specs`; curves confirmed in
|
||||
androidx `MotionTokens.kt`, noted where Compose's single-cubic-bezier approximation differs from the
|
||||
platform-accurate multi-segment Android `PathInterpolator`)
|
||||
|
||||
| Token | Android | CSS | Flutter | iOS |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `md.sys.motion.easing.emphasized` | PathInterpolator(M 0,0 C 0.05,0 0.133333,0.06 0.166666,0.4 C 0.208333,0.82 0.25,1 1,1) | N/A (use Standard) | `easeInOutCubicEmphasized` | N/A (use Standard) |
|
||||
| `md.sys.motion.easing.emphasized.decelerate` | PathInterpolator(0.05, 0.7, 0.1, 1) | cubic-bezier(0.05,0.7,0.1,1.0) | Cubic(0.05,0.7,0.1,1.0) | ControlPoints 0.05,0.7,0.1,1.0 |
|
||||
| `md.sys.motion.easing.emphasized.accelerate` | PathInterpolator(0.3, 0, 0.8, 0.15) | cubic-bezier(0.3,0.0,0.8,0.15) | Cubic(0.3,0.0,0.8,0.15) | ControlPoints 0.3,0.0,0.8,0.15 |
|
||||
| `md.sys.motion.easing.standard` | PathInterpolator(0.2, 0, 0, 1) | cubic-bezier(0.2,0.0,0,1.0) | Cubic(0.2,0.0,0,1.0) | ControlPoints 0.2,0.0,0.0,1.0 |
|
||||
| `md.sys.motion.easing.standard.decelerate` | PathInterpolator(0, 0, 0, 1) | cubic-bezier(0,0,0,1) | Cubic(0,0,0,1) | ControlPoints 0.0,0.0,0.0,1.0 |
|
||||
| `md.sys.motion.easing.standard.accelerate` | PathInterpolator(0.3, 0, 1, 1) | cubic-bezier(0.3,0,1,1) | Cubic(0.3,0,1,1) | ControlPoints 0.3,0.0,1.0,1.0 |
|
||||
|
||||
Compose `MotionTokens.kt` also defines `EasingLegacyCubicBezier` = CubicBezierEasing(0.4, 0.0, 0.2, 1.0)
|
||||
(M2-era "standard" curve) and `EasingLinearCubicBezier` = (0,0,1,1) — not shown on the site pages fetched
|
||||
(not on the site; from androidx `MotionTokens.kt`).
|
||||
|
||||
#### Legacy duration tokens (site table; matches androidx `MotionTokens.kt` exactly)
|
||||
|
||||
| Token | Value |
|
||||
| --- | --- |
|
||||
| `md.sys.motion.duration.short1` | 50ms |
|
||||
| `md.sys.motion.duration.short2` | 100ms |
|
||||
| `md.sys.motion.duration.short3` | 150ms |
|
||||
| `md.sys.motion.duration.short4` | 200ms |
|
||||
| `md.sys.motion.duration.medium1` | 250ms |
|
||||
| `md.sys.motion.duration.medium2` | 300ms |
|
||||
| `md.sys.motion.duration.medium3` | 350ms |
|
||||
| `md.sys.motion.duration.medium4` | 400ms |
|
||||
| `md.sys.motion.duration.long1` | 450ms |
|
||||
| `md.sys.motion.duration.long2` | 500ms |
|
||||
| `md.sys.motion.duration.long3` | 550ms |
|
||||
| `md.sys.motion.duration.long4` | 600ms |
|
||||
| `md.sys.motion.duration.extra-long1` | 700ms |
|
||||
| `md.sys.motion.duration.extra-long2` | 800ms |
|
||||
| `md.sys.motion.duration.extra-long3` | 900ms |
|
||||
| `md.sys.motion.duration.extra-long4` | 1000ms |
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
|
||||
- **Physics-based motion system** (springs: stiffness/damping/initial velocity) replaces the
|
||||
easing-and-duration system as the primary motion model. Legacy easing/duration tokens remain as a
|
||||
fallback and still drive some transitions.
|
||||
- Two schemes, **Expressive** and **Standard**, selectable/customizable at up to 3 levels of depth
|
||||
(default scheme → custom scheme → per-element scheme override).
|
||||
- Spring tokens split into **spatial** vs **effects**, each with **fast/default/slow** — a 2×3 matrix per
|
||||
scheme (12 values total across both schemes).
|
||||
- 21 Jetpack Compose components use the physics system by default; component motion is largely driven by
|
||||
just two tokens in practice: **expressive fast spatial** and **expressive fast effects**.
|
||||
- Shape morph (see Shape section) uses the **expressive** motion scheme by default.
|
||||
|
||||
### Accessibility requirements
|
||||
|
||||
- No general "prefers-reduced-motion" policy was found stated on `styles/motion` itself. Reduced-motion
|
||||
handling is documented per component instead — e.g. `components/carousel/accessibility`: "When reduced
|
||||
motion settings are turned on, the parallax effect should be removed and carousel items should no
|
||||
longer expand as they come into view. All items are the same size," and hero carousels should keep the
|
||||
small item only partially visible rather than animating it. (Component-level source, included here
|
||||
because it is the only "reduced motion" guidance found in the crawl.)
|
||||
- `components/menus/guidelines`: "In dense products, such as on desktop, menus can open instantly to
|
||||
reduce motion."
|
||||
|
||||
### Source URLs
|
||||
|
||||
- https://m3.material.io/styles/motion (= /styles/motion/overview = /styles/motion/overview/how-it-works)
|
||||
- https://m3.material.io/styles/motion/overview/specs
|
||||
- https://m3.material.io/styles/motion/easing-and-duration
|
||||
- https://m3.material.io/styles/motion/easing-and-duration/tokens-specs
|
||||
- https://m3.material.io/styles/motion/transitions
|
||||
- https://m3.material.io/components/carousel/accessibility (reduced motion example)
|
||||
- https://m3.material.io/components/menus/guidelines (reduced motion example)
|
||||
- https://raw.githubusercontent.com/androidx/androidx/androidx-main/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/MotionTokens.kt
|
||||
- .../tokens/ExpressiveMotionTokens.kt
|
||||
- .../tokens/StandardMotionTokens.kt
|
||||
|
||||
---
|
||||
|
||||
## Shape
|
||||
|
||||
### Principles
|
||||
|
||||
- The M3 shape system includes original shapes, a corner radius scale, and built-in shape morphing.
|
||||
- Use abstract shapes thoughtfully to add emphasis and decorative flair; leverage Material shapes for
|
||||
built-in shape morphing.
|
||||
- Rectangular shapes are fully rounded in all corners by default; individual corners can be adjusted to
|
||||
create asymmetrical shapes.
|
||||
- Use shape and type "in harmony" — M3 shapes are designed to echo key visual attributes of M3 typography
|
||||
(e.g. M3 shapes and Google Sans Flex share roundness attributes).
|
||||
- Shape morph should respond to user interaction and connect function and feeling — use it to communicate
|
||||
interaction states (e.g. a selected button), actions in progress (e.g. typing, loading), or environment
|
||||
changes (sound, temperature, time of day).
|
||||
- "Be bold and dare to embrace tension" — contrast (square + rounded, unconventional shapes) creates more
|
||||
dynamic, memorable, expressive design; Material historically favored rounded shapes only.
|
||||
- Shape is **versatile, not semantic** — avoid making a shape literal or assigning it one fixed meaning
|
||||
(e.g. a wavy loading indicator isn't a literal progress waveform and could be reused elsewhere).
|
||||
- Use abstract shapes sparingly — don't compromise clarity for visual design; be intentional and consider
|
||||
how a shape fits the product narrative.
|
||||
- Emphasize aesthetic moments (imagery, avatar masking) with shape — these are the most flexible,
|
||||
creative uses.
|
||||
- Shape (with motion) can create a 2.5D illusion of depth on flat visuals.
|
||||
|
||||
### Rules (do/don't)
|
||||
|
||||
- Components can have symmetric (all corners equal) or asymmetric ("inner corner") shapes; both use the
|
||||
same 10-step scale. Asymmetric/inner corners are used in components with closely-grouped items (menus,
|
||||
split buttons) — inner-corner component tokens always map to individual corner-shape tokens.
|
||||
- Customization happens at **style** level (changing what a named style like "medium" resolves to —
|
||||
affects every component mapped to it, unless overridden) or **component** level (remapping one
|
||||
component to a different named style, e.g. remapping a button's default "full" to "small"/"medium").
|
||||
- The shape style family can be customized from **rounded** to **cut** (straight line instead of curve);
|
||||
add extra padding to avoid clipping content, especially in information-dense components. **Caution**:
|
||||
don't apply large/full corners to information-dense components (e.g. cards).
|
||||
- **Optical roundness**: when nesting rounded objects, don't reuse the same radius for both. Formula:
|
||||
**outer radius − padding = inner radius** (example given: 48dp − 14dp = 34dp). **Do** use different,
|
||||
proportional radii for nested components; **don't** use the same radius as the container.
|
||||
- Use the shape library for mostly visual/decorative elements; avoid unconventional shapes on text-heavy
|
||||
containers; use shapes sparingly for emphasis and delight.
|
||||
|
||||
### Values
|
||||
|
||||
#### Corner radius scale (10 styles; matches androidx `ShapeTokens.kt` exactly)
|
||||
|
||||
| # | Style | Value |
|
||||
| --- | --- | --- |
|
||||
| 1 | None | 0dp |
|
||||
| 2 | Extra small | 4dp |
|
||||
| 3 | Small | 8dp |
|
||||
| 4 | Medium | 12dp |
|
||||
| 5 | Large | 16dp |
|
||||
| 6 | Large increased | 20dp |
|
||||
| 7 | Extra large | 28dp |
|
||||
| 8 | Extra large increased | 32dp |
|
||||
| 9 | Extra extra large | 48dp |
|
||||
| 10 | Full | fully rounded (Compose: `CircleShape`, not a fixed dp value) |
|
||||
|
||||
(M2 used a 3-level shape scale based on component-container size; M3 uses this 10-level scale based on
|
||||
corner roundedness instead.)
|
||||
|
||||
#### Shape library
|
||||
|
||||
35 pre-made shapes in the Material shape library (Figma Design Kit / Jetpack Compose `MaterialShapes`
|
||||
API), designed to morph seamlessly into one another. Used by the standard **button group** and
|
||||
**loading indicator** components by default.
|
||||
|
||||
### M3 vs M3 Expressive differences (May 2025 "M3 Expressive update")
|
||||
|
||||
- Added **35 new shapes** and **shape morphing** to the Material Shape Library (Figma) and Jetpack
|
||||
Compose (`MaterialShapes` API).
|
||||
- Added new shape principles and a refreshed art direction.
|
||||
- Added corner-radius tokens: **Large increased (20dp)**, **Extra large increased (32dp)**,
|
||||
**Extra extra large (48dp)** — these three styles are new to the 10-step scale.
|
||||
- Updated "fully rounded" corners to use the dedicated **Full** style; previously this was defined as
|
||||
50% of the component's size.
|
||||
- Shape morphing uses the **expressive** motion scheme by default (see Motion section); can be switched
|
||||
to the standard scheme. Access is via a platform API — Android: Compose `MaterialShapes`; Web: not
|
||||
currently available.
|
||||
|
||||
### Accessibility requirements
|
||||
|
||||
No shape-specific accessibility requirements (contrast ratios, color-alone rule, reduced motion) were
|
||||
found on the fetched shape pages. The only adjacent accessibility-flavored guidance is the "optical
|
||||
roundness" and "cut corners need extra padding" rules above, which are about legibility/clipping rather
|
||||
than a formal accessibility requirement.
|
||||
|
||||
### Source URLs
|
||||
|
||||
- https://m3.material.io/styles/shape (= /styles/shape/overview-principles)
|
||||
- https://m3.material.io/styles/shape/corner-radius-scale
|
||||
- https://m3.material.io/styles/shape/shape-morph
|
||||
- https://raw.githubusercontent.com/androidx/androidx/androidx-main/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ShapeTokens.kt
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
### Principles
|
||||
|
||||
- Use typography to make content readable and beautiful.
|
||||
- M3 has **one type scale** containing two sets of type styles: **15 baseline** and **15 emphasized**
|
||||
(added in the Expressive update) — 30 styles total, both sets following the same scale from Display
|
||||
Large to Label Small.
|
||||
- Five style **roles**: **Display, Headline, Title, Body, Label** — each with Large/Medium/Small (Display/
|
||||
Headline/Title/Body) — see the type-scale table below for exact members.
|
||||
- **Display**: largest text on screen; short, important text/numerals; best on large screens; can use
|
||||
a more expressive/handwritten/script font; set optical size appropriately.
|
||||
- **Headline**: short, high-emphasis text on smaller screens; can use expressive typefaces if line
|
||||
height/letter spacing are adjusted to preserve readability.
|
||||
- **Title**: medium-emphasis, relatively short text (e.g. dividing secondary content); use caution with
|
||||
expressive/display/handwritten/script fonts here.
|
||||
- **Body**: longer passages; use typefaces meant for body copy, readable at small size; avoid
|
||||
expressive/decorative fonts.
|
||||
- **Label**: small, utilitarian text inside components (e.g. buttons use Label Large) or captions.
|
||||
- Two typeface roles: **Brand** (larger styles — Display/Headline — focused on expression) and **Plain**
|
||||
(smaller styles — Body/Label — focused on readability). Roboto is the default for both.
|
||||
- Typesetting: use **padding + bounding box** method for Web/iOS (line height = bounding box height, text
|
||||
vertically centered — CSS "half-leading"); use the **baseline** method for Android/platform-agnostic
|
||||
specs (distances measured to the invisible baseline a line of text rests on).
|
||||
- **Line height**: recommend a **1.2×** ratio of type size for larger styles (title/headline/display);
|
||||
recommend **~1.5×** for smaller styles (body/label) — too tight undermines flow, too loose feels
|
||||
incohesive.
|
||||
- Use **tabular (monospaced) numbers**, not proportional digits, wherever values change often (tables,
|
||||
clocks) to prevent layout shift and aid scanning.
|
||||
- Language script height: line height can auto-adapt by script category so components (with vertical
|
||||
padding) don't clip. Categories: **Small/base** (~Latin, Cyrillic, Greek, Hebrew, except Vietnamese);
|
||||
**Medium** (~7% taller: Arabic, Chinese, Hindi, Japanese, Korean, Vietnamese, most others); **Large**
|
||||
(~30% taller: Burmese, Telugu); **Extra large** (~100% taller: Nastaliq). Default to Medium since most
|
||||
languages fall there; components with a fixed height are built for Small and may not auto-adapt.
|
||||
- Material's default type scale follows the **Major Second (1.125)** modular scale, anchored to 14 (the
|
||||
base size for body text).
|
||||
|
||||
### Rules (do/don't)
|
||||
|
||||
- Emphasized styles are **not** used by default by Material components — swap the baseline token for the
|
||||
matching emphasized token (e.g. `md.sys.typescale.display-large` → `md.sys.typescale.emphasized.
|
||||
display-large`) deliberately.
|
||||
- Recommended emphasized-style use: badges, buttons (primary actions), extended FAB, selected list items,
|
||||
selected menu items, and generally selection/actions/headlines/other editorial-treatment moments —
|
||||
not just for decoration.
|
||||
- Emphasized can be combined with **weight** (apply to already-bold text) and **context** (apply
|
||||
selectively to selected/unread/key-interaction states) simultaneously.
|
||||
- When customizing the scale: change brand/plain typeface tokens if swapping fonts; adjust line height
|
||||
and letter spacing to refine appearance; **avoid changing type size** (can break component
|
||||
layout/reflow); keep both baseline and emphasized styles visually consistent (e.g. all wider than
|
||||
baseline, not just some).
|
||||
- Customizing the M3 type scale or individual styles may forgo future Material typography token updates.
|
||||
- Hyperlinks: use **primary** (or **tertiary** for a less prominent link) and the text must be
|
||||
**underlined** — never color alone.
|
||||
- **Do** use the same size and same optical weight for Material Symbols paired with text; **don't** mix.
|
||||
|
||||
### Values
|
||||
|
||||
#### Type scale — baseline set (`TypeScaleTokens.kt`)
|
||||
|
||||
| Style | Size | Line height | Tracking | Weight | Family |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Display Large | 57sp | 64sp | −0.2sp | Regular | Brand |
|
||||
| Display Medium | 45sp | 52sp | 0sp | Regular | Brand |
|
||||
| Display Small | 36sp | 44sp | 0sp | Regular | Brand |
|
||||
| Headline Large | 32sp | 40sp | 0sp | Regular | Brand |
|
||||
| Headline Medium | 28sp | 36sp | 0sp | Regular | Brand |
|
||||
| Headline Small | 24sp | 32sp | 0sp | Regular | Brand |
|
||||
| Title Large | 22sp | 28sp | 0sp | Regular | Brand |
|
||||
| Title Medium | 16sp | 24sp | 0.2sp | Medium | Plain |
|
||||
| Title Small | 14sp | 20sp | 0.1sp | Medium | Plain |
|
||||
| Body Large | 16sp | 24sp | 0.5sp | Regular | Plain |
|
||||
| Body Medium | 14sp | 20sp | 0.2sp | Regular | Plain |
|
||||
| Body Small | 12sp | 16sp | 0.4sp | Regular | Plain |
|
||||
| Label Large | 14sp | 20sp | 0.1sp | Medium | Plain |
|
||||
| Label Medium | 12sp | 16sp | 0.5sp | Medium | Plain |
|
||||
| Label Small | 11sp | 16sp | 0.5sp | Medium | Plain |
|
||||
|
||||
#### Type scale — emphasized set (Expressive; same size/line-height/family as baseline, heavier weight
|
||||
and slightly different tracking on some styles)
|
||||
|
||||
| Style | Size | Line height | Tracking | Weight | Family |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Display Large Emphasized | 57sp | 64sp | 0sp | Medium | Brand |
|
||||
| Display Medium Emphasized | 45sp | 52sp | 0sp | Medium | Brand |
|
||||
| Display Small Emphasized | 36sp | 44sp | 0sp | Medium | Brand |
|
||||
| Headline Large Emphasized | 32sp | 40sp | 0sp | Medium | Brand |
|
||||
| Headline Medium Emphasized | 28sp | 36sp | 0sp | Medium | Brand |
|
||||
| Headline Small Emphasized | 24sp | 32sp | 0sp | Medium | Brand |
|
||||
| Title Large Emphasized | 22sp | 28sp | 0sp | Medium | Brand |
|
||||
| Title Medium Emphasized | 16sp | 24sp | 0.15sp | Bold | Plain |
|
||||
| Title Small Emphasized | 14sp | 20sp | 0.1sp | Bold | Plain |
|
||||
| Body Large Emphasized | 16sp | 24sp | 0.15sp | Medium | Plain |
|
||||
| Body Medium Emphasized | 14sp | 20sp | 0.25sp | Medium | Plain |
|
||||
| Body Small Emphasized | 12sp | 16sp | 0.4sp | Medium | Plain |
|
||||
| Label Large Emphasized | 14sp | 20sp | 0.1sp | Bold | Plain |
|
||||
| Label Medium Emphasized | 12sp | 16sp | 0.5sp | Bold | Plain |
|
||||
| Label Small Emphasized | 11sp | 16sp | 0.5sp | Bold | Plain |
|
||||
|
||||
(Size, line height and font family are unchanged between a baseline style and its emphasized counterpart;
|
||||
weight steps up — Regular→Medium or Medium→Bold — and a few styles gain slightly wider tracking. Source:
|
||||
`TypeScaleTokens.kt`; the site's `styles/typography/type-scale-tokens` page describes these sets in prose
|
||||
and confirms "30 styles: 15 baseline, 15 emphasized" and the naming convention, but renders the actual
|
||||
numeric table only inside an interactive, non-text widget.)
|
||||
|
||||
`TypefaceTokens.kt` weight mapping used above: Regular = `FontWeight.Normal`, Medium = `FontWeight.
|
||||
Medium`, Bold = `FontWeight.Bold`; Brand/Plain both default to the platform sans-serif (Compose fallback;
|
||||
the actual M3 web/design spec default typeface is **Roboto** for both Brand and Plain, per
|
||||
`styles/typography/fonts`).
|
||||
|
||||
#### Fonts
|
||||
|
||||
| Font | Role | Notes |
|
||||
| --- | --- | --- |
|
||||
| Roboto | Default (Brand + Plain) | Static; used in the M3 type scale by default; 3,300+ glyphs |
|
||||
| Roboto Flex | Variable, not yet part of the type scale | Axes: Slant, Width, Weight, Grade, Optical Size, plus advanced axes (thick stroke XOPQ, thin stroke YOPQ, counter width XTRA, uppercase height YTUC, lowercase height YTLC, ascender height YTAS, descender depth YTDE, figure height YTFI); width range 25–150 (example) |
|
||||
| Roboto Serif | Variable | Reading-focused; wide weight/width range |
|
||||
| Roboto Mono | Variable | Axes: Weight, Italic; monospaced, good for code/tabular numbers |
|
||||
| Noto Sans | Fallback | Axes: Width, Weight, Italic; 150+ scripts, used when a language is unsupported |
|
||||
|
||||
Font fallback order (e.g. for Roboto Flex): **Roboto Flex → Roboto → Noto Sans**.
|
||||
|
||||
#### Units
|
||||
|
||||
| Platform | Font size unit | Conversion | Letter spacing unit | Conversion |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Android | sp | 1.0 (base) | em | tracking(px) / font size(sp) |
|
||||
| Web | rem | sp / 16 = rem (16px root default) | rem | tracking(px) / font size(sp) |
|
||||
|
||||
Example: 16sp = 1rem; 24sp = 1.5rem; 60sp = 3.75rem. 0.2 tracking / 16sp = 0.0125 em (or rem on web).
|
||||
|
||||
### M3 vs M3 Expressive differences (May 2025 "M3 Expressive update")
|
||||
|
||||
- Added **15 emphasized type styles** alongside the existing 15 baseline styles — 30 total. Emphasized
|
||||
styles have higher weight (and minor tracking adjustments) and are meant for bold/selection/emphasis
|
||||
moments; baseline and emphasized are meant to be used together, not as a full replacement.
|
||||
- New **emphasized token namespace**: `md.sys.typescale.emphasized.<style>` alongside
|
||||
`md.sys.typescale.<style>`.
|
||||
- (Aug 2026, post-Expressive) **Language script height support**: line height can automatically adapt by
|
||||
language script category (small/medium/large/extra large) — see Principles above.
|
||||
- Roboto Flex / variable-font axis guidance (weight, grade, width, optical size — "editorial treatments")
|
||||
is presented as an Expressive-era authoring technique, though Roboto Flex itself is "not yet part of
|
||||
the M3 type scale."
|
||||
|
||||
### Accessibility requirements
|
||||
|
||||
- Text contrast: **≥3:1** for large text (≥14pt bold / ≥18pt regular) and graphics; **≥4.5:1** for small
|
||||
text (restated from `foundations/designing/color-contrast`).
|
||||
- Default typography color is **on surface** (or **on surface variant**).
|
||||
- **Color-alone rule**: hyperlinked text must be **underlined**, in addition to using primary/tertiary color.
|
||||
- Avoid very light/thin weights for body text — low-resolution displays can struggle to render thin type,
|
||||
especially at small sizes; reserve very light weights for larger sizes such as display type.
|
||||
- Avoid excessive weight/bold at small label sizes — it can reduce readability (navigation-bar example).
|
||||
- Tabular numbers should be used where digits change frequently, to avoid disorienting layout shifts.
|
||||
|
||||
### Source URLs
|
||||
|
||||
- https://m3.material.io/styles/typography (= /styles/typography/overview)
|
||||
- https://m3.material.io/styles/typography/type-scale-tokens
|
||||
- https://m3.material.io/styles/typography/fonts
|
||||
- https://m3.material.io/styles/typography/applying-type
|
||||
- https://m3.material.io/styles/typography/editorial-treatments
|
||||
- https://raw.githubusercontent.com/androidx/androidx/androidx-main/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/TypeScaleTokens.kt
|
||||
- .../tokens/TypefaceTokens.kt
|
||||
Generated
+119
-656
@@ -6,11 +6,10 @@
|
||||
"": {
|
||||
"devDependencies": {
|
||||
"@material/material-color-utilities": "^0.4.0",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"esbuild": "^0.28.2",
|
||||
"fontkit": "^2.0.4",
|
||||
"laravel-vite-plugin": "^3.2.0",
|
||||
"playwright": "^1.62.1",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"vite": "^8.2.2"
|
||||
}
|
||||
},
|
||||
@@ -456,56 +455,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/remapping": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
|
||||
"integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@material/material-color-utilities": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@material/material-color-utilities/-/material-color-utilities-0.4.0.tgz",
|
||||
@@ -803,288 +752,55 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz",
|
||||
"integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==",
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.23",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
|
||||
"integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brotli": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
|
||||
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"enhanced-resolve": "^5.24.1",
|
||||
"jiti": "^2.7.0",
|
||||
"lightningcss": "1.32.0",
|
||||
"magic-string": "^0.30.21",
|
||||
"source-map-js": "^1.2.1",
|
||||
"tailwindcss": "4.3.3"
|
||||
"base64-js": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz",
|
||||
"integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==",
|
||||
"node_modules/clone": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tailwindcss/oxide-android-arm64": "4.3.3",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.3.3",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.3.3",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.3.3",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.3.3",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.3.3",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.3.3",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.3.3",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.3.3",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.3.3",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz",
|
||||
"integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz",
|
||||
"integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz",
|
||||
"integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz",
|
||||
"integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz",
|
||||
"integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz",
|
||||
"integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz",
|
||||
"integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz",
|
||||
"integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz",
|
||||
"integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz",
|
||||
"integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==",
|
||||
"bundleDependencies": [
|
||||
"@napi-rs/wasm-runtime",
|
||||
"@emnapi/core",
|
||||
"@emnapi/runtime",
|
||||
"@tybys/wasm-util",
|
||||
"@emnapi/wasi-threads",
|
||||
"tslib"
|
||||
],
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.11.1",
|
||||
"@emnapi/runtime": "^1.11.1",
|
||||
"@emnapi/wasi-threads": "^1.2.2",
|
||||
"@napi-rs/wasm-runtime": "^1.1.4",
|
||||
"@tybys/wasm-util": "^0.10.2",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz",
|
||||
"integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz",
|
||||
"integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/vite": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz",
|
||||
"integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tailwindcss/node": "4.3.3",
|
||||
"@tailwindcss/oxide": "4.3.3",
|
||||
"tailwindcss": "4.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
@@ -1097,19 +813,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.25.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.25.0.tgz",
|
||||
"integrity": "sha512-ghq3mhs649mvbarTCAlZn2wRhbfHmzAFiKxoWA14B3VtqnxtZt+wz8BroKXU0tF3GiJsnUldjVncQGZ8qk4rdA==",
|
||||
"node_modules/dfa": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
|
||||
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"tapable": "^2.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.28.2",
|
||||
@@ -1153,6 +862,13 @@
|
||||
"@esbuild/win32-x64": "0.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
@@ -1171,6 +887,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fontkit": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
|
||||
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.12",
|
||||
"brotli": "^1.3.2",
|
||||
"clone": "^2.1.2",
|
||||
"dfa": "^1.2.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"restructure": "^3.0.0",
|
||||
"tiny-inflate": "^1.0.3",
|
||||
"unicode-properties": "^1.4.0",
|
||||
"unicode-trie": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -1186,23 +920,6 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
||||
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/laravel-vite-plugin": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-3.2.0.tgz",
|
||||
@@ -1230,289 +947,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-android-arm64": "1.32.0",
|
||||
"lightningcss-darwin-arm64": "1.32.0",
|
||||
"lightningcss-darwin-x64": "1.32.0",
|
||||
"lightningcss-freebsd-x64": "1.32.0",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.32.0",
|
||||
"lightningcss-linux-arm64-gnu": "1.32.0",
|
||||
"lightningcss-linux-arm64-musl": "1.32.0",
|
||||
"lightningcss-linux-x64-gnu": "1.32.0",
|
||||
"lightningcss-linux-x64-musl": "1.32.0",
|
||||
"lightningcss-win32-arm64-msvc": "1.32.0",
|
||||
"lightningcss-win32-x64-msvc": "1.32.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-android-arm64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
|
||||
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
|
||||
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
|
||||
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
|
||||
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
|
||||
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.19",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
|
||||
@@ -1532,6 +966,13 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
|
||||
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -1610,6 +1051,13 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/restructure": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
|
||||
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz",
|
||||
@@ -1654,27 +1102,13 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
|
||||
"integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==",
|
||||
"node_modules/tiny-inflate": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
|
||||
"integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
@@ -1692,6 +1126,35 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/unicode-properties": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
|
||||
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0",
|
||||
"unicode-trie": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unicode-trie": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
|
||||
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pako": "^0.2.5",
|
||||
"tiny-inflate": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz",
|
||||
|
||||
+4
-3
@@ -7,15 +7,16 @@
|
||||
"dev": "vite",
|
||||
"build:scheme": "esbuild bin/scheme.mjs --bundle --platform=node --format=esm --target=node20 --minify --legal-comments=eof --outfile=resources/node/scheme.mjs",
|
||||
"build:shapes": "node bin/shapes.mjs",
|
||||
"build:loading": "node bin/loading-indicator.mjs"
|
||||
"build:stylesheets": "node bin/stylesheets.mjs",
|
||||
"build:loading": "node bin/loading-indicator.mjs",
|
||||
"build:springs": "node bin/springs.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@material/material-color-utilities": "^0.4.0",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"esbuild": "^0.28.2",
|
||||
"fontkit": "^2.0.4",
|
||||
"laravel-vite-plugin": "^3.2.0",
|
||||
"playwright": "^1.62.1",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"vite": "^8.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
@verbatim
|
||||
## Livewire Material
|
||||
|
||||
This application uses `nonameweb/livewire-material`: Material 3 Expressive components for Laravel and Livewire, built on Tailwind CSS. It replaces UI kits such as maryUI, daisyUI and Flux in this application.
|
||||
This application uses `nonameweb/livewire-material`: Material 3 Expressive components for Laravel and Livewire, in plain CSS. No utility classes — Tailwind, daisyUI or any other — belong here: a class your CSS does not declare does nothing, and `DesignGuard` fails it.
|
||||
|
||||
- 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.
|
||||
- Never write maryUI tags (`<x-mary-*>`) or daisyUI classes (`btn`, `card`, `badge`, `bg-base-200`, `text-base-content`…). They compile to nothing and fail silently.
|
||||
- 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`.
|
||||
- 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`.
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
@verbatim
|
||||
## Material 3
|
||||
|
||||
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. No utility classes — Tailwind, daisyUI or any other — belong here: a class your CSS does not declare does nothing, and `DesignGuard` fails it. 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.
|
||||
|
||||
### Colour
|
||||
- A colour is always a role: `md-ink-variant` on text, `var(--md-sys-color-outline-variant)` in the application's CSS, `color="error"` on a component. Never a hex, a palette tone or an opacity.
|
||||
- Pair a role only with its `on-` partner: a `primary` fill takes `on-primary` text, a `secondary-container` fill takes `on-secondary-container`. That pair is the one whose contrast is guaranteed at every contrast level; mixing pairs (`primary-container` under `on-surface`) is not.
|
||||
- `primary` is the one key action on a screen (a filled button; the FAB in `primary-container`). `secondary-container` is the quiet fill (tonal buttons, selected navigation, selected chips). `tertiary` is a contrasting accent, used rarely. `error`, `success`, `warning`, `info` mean state and nothing else: the `-container` for a tinted panel, the role itself for its text and icon.
|
||||
- Ink is `on-surface` (`md-ink`); lower emphasis is `on-surface-variant` (`md-ink-variant`); decoration is `outline` (`md-ink-quiet`). Never dim ink with an opacity: 38% means disabled.
|
||||
- `outline` is a boundary that must be read (a text field, the edge of a target). `outline-variant` is a divider or a card edge (`<x-divider>`, `<x-surface outlined>`). Never `outline` on a divider.
|
||||
- Fixed and dim roles (`primary-fixed`, `surface-dim`, …) are for a colour that must not change with the theme; if unsure, don't. Inverse roles only on an inverse surface (the snackbar).
|
||||
- A link in running text is underlined (`md-link`, with `md-ink-primary`); colour alone signals nothing.
|
||||
- Contrast: 4.5:1 for text, 3:1 for large text, icons and grouped controls; disabled is exempt. Three contrast levels exist (`<html data-contrast>`: standard, medium, high) and every role changes with them — which is why only roles are allowed.
|
||||
|
||||
### Surfaces and elevation
|
||||
- The page is `surface`. Panels separate by tone first: `surface-container-lowest` … `surface-container-highest` is a hierarchy of emphasis, not of height (`<x-surface level="surface-container-high">`). Navigation chrome is `surface-container`; a dialog, a menu, the search bar are `surface-container-high`; a modal sheet is `surface-container-low`; a filled card is `surface-container-highest`. A region keeps its role at every width.
|
||||
- Shadows (`var(--md-sys-elevation-1)` … `-5`) are for what floats or lifts: 1 for elevated cards, buttons and modal sheets; 2 for menus, the navigation bar, a scrolled app bar; 3 for the FAB, dialogs, pickers and search; one level more on hover; nothing rests above 3. Fewer shadows carry more meaning.
|
||||
- A scrim is `scrim` at 32%: `color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent)`.
|
||||
|
||||
### Shape
|
||||
- Corners come from the scale `var(--md-sys-shape-corner-{none|xs|sm|md|lg|lg-increased|xl|xl-increased|xxl|full})`, or `<x-surface corner="md">`; never a length of your own.
|
||||
- By family: `full` buttons, icon buttons, chips' avatars, badges, switches, sliders, the search bar, navigation indicators; `xs` text fields, menus, snackbars, plain tooltips; `sm` chips; `md` cards, rich tooltips; `lg` the FAB and a side sheet's inner corners; `xl` dialogs, bottom sheets, the search view, pickers, carousel items; `xxl` large hero containers.
|
||||
- Nested shapes: inner radius = outer radius − padding; never the same radius inside and out.
|
||||
- A press squares a round shape (the components do it; nothing morphs on hover). The 35 `<x-shape>`s are decoration, never meaning, used sparingly.
|
||||
|
||||
### Type
|
||||
- Every text element carries one `md-type-*` class: `display` for hero figures and short marketing lines; `headline` for page and section titles; `title` for card, dialog and list-section titles; `body` for paragraphs (`md-type-body-lg` for reading); `label` inside components (buttons, chips, tabs, captions). In the application's CSS a style is `font: var(--md-sys-typescale-body-md)` with its `-tracking`; never a size, weight, line height or letter spacing of your own.
|
||||
- `md-type-emphasized-*` is opt-in: a selected item, a primary action, a headline, a badge — not decoration.
|
||||
- 40–60 characters per line; `md-tabular` on figures that change; text must scale to 200% without loss (containers grow, rows wrap, no fixed heights on text, no ellipsis without a way to read the rest).
|
||||
|
||||
### Motion
|
||||
- Position, size and shape move on the spatial springs (they overshoot): `transition: transform var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default)` — `fast` for small elements, `slow` for large ones. Colour and opacity move on the effects springs (`--md-sys-motion-effects-*`), which never overshoot. Always pair an easing with its duration.
|
||||
- Entering decelerates, a permanent exit accelerates, a temporary exit (a sheet, a drawer) takes the emphasized curve; exits are shorter than entrances.
|
||||
- Everything that moves goes through these tokens, so reduced motion makes it instant; a literal duration is a bug.
|
||||
|
||||
### States and targets
|
||||
- Interactive elements carry `md-state-layer md-focus-ring`: hover 8%, focus 10%, pressed 10%, dragged 16% (`data-md-dragged`) of the content colour. Disabled is content at 38% and a container at 12% of `on-surface` (`--md-sys-state-disabled-content-opacity`, `--md-sys-state-disabled-container-opacity`, through `color-mix()`), with no state layer. Every state shows two indicators: colour plus a shape, an outline, an icon or a word.
|
||||
- Every target is at least 48×48px with 8px between targets (`md-touch-target` on anything drawn smaller); a denser layout is an opt-in prop, never a default.
|
||||
- Keyboard: Tab between components, arrows within one, Enter and Space activate, Escape dismisses; a dialog takes focus and gives it back to what opened it.
|
||||
|
||||
### Layout and breakpoints
|
||||
- Widths are M3's window size classes and only those: compact below 600px (the default), medium 600, expanded 840, large 1200, extra-large 1600. A layout component takes them as props (`<x-row stack-below="medium">`, `<x-stack hide-from="expanded">`, `<x-grid :columns="['compact' => 1, 'expanded' => 2]">`); the application's CSS writes `@media (width >= 840px)`; a script asks `from()` and `upTo()` from `resources/js/breakpoints.js`.
|
||||
- What changes per class: compact — navigation bar, one pane, full-screen dialogs, a bottom sheet for choices; medium — collapsed rail, one pane; expanded — rail (collapsible), two panes, menus and basic dialogs; large and extra-large — the rail expanded, two panes, a third only at extra-large as a side sheet. `<x-scaffold>` does this; content lives in panes (`<x-pane>`, `<x-list-detail>` for a list's second pane), never beside the rail by hand.
|
||||
- Margins are 16px below medium and 24px from it (`<x-pane>` draws them); spacing sits on the 4px grid as `space25` … `space900`, as padding and gaps on the parent, with margins only between layout regions. A fixed pane is 360px (expanded) or 412px (large); a side sheet at most 400px.
|
||||
- Write logical properties (`padding-inline-start`, `inset-inline-end`, `md-text-start`); directional icons mirror in RTL; charts and media controls stay LTR. Keep controls inside the safe area (`--material-safe-*`).
|
||||
|
||||
### Accessibility
|
||||
- Native elements first (`<button>`, `<dialog>`, `<input>`), then ARIA. One `main`, one `banner`, one `contentinfo`; every repeated `nav` labelled, without the word "navigation".
|
||||
- Headings in order from a single H1; the level is structure, the `md-type-*` class is appearance.
|
||||
- An icon-only control has an accessible name that does not include its role; decorative icons are hidden; an error is announced and tied to its field (`aria-describedby`); a toast uses a polite live region and never takes focus. A single-key shortcut needs a modifier or a focused component.
|
||||
|
||||
### Icons
|
||||
- `<x-icon name="home">` is a Material Symbol Rounded: `filled` means active or selected, `optical="20"` when drawn at 20px or less, one weight per group, the size and colour of the text beside it.
|
||||
|
||||
### Don'ts
|
||||
- No icon in a snackbar; no disabled FAB (hide it); no horizontal radio rows; no hover morph on cards; no `outline` on dividers; no hex colours; no utility classes, and no breakpoint, radius, shadow, type size or duration off M3's scales; no segmented buttons, navigation drawer or bottom app bar — use `<x-button-group connected>`, the expanded rail and `<x-toolbar>`.
|
||||
@endverbatim
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,293 @@
|
||||
---
|
||||
name: material-3-design
|
||||
description: Material 3 Expressive's design system as Livewire Material implements it — colour roles and surface containers, elevation, shape, type, motion, states and targets, window size classes, spacing, icons, accessibility — each M3 name beside the class, prop or token that draws it and Google's source page, for deciding how a screen should look and behave before writing it.
|
||||
---
|
||||
|
||||
# Material 3 design
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when deciding how a screen, panel or control should look or behave — which colour, container, corner, type style, motion, breakpoint or spacing — in an application that requires `nonameweb/livewire-material`, and when reviewing a view against Material 3. The props and slots of each component are in the `livewire-material-development` skill; this one is the design language they implement. The rules an agent must always follow are in the `material-3` guideline; the tables here are what those rules compress.
|
||||
|
||||
Every table pairs the M3 name with what the library gives for it. The library is plain CSS with no utility classes, so that is one of three things: a component or layout component prop (`color="error"`, `<x-surface level="surface-container">`, `gap="space200"`), one of the fixed text and interaction classes (`md-type-*`, `md-ink-*`, `md-state-layer`, `md-focus-ring`, `md-touch-target`, `md-link`), or a token the application's own CSS reads with `var()`. The tokens are CSS custom properties (`--md-sys-color-*`, `--md-sys-typescale-*`, `--md-sys-shape-*`, `--md-sys-elevation-*`, `--md-sys-motion-*`, `--md-sys-state-*`, `--md-sys-measurement-*`, `--md-ref-typeface-*`), so a stylesheet names a token and never a value.
|
||||
|
||||
## Colour
|
||||
|
||||
A colour scheme is generated from one seed by Google's colour science (`php artisan material:scheme`); every role below is a slot in that scheme, light and dark, at three contrast levels. A view names a role and nothing else — never a hex, a palette tone or an opacity — because only a role follows the theme, the contrast level and a colour profile.
|
||||
|
||||
### Roles
|
||||
|
||||
| Role | Purpose | Its `on-` pair | In this library |
|
||||
| --- | --- | --- | --- |
|
||||
| primary | High-emphasis fills, text and icons: the key action on a screen | on-primary | `var(--md-sys-color-primary)`, `md-ink-primary`, `<x-button variant="filled">` |
|
||||
| primary-container | A standout fill for key components (FAB, an emphasised panel) | on-primary-container | `var(--md-sys-color-primary-container)` |
|
||||
| primary-dim | A darker primary for a pressed or contrasting fill (2025 spec) | on-primary | `var(--md-sys-color-primary-dim)` |
|
||||
| secondary | Less prominent fills, text and icons | on-secondary | `var(--md-sys-color-secondary)` |
|
||||
| secondary-container | The recessive fill: tonal buttons, selected navigation, selected chips | on-secondary-container | `var(--md-sys-color-secondary-container)`, `<x-button variant="tonal">` |
|
||||
| tertiary | A complementary accent, used sparingly for contrast | on-tertiary | `var(--md-sys-color-tertiary)`, `color="tertiary"` |
|
||||
| tertiary-container | The complementary fill | on-tertiary-container | `var(--md-sys-color-tertiary-container)` |
|
||||
| error | Urgency and errors; static, does not follow dynamic colour | on-error | `md-ink-error`, `var(--md-sys-color-error)`, `color="error"` |
|
||||
| error-container | An error panel | on-error-container | `var(--md-sys-color-error-container)`, `<x-alert color="error">` |
|
||||
| success, warning, info | This library's custom state colours, built like error on the 2025 spec, with `-container` and `on-` pairs | on-success … | `md-ink-success`, `var(--md-sys-color-warning-container)`, `color="info"` |
|
||||
| surface | The page background | on-surface | the page itself (the foundation paints it), `<x-surface level="surface">` |
|
||||
| on-surface-variant | Lower-emphasis text and icons on any surface | — | `md-ink-variant` |
|
||||
| outline | A boundary that must be read: a text field, a target's edge (3:1 against surface) | — | `md-ink-quiet`, `var(--md-sys-color-outline)` |
|
||||
| outline-variant | Decorative lines: dividers, card edges | — | `<x-divider>`, `<x-surface outlined>`, `var(--md-sys-color-outline-variant)` |
|
||||
| inverse-surface | A surface that contrasts with its surroundings (the snackbar) | inverse-on-surface | `var(--md-sys-color-inverse-surface)` with `md-ink-inverse` |
|
||||
| inverse-primary | An action on an inverse surface (the snackbar's action) | — | `var(--md-sys-color-inverse-primary)` |
|
||||
| scrim | Behind a modal, at 32% | — | `color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent)` |
|
||||
| shadow | The shadow colour, inside every `--md-sys-elevation-*` | — | — |
|
||||
| surface-dim, surface-bright | Add-on surfaces that keep their relative brightness in both themes | on-surface | `<x-surface level="surface-dim">`, `<x-surface level="surface-bright">` |
|
||||
| primary-fixed, primary-fixed-dim, on-primary-fixed, on-primary-fixed-variant (and secondary, tertiary) | Add-on roles with the same tone in light and dark; for a colour that must not change with the theme; never where contrast matters | — | `var(--md-sys-color-primary-fixed)` with `var(--md-sys-color-on-primary-fixed)` |
|
||||
|
||||
Pairing: a role's `on-` pair is the only combination whose contrast is guaranteed at every contrast level. A `primary` fill under `on-primary` text and a `secondary-container` fill under `on-secondary-container` are right; `primary-container` under `on-surface`, or `secondary-container` under `primary`, are not, and break as the contrast level rises. A component sets its own pair; the application's CSS writes both halves (`background-color: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary)`). Google: "Pair and layer color roles only as intended … Don't mix roles improperly."
|
||||
|
||||
### Surface containers
|
||||
|
||||
A hierarchy of emphasis, not of height: the tone separates panels before any shadow does, and a region keeps its role at every breakpoint (body always `surface`, navigation always `surface-container`).
|
||||
|
||||
| Role | Use | In this library |
|
||||
| --- | --- | --- |
|
||||
| surface | The page | the page itself, `<x-surface level="surface">` |
|
||||
| surface-container-lowest | The most recessed panel; an elevated card's body in dark themes | `<x-surface level="surface-container-lowest">` |
|
||||
| surface-container-low | An elevated card, a modal bottom or side sheet, the full-screen search view | `<x-surface level="surface-container-low">` |
|
||||
| surface-container | Navigation bar and rail, docked and floating toolbars, menus, the segmented list | `<x-surface>` (the default level) |
|
||||
| surface-container-high | Dialogs, the search bar, date and time pickers, a rich tooltip | `<x-surface level="surface-container-high">` |
|
||||
| surface-container-highest | A filled card, a filled text field, a filled chip's selected state | `<x-surface level="surface-container-highest">` |
|
||||
|
||||
In the application's CSS each is `var(--md-sys-color-surface-container-low)` and so on; the ink on every one of them is `on-surface`.
|
||||
|
||||
### Emphasis and lines
|
||||
|
||||
- Default ink is `on-surface` (`md-ink`); lower emphasis is `on-surface-variant` (`md-ink-variant`); decoration is `outline` (`md-ink-quiet`). Emphasis is never an opacity: M3 reserves 38% (`--md-sys-state-disabled-content-opacity`) for disabled content and 12% (`--md-sys-state-disabled-container-opacity`) for a disabled container.
|
||||
- `outline` for a boundary that has to be perceived (a text field's edge, a target's edge — 3:1 against the surface); `outline-variant` for dividers and the edge of a card or any component holding several elements. Google: "Don't use the outline color for dividers … use outline variant instead." `outline-variant` may edge a chip or a button only because the content inside already carries the contrast.
|
||||
- A hyperlink in running text is `primary` (or `tertiary` for a quieter link) **and** underlined: `md-link` with `md-ink-primary`.
|
||||
|
||||
### Contrast
|
||||
|
||||
| Level | Target | How |
|
||||
| --- | --- | --- |
|
||||
| Standard | Hierarchy from high- and low-contrast elements together; text 4.5:1, large text and icons 3:1, grouped non-text controls 3:1 | the default scheme |
|
||||
| Medium | 3:1 minimum everywhere, without halation | `<html data-contrast="medium">` |
|
||||
| High | 7:1 | `<html data-contrast="high">`, or the visitor's OS setting (`theme.contrast.default` = `system`) |
|
||||
|
||||
Every role changes with the level automatically; a component built from roles needs nothing else. Disabled states are exempt from contrast. A colour outside the roles (a hex, white, black) does not change and is the one thing that breaks a contrast level.
|
||||
|
||||
Sources: https://m3.material.io/styles/color/roles · https://m3.material.io/styles/color/system/how-the-system-works · https://m3.material.io/styles/color/advanced/apply-colors · https://m3.material.io/foundations/designing/color-contrast
|
||||
|
||||
## Surfaces and elevation
|
||||
|
||||
M3 separates surfaces by tone first; a shadow says that something floats over the content or is being interacted with. "When it comes to applying shadows, less is more."
|
||||
|
||||
| Level | Shadow | Rests here | In this library |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 | none | The page, cards (filled, outlined), buttons (filled, tonal, outlined), button groups, icon buttons, lists, chips, tabs, sliders, the rail, a docked side sheet, a carousel, a full-screen dialog, a FAB inside the rail, an app bar at rest | — |
|
||||
| 1 | 1dp | Elevated cards, elevated buttons and chips, modal bottom and side sheets, a banner | `box-shadow: var(--md-sys-elevation-1)` |
|
||||
| 2 | 3dp | Menus, the navigation bar, a scrolled app bar, toolbars, rich tooltips | `var(--md-sys-elevation-2)` |
|
||||
| 3 | 6dp | FAB and extended FAB, the FAB menu's close button, dialogs, date and time pickers, the search bar | `var(--md-sys-elevation-3)` |
|
||||
| 4 | 8dp | Interaction only: a level-3 element on hover or while dragged | `var(--md-sys-elevation-4)` |
|
||||
| 5 | 12dp | Interaction only | `var(--md-sys-elevation-5)` |
|
||||
|
||||
- Hover lifts an element one level (a FAB 3 → 4, an elevated card 1 → 2); focus and selection may too; a raised element lowers when something higher appears.
|
||||
- Overlapping panels take different surface-container roles to show separation; the roles are not tied to the levels.
|
||||
- A scrim (`scrim` at 32%) brings focus to a modal over a large surface; it is never a substitute for a shadow on a small floating element.
|
||||
- On a dark surface a shadow is nearly invisible, so the tone does the work there.
|
||||
|
||||
Sources: https://m3.material.io/styles/elevation/overview · https://m3.material.io/styles/elevation/applying-elevation · https://m3.material.io/styles/elevation/tokens
|
||||
|
||||
## Shape
|
||||
|
||||
### The corner scale
|
||||
|
||||
| Style | Value | In this library |
|
||||
| --- | --- | --- |
|
||||
| None | 0 | `var(--md-sys-shape-corner-none)`, `corner="none"` |
|
||||
| Extra small | 4px | `var(--md-sys-shape-corner-xs)`, `corner="xs"` |
|
||||
| Small | 8px | `var(--md-sys-shape-corner-sm)`, `corner="sm"` |
|
||||
| Medium | 12px | `var(--md-sys-shape-corner-md)`, `corner="md"` |
|
||||
| Large | 16px | `var(--md-sys-shape-corner-lg)`, `corner="lg"` |
|
||||
| Large increased | 20px | `var(--md-sys-shape-corner-lg-increased)`, `corner="lg-increased"` |
|
||||
| Extra large | 28px | `var(--md-sys-shape-corner-xl)`, `corner="xl"` |
|
||||
| Extra large increased | 32px | `var(--md-sys-shape-corner-xl-increased)`, `corner="xl-increased"` |
|
||||
| Extra extra large | 48px | `var(--md-sys-shape-corner-xxl)`, `corner="xxl"` |
|
||||
| Full | a stadium or circle | `var(--md-sys-shape-corner-full)`, `corner="full"` |
|
||||
|
||||
`corner` is `<x-surface>`'s prop. In the application's CSS a corner is `border-radius` on a token, and one side at a time a logical longhand (`border-start-start-radius` and `border-start-end-radius` for a bottom sheet's top); a length of your own is off the scale.
|
||||
|
||||
### Corner by component
|
||||
|
||||
| Component | Corner | Note |
|
||||
| --- | --- | --- |
|
||||
| Buttons, icon buttons, split button (outer), FAB menu items | full | a press morphs to `md` (xs/sm sizes), `lg` (md), `xl` (lg/xl); a selected toggle swaps round ↔ square |
|
||||
| Connected button group | full outside, `sm` between segments | segments press to `xs` |
|
||||
| FAB | `lg` 16 (baseline 56px), `lg-increased` 20 (medium 80px), `xl` 28 (large 96px) | extended FAB `lg` |
|
||||
| Chips | `sm` 8 | an avatar in a chip `md` 12 |
|
||||
| Cards | `md` 12 | no change on hover |
|
||||
| Text fields | `xs` 4 (outlined: all corners; filled: top corners only) | |
|
||||
| Menus, snackbar, plain tooltip | `xs` 4 | the Expressive vertical menu rounds the focused item |
|
||||
| Rich tooltip | `md` 12 | |
|
||||
| Dialogs | `xl` 28 | full-screen dialog `none` |
|
||||
| Bottom sheet | `xl` 28 on top | |
|
||||
| Side sheet | `lg` 16 on the inner side | |
|
||||
| Search bar | full | search view `xl` 28 when docked, `none` full-screen |
|
||||
| Date and time pickers | `xl` 28 | date cells full |
|
||||
| Carousel items | `xl` 28 | |
|
||||
| Navigation indicator, badges, switch, slider handle, checkbox state layer | full | checkbox box 2px, tab indicator 3px on top |
|
||||
| Navigation bar, app bar, docked toolbar, tabs | none | floating toolbar full |
|
||||
| Segmented list rows | `xs` inner, `lg` outer; a selected row `lg` | |
|
||||
|
||||
### Rules
|
||||
|
||||
- Optical roundness: a shape nested in a rounded container takes inner radius = outer radius − padding (48 − 14 = 34), never the container's own radius.
|
||||
- Large and full corners do not belong on information-dense containers (cards, tables, text fields).
|
||||
- A press squares a round shape and rounds a square one (the components carry the morph on the fast spatial spring); nothing morphs on hover.
|
||||
- The 35 Expressive shapes (`<x-shape name="cookie-9">`, also the loading indicator and the standard button group's press shape) are decoration for emphasis and delight — never a carrier of meaning, never behind text-heavy content, and used sparingly.
|
||||
|
||||
Sources: https://m3.material.io/styles/shape/corner-radius-scale · https://m3.material.io/styles/shape/shape-morph · https://m3.material.io/styles/shape/overview-principles
|
||||
|
||||
## Type
|
||||
|
||||
The typeface is Google Sans Flex for brand and plain styles (`--md-ref-typeface-brand`, `--md-ref-typeface-plain`); an application may replace it after importing the stylesheet. Each style is one class that sets size, line height, weight, family and tracking together — or, in the application's CSS, `font: var(--md-sys-typescale-body-md)` with `letter-spacing: var(--md-sys-typescale-body-md-tracking)`. A size, weight, line height or letter spacing of your own is off the scale.
|
||||
|
||||
| Role | Style | Size / line | Weight | In this library | Use for |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Display | large / medium / small | 57/64 · 45/52 · 36/44 | 400 | `md-type-display-lg` … | hero figures, one short marketing line; never running text |
|
||||
| Headline | large / medium / small | 32/40 · 28/36 · 24/32 | 400 | `md-type-headline-lg` … | page titles, section titles, a dialog's headline (`headline-sm`) |
|
||||
| Title | large / medium / small | 22/28 · 16/24 · 14/20 | 400 / 500 / 500 | `md-type-title-lg` … | app bar title (`lg`), card and list-section titles (`md`), dense headers (`sm`) |
|
||||
| Body | large / medium / small | 16/24 · 14/20 · 12/16 | 400 | `md-type-body-lg` … | paragraphs (`lg` for reading, `md` in components), supporting text (`sm`) |
|
||||
| Label | large / medium / small | 14/20 · 12/16 · 11/16 | 500 | `md-type-label-lg` … | buttons and tabs (`lg`), chips and navigation (`md`), captions and badges (`sm`) |
|
||||
|
||||
- `md-type-emphasized-*` (`--md-sys-typescale-emphasized-*`) is the same size and line height one weight step heavier (400 → 500, 500 → 700), fully rounded in Google Sans Flex, with its own tracking. M3 uses it deliberately, never by default: a selected list or menu item, a button's label on a primary action, an extended FAB, a badge, a headline given editorial weight.
|
||||
- Tracking follows Compose's `TypeScaleTokens`: display-large −0.2, title-medium 0.2, title-small 0.1, body-large 0.5, body-medium 0.2, body-small 0.4, label-large 0.1, label-medium and small 0.5 (sp; rem = sp/16); the emphasized set tightens a few (display-large 0, title-medium 0.15, body-large 0.15, body-medium 0.25).
|
||||
- Line length 40–60 characters (`max-inline-size: 60ch` in the application's CSS). Figures that change take `md-tabular`.
|
||||
- Text must scale to 200%: containers grow, side-by-side controls stack, padding stays; components without text (progress, checkboxes) do not scale. Truncate to an ellipsis (`md-truncate`) only when the full text is one tooltip or link away.
|
||||
- When customising, change the typeface or tracking, never the sizes: component layout depends on them.
|
||||
|
||||
Sources: https://m3.material.io/styles/typography/type-scale-tokens · https://m3.material.io/styles/typography/applying-type · https://m3.material.io/styles/typography/fonts · https://m3.material.io/foundations/writing/text-resizing · https://m3.material.io/foundations/writing/text-truncation
|
||||
|
||||
## Motion
|
||||
|
||||
M3 Expressive moves on physics: every transition is a spring, and the library samples each spring into a CSS `linear()` easing paired with a duration. Use the pair together, or the curve is stretched over the wrong time.
|
||||
|
||||
| Spring | Damping / stiffness | Duration | In this library | For |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Spatial fast | 0.6 / 800 | 350ms | `var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast)` | small elements: a button's press morph, a switch, a chip |
|
||||
| Spatial default | 0.8 / 380 | 500ms | `var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default)` | most position, size and shape changes |
|
||||
| Spatial slow | 0.8 / 200 | 650ms | `var(--md-sys-motion-spatial-slow-duration) var(--md-sys-motion-spatial-slow)` | large surfaces: a sheet, a pane, a full-screen transition |
|
||||
| Effects fast | 1.0 / 3800 | 150ms | `var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast)` | state layers, small fades |
|
||||
| Effects default | 1.0 / 1600 | 200ms | `var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-effects-default)` | most colour and opacity changes |
|
||||
| Effects slow | 1.0 / 800 | 300ms | `var(--md-sys-motion-effects-slow-duration) var(--md-sys-motion-effects-slow)` | large fades, a scrim |
|
||||
|
||||
A transition names the property, then the pair: `transition: transform var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default), opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast)`.
|
||||
|
||||
- Spatial springs are underdamped and overshoot — that bounce is what reads as Expressive — so they carry only position, size and shape. Effects springs are critically damped and carry colour and opacity, which must never overshoot. A transition on `all` mixes the two and is wrong.
|
||||
- The Standard motion scheme (`<html data-motion="standard">`, config `motion.scheme`) swaps the spatial springs for stiffer ones with almost no bounce (0.9 / 1400, 700, 300; 350, 500, 750ms) for utilitarian products; effects are shared.
|
||||
- Direction: something entering decelerates (`--md-sys-motion-easing-emphasized-decelerate`, or a spatial spring from off-screen), a permanent exit accelerates (`--md-sys-motion-easing-emphasized-accelerate`), a temporary exit that can be recalled (a drawer, a sheet) takes `--md-sys-motion-easing-emphasized`; exits are shorter than entrances, and larger areas move longer.
|
||||
- The cubic-bezier set (`--md-sys-motion-easing-standard`, `-emphasized`, `-emphasized-decelerate`, `-emphasized-accelerate`, with `--md-sys-motion-duration-short|medium|long`) is for the few transitions whose duration is fixed from outside: a view transition, an animated scroll.
|
||||
- Reduced motion zeroes every duration token, so anything animated through them turns instant; a literal `300ms`, or a keyframe animation with its own timing, ignores the visitor's setting and is a bug. Container transforms, parallax and expansions are removed, not slowed.
|
||||
|
||||
Sources: https://m3.material.io/styles/motion/overview · https://m3.material.io/styles/motion/overview/specs · https://m3.material.io/styles/motion/easing-and-duration/tokens-specs · https://m3.material.io/styles/motion/transitions/transition-patterns
|
||||
|
||||
## States and targets
|
||||
|
||||
| State | Layer | Class or hook | Also |
|
||||
| --- | --- | --- | --- |
|
||||
| Enabled | none | — | |
|
||||
| Hover | 8% of the content colour | `md-state-layer` (pointer devices only) | one level of elevation on floating elements |
|
||||
| Focused | 10% | `md-state-layer md-focus-ring` (keyboard focus: a 3px `secondary` ring, 2px out) | only one focused element at a time |
|
||||
| Pressed | 10% | `md-state-layer` (`:active`) | the shape morph on buttons |
|
||||
| Dragged | 16% | `md-state-layer` with `data-md-dragged` | one level of elevation |
|
||||
| Disabled | content 38%, container 12%, no state layer, not focusable | `color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent)`, and the container likewise with `--md-sys-state-disabled-container-opacity` | exempt from contrast; a FAB is hidden rather than disabled |
|
||||
| Selected | the `secondary-container` pair, a filled icon, the emphasized style | component props (`selected`, `aria-selected`, `aria-pressed`) | combines with hover, focus and press |
|
||||
|
||||
- The state layer takes the content's `on-` colour (on `secondary-container` it is `on-secondary-container`), is 40px on a 48px target, and only one shows at a time. `md-state-layer` draws it in `currentColor` as a `::before`, so the element becomes `position: relative`.
|
||||
- Every state shows two indicators, so a colour change alone is never a state: add a shape, an outline, an icon, a weight or a word (`aria-selected` plus the container, an error colour plus an icon and a message).
|
||||
- Targets: 48×48px minimum, 8px between targets, on every device; `md-touch-target` extends a smaller drawing to 48px. Density is an opt-in prop (`dense`) that steps padding by 4px and never applies to menus, snackbars, dialogs or settings controls, and never takes a target below 48px.
|
||||
- Keyboard: Tab and Shift+Tab between components in DOM order, arrows within a component (menu, tabs, grid, radio group), Enter and Space activate, Escape dismisses; a dialog moves focus in on open and back to its opener on close.
|
||||
|
||||
Sources: https://m3.material.io/foundations/interaction/states/state-layers · https://m3.material.io/foundations/interaction/states/applying-states · https://m3.material.io/foundations/designing/structure · https://m3.material.io/foundations/layout/grids-spacing/density
|
||||
|
||||
## Layout and breakpoints
|
||||
|
||||
Layout keys on the width of the window, in M3's five window size classes and only those. A layout component names the class in a prop (`hide-below`, `hide-from`, `stack-below`, `<x-grid>`'s `columns` map); the application's CSS writes the width as a range media query; a script asks `resources/js/breakpoints.js` (`from('expanded')`, `upTo('medium')`) for the same numbers.
|
||||
|
||||
| Class | Width | Prop value · CSS | Navigation | Panes | Dialogs and choices | Margins |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| Compact | below 600px | the default; `hide-from="medium"` for "only here" · `@media (width < 600px)` | navigation bar; the rail opens as a modal | 1 | full-screen or basic dialog; a bottom sheet for choices | 16px |
|
||||
| Medium | 600–839px | `medium` · `@media (width >= 600px)` | collapsed rail (96px) | 1, or 2 for low-density content at 50% each | basic dialog; a menu for choices | 24px |
|
||||
| Expanded | 840–1199px | `expanded` · `@media (width >= 840px)` | rail, collapsed or expanded, collapsible | 2 recommended; a fixed pane 360px | basic dialog; menu | 24px |
|
||||
| Large | 1200–1599px | `large` · `@media (width >= 1200px)` | rail expanded | 2; a fixed pane 412px | basic dialog; menu | 24px |
|
||||
| Extra-large | 1600px and up | `extra-large` · `@media (width >= 1600px)` | rail expanded | 2, or 3 with a standard side sheet (at most 400px) | basic dialog; menu | 24px |
|
||||
|
||||
- `<x-scaffold>` implements the navigation column; `<x-pane>` is a content region with the margins above; `<x-list-detail>` is the second pane of a list-detail layout from expanded, `<x-supporting-pane>` puts a supporting pane (360px, beside the focus pane) from expanded and below it before that. Moving up a class, ask what to reveal, divide into panes, resize, reposition or swap — never swap a component for one that does not do the same job.
|
||||
- Scaffold: bars (app bar at the top, navigation bar at the bottom: 3–5 destinations), rails (the navigation rail, toolbars, the FAB, on the leading edge), panes (all content), around a safety region that stays clear of the device's own chrome (`--material-safe-top|bottom|left|right`).
|
||||
- Canonical layouts: feed (`<x-feed>`, a grid of cards that gains columns as the room grows), list-detail (one pane on compact, two from expanded; a back button only in single-pane mode, a selected row only in two-pane mode), supporting pane (two thirds focus, one third support).
|
||||
- Bidirectionality: write logical properties (`padding-inline-start`, `margin-inline-end`, `inset-inline-start`, `border-inline-start`, `md-text-start`); `<x-row>` runs in the inline direction and mirrors by itself; leading and trailing icons swap, directional icons (back, send) mirror, the rail moves to the right; charts, media controls, clocks and Hebrew progress bars stay left-to-right.
|
||||
|
||||
Sources: https://m3.material.io/foundations/layout/breakpoints/overview · https://m3.material.io/foundations/layout/breakpoints/compact (medium, expanded, large-extra-large) · https://m3.material.io/foundations/layout/scaffold/overview · https://m3.material.io/foundations/layout/canonical-examples/overview · https://m3.material.io/foundations/layout/bidirectionality-rtl
|
||||
|
||||
## Spacing
|
||||
|
||||
M3's spacing tokens are multiples of an 8px base on a 4px grid. A layout component takes the token's name (`gap="space200"`, `<x-surface padding="space300">`); the application's CSS reads it (`var(--md-sys-measurement-space200)`).
|
||||
|
||||
| Token | Value | In this library |
|
||||
| --- | --- | --- |
|
||||
| space25 | 2px | `space25` |
|
||||
| space50 | 4px | `space50` |
|
||||
| space75 | 6px | `space75` |
|
||||
| space100 | 8px (the base) | `space100` |
|
||||
| space125 | 10px | `space125` |
|
||||
| space200 | 16px | `space200` — a component's padding, compact margins |
|
||||
| space300 | 24px | `space300` — a dialog's padding, margins from medium |
|
||||
| space400 | 32px | `space400` |
|
||||
| space500 | 40px | `space500` |
|
||||
| space600 | 48px | `space600` — a target |
|
||||
| space700 | 56px | `space700` |
|
||||
| space800 | 64px | `space800` |
|
||||
| space900 | 72px | `space900` |
|
||||
|
||||
- Padding and gaps live on the parent (`<x-surface padding="space200">` around `<x-stack gap="space100">`), never as margins on children; a margin is for space beyond a container's padding or between layout regions.
|
||||
- Spacing does not scale with text: at 200% text size the same padding and gaps stay.
|
||||
- Name a gap by what it separates when a component has several (icon–label 8px, label–supporting text 4px).
|
||||
|
||||
Sources: https://m3.material.io/styles/spacing/overview · https://m3.material.io/styles/spacing/tokens · https://m3.material.io/styles/spacing/applying-spacing
|
||||
|
||||
## Icons
|
||||
|
||||
`<x-icon name="lock">` draws a Material Symbol Rounded (weight 400, grade 0), outlined or `filled`, at optical size 24 or 20.
|
||||
|
||||
| Axis | Values | In this library |
|
||||
| --- | --- | --- |
|
||||
| Fill | 0 outlined, 1 filled | `filled` — active, selected or on state (a selected navigation item, a FAB's icon, a checked filter chip) |
|
||||
| Weight | 100–700; never below 200 at 24px | 400 for every icon; one weight per group |
|
||||
| Grade | −25 on dark backgrounds, 0 otherwise, positive for emphasis | 0 |
|
||||
| Optical size | 20 dense, 24 standard, 40–48 with display type | `size="20"` and below pick the 20 cut (small buttons, chips, dense lists); `optical="20"` for an icon sized by the application's own CSS |
|
||||
|
||||
- An icon beside text takes the text's size and colour (`size="20"` beside `md-type-label-lg`, 24 beside body) and the same optical weight; its baseline sits about 11.5% of the text size below the text's.
|
||||
- Icons stay flat and forward-facing, on the pixel grid, inside their 20px live area of the 24px canvas.
|
||||
- An icon-only control has an accessible name (`aria-label`, or a tooltip that names it); a decorative icon is `aria-hidden`; a complex icon drawn below 20px needs a label beside it.
|
||||
|
||||
Sources: https://m3.material.io/styles/icons/overview · https://m3.material.io/styles/icons/designing-icons · https://m3.material.io/styles/icons/applying-icons
|
||||
|
||||
## Accessibility
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
The rules, tables and wording here are Google's, condensed from the Material Design 3 documentation at https://m3.material.io (Foundations, Styles and Components), which Google publishes under the Creative Commons Attribution 4.0 License except as otherwise noted; the numeric token values are from the Android Open Source Project's Material 3 token files in androidx Compose (Apache License 2.0). Copyright Google LLC; Copyright The Android Open Source Project. The library's `NOTICE` records the same. Dates and page names are those of the site as read on 2026-09-13; the full extracted references, with every source page, are kept in the package repository under `docs/reference/m3/`.
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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 — 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
|
||||
* every component's rules whether an application's views render them or not. The recommended path
|
||||
* is `foundation.css` (the one required import) followed by only the component stylesheets a
|
||||
* view actually renders — each component file already imports the stylesheets of the components
|
||||
* it draws, so the list an application writes stays short. `all.css` is for the showcase, for an
|
||||
* error page rendered without a build, and for an application that would rather not track
|
||||
* per-component imports at all.
|
||||
*
|
||||
* Same shape as every package stylesheet: the layer statement first, then plain imports, nothing
|
||||
* else at the top level — an `@import` cannot sit inside a `@layer` block, and `layer()` on it
|
||||
* would nest the layer a second time inside a file that already declares it.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './foundation.css';
|
||||
|
||||
/* Layout */
|
||||
@import './layout/spacing.css';
|
||||
@import './layout/visibility.css';
|
||||
@import './layout/stack.css';
|
||||
@import './layout/row.css';
|
||||
@import './layout/grid.css';
|
||||
@import './layout/surface.css';
|
||||
@import './layout/pane.css';
|
||||
@import './layout/list-detail.css';
|
||||
@import './layout/supporting-pane.css';
|
||||
@import './layout/feed.css';
|
||||
@import './layout/scaffold.css';
|
||||
|
||||
/* Foundation components */
|
||||
@import './components/icon.css';
|
||||
@import './components/shape.css';
|
||||
@import './components/color.css';
|
||||
|
||||
/* Actions and communication */
|
||||
@import './components/loading.css';
|
||||
@import './components/tooltip.css';
|
||||
@import './components/badge.css';
|
||||
@import './components/button.css';
|
||||
@import './components/group.css';
|
||||
@import './components/button-group.css';
|
||||
@import './components/split-button.css';
|
||||
@import './components/fab.css';
|
||||
@import './components/menu-separator.css';
|
||||
@import './components/menu-group.css';
|
||||
@import './components/menu-item.css';
|
||||
@import './components/menu.css';
|
||||
@import './components/fab-menu-item.css';
|
||||
@import './components/fab-menu.css';
|
||||
@import './components/rich-tooltip.css';
|
||||
@import './components/toast.css';
|
||||
@import './components/progress.css';
|
||||
@import './components/alert.css';
|
||||
@import './components/stat.css';
|
||||
@import './components/empty-state.css';
|
||||
|
||||
/* Inputs, selection and data */
|
||||
@import './components/form.css';
|
||||
@import './components/field.css';
|
||||
@import './components/input.css';
|
||||
@import './components/password.css';
|
||||
@import './components/textarea.css';
|
||||
@import './components/select.css';
|
||||
@import './components/file.css';
|
||||
@import './components/selection.css';
|
||||
@import './components/checkbox.css';
|
||||
@import './components/radio.css';
|
||||
@import './components/toggle.css';
|
||||
@import './components/chip.css';
|
||||
@import './components/chip-set.css';
|
||||
@import './components/choices.css';
|
||||
@import './components/slider.css';
|
||||
@import './components/search.css';
|
||||
@import './components/table.css';
|
||||
@import './components/sort-header.css';
|
||||
@import './components/pagination.css';
|
||||
@import './components/datepicker.css';
|
||||
@import './components/timepicker.css';
|
||||
|
||||
/* Containment */
|
||||
@import './components/divider.css';
|
||||
@import './components/collapse.css';
|
||||
@import './components/card.css';
|
||||
@import './components/list-item.css';
|
||||
@import './components/list.css';
|
||||
@import './components/modal.css';
|
||||
@import './components/drawer.css';
|
||||
@import './components/bottom-sheet.css';
|
||||
@import './components/carousel-item.css';
|
||||
@import './components/carousel.css';
|
||||
@import './components/error-page.css';
|
||||
|
||||
/* Navigation */
|
||||
@import './components/app-bar.css';
|
||||
@import './components/toolbar.css';
|
||||
@import './components/tabs.css';
|
||||
@import './components/navigation-item.css';
|
||||
@import './components/navigation-bar.css';
|
||||
@import './components/navigation-bar-item.css';
|
||||
@import './components/navigation-rail.css';
|
||||
@import './components/navigation-rail-item.css';
|
||||
@import './components/navigation-rail-section.css';
|
||||
@import './components/section-nav.css';
|
||||
@import './components/account-menu.css';
|
||||
@import './components/theme-toggle.css';
|
||||
@import './components/scheme-picker.css';
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* <x-account-menu>: the avatar trigger of an `<x-menu>`, its name-and-email header and its theme
|
||||
* 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 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.
|
||||
*
|
||||
* `[data-md-account-menu-header]` is the name-and-email block above the separator; its own type and
|
||||
* ink are the fixed text classes on the view (`md-type-title-sm md-ink`, `md-type-body-sm
|
||||
* md-ink-variant`), since this is prose the caller supplies, not the component's own part.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './menu.css';
|
||||
@import './menu-separator.css';
|
||||
@import './menu-item.css';
|
||||
@import './icon.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-account-menu] {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
inline-size: var(--md-sys-measurement-space500);
|
||||
block-size: var(--md-sys-measurement-space500);
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
cursor: pointer;
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-account-menu-avatar] {
|
||||
position: relative;
|
||||
z-index: -2;
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
[data-md-account-menu-header] {
|
||||
padding-inline: 12px;
|
||||
padding-block: var(--md-sys-measurement-space100) 12px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* <x-alert>: a notice in the page — not an M3 component (M3's banner is gone), drawn in M3's own
|
||||
* terms as the plan's deliberate-deviations note records: the state's container colour (tinted,
|
||||
* never filled), a medium corner, the state's icon, title-small/body-medium text and an actions
|
||||
* row of text buttons pulled back 12px to the text's edge, as the rich tooltip's actions row is
|
||||
* (rich-tooltip.css).
|
||||
*
|
||||
* `data-md-color`: info (the default), success, warning, error, primary, secondary, tertiary or
|
||||
* 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; 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] {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
border-radius: var(--md-sys-shape-corner-md);
|
||||
padding: var(--md-sys-measurement-space200);
|
||||
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'] {
|
||||
background-color: var(--md-sys-color-surface-container-high);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
[data-md-alert-content] {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
[data-md-alert-content] > * + * {
|
||||
margin-top: var(--md-sys-measurement-space50);
|
||||
}
|
||||
|
||||
[data-md-alert-title] {
|
||||
font: var(--md-sys-typescale-title-sm);
|
||||
letter-spacing: var(--md-sys-typescale-title-sm-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-alert-description] {
|
||||
font: var(--md-sys-typescale-body-md);
|
||||
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-alert-actions] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
margin-inline-start: -12px;
|
||||
padding-top: var(--md-sys-measurement-space50);
|
||||
}
|
||||
|
||||
[data-md-alert-dismiss] {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--md-sys-measurement-space500);
|
||||
height: var(--md-sys-measurement-space500);
|
||||
margin: -8px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
@@ -1,66 +1,93 @@
|
||||
/*
|
||||
* M3 Expressive's top app bars (AppBarTokens, AppBarSmallTokens, AppBarMediumFlexibleTokens,
|
||||
* AppBarLargeFlexibleTokens, androidx Compose Material 3, Apache-2.0).
|
||||
* `<x-app-bar>`: M3 Expressive's top app bars (AppBarTokens.kt, AppBarSmallTokens.kt,
|
||||
* AppBarMediumFlexibleTokens.kt, AppBarLargeFlexibleTokens.kt, androidx Compose Material 3,
|
||||
* Apache-2.0; docs/reference/m3/components-navigation-selection-inputs.md § Top App Bar).
|
||||
*
|
||||
* [data-md-app-bar] the bar; data-md-variant, data-md-sticky, data-md-subtitled,
|
||||
* data-md-scrolled/data-md-collapsed (resources/js/app-bar.js)
|
||||
* [data-md-app-bar-row] 64px, sticky at the top of the bar
|
||||
* [data-md-app-bar-leading] the `navigation` slot
|
||||
* [data-md-app-bar-headline] [data-md-app-bar-title], [data-md-app-bar-subtitle]
|
||||
* [data-md-app-bar-search] the `search` variant's slot
|
||||
* [data-md-app-bar-trailing] [data-md-app-bar-action], [data-md-app-bar-overflow]
|
||||
* [data-md-app-bar-expanded] medium/large's big title, under the row
|
||||
*
|
||||
* 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
|
||||
* center the same, the title centred — a three-column grid, so it shrinks against whatever is
|
||||
* 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
|
||||
* 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
|
||||
* negative top, so it scrolls up until only 64px of it remain, while its row of icons is sticky at
|
||||
* 0 inside it and stays put — the big title scrolls away under the row. Script only says when that
|
||||
* has happened (resources/js/app-bar.js), so the small title can fade in and the colour change.
|
||||
*
|
||||
* Trailing actions given as a list collapse into an overflow menu below `medium` and come back
|
||||
* from it, by media query alone (the site's "trailing actions collapse into an overflow menu at
|
||||
* smaller breakpoints" and "become visible again at larger sizes", read together with "up to two
|
||||
* icon buttons" and, for a wider window, "up to four trailing icons"): both forms of every action
|
||||
* are in the page, and each width hides the one it does not use, so nothing flashes and no script
|
||||
* counts.
|
||||
*/
|
||||
|
||||
@layer components {
|
||||
[data-app-bar] {
|
||||
--app-bar-row: 4rem;
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './button.css';
|
||||
@import './menu.css';
|
||||
@import './menu-item.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-app-bar] {
|
||||
--app-bar-row: 64px;
|
||||
--app-bar-height: var(--app-bar-row);
|
||||
|
||||
z-index: 20;
|
||||
display: block;
|
||||
min-height: var(--app-bar-height);
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-top: var(--material-safe-top, env(safe-area-inset-top));
|
||||
background-color: var(--md-sys-color-surface);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
transition: background-color var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-effects-default);
|
||||
transition-property: background-color;
|
||||
transition-duration: var(--md-sys-motion-effects-default-duration);
|
||||
transition-timing-function: var(--md-sys-motion-effects-default);
|
||||
}
|
||||
|
||||
[data-app-bar][data-variant="medium"] {
|
||||
--app-bar-height: 7rem;
|
||||
[data-md-app-bar][data-md-variant='medium'] {
|
||||
--app-bar-height: 112px;
|
||||
}
|
||||
|
||||
[data-app-bar][data-variant="medium"][data-subtitled] {
|
||||
--app-bar-height: 8.5rem;
|
||||
[data-md-app-bar][data-md-variant='medium'][data-md-subtitled] {
|
||||
--app-bar-height: 136px;
|
||||
}
|
||||
|
||||
[data-app-bar][data-variant="large"] {
|
||||
--app-bar-height: 7.5rem;
|
||||
[data-md-app-bar][data-md-variant='large'] {
|
||||
--app-bar-height: 120px;
|
||||
}
|
||||
|
||||
[data-app-bar][data-variant="large"][data-subtitled] {
|
||||
--app-bar-height: 9.5rem;
|
||||
[data-md-app-bar][data-md-variant='large'][data-md-subtitled] {
|
||||
--app-bar-height: 152px;
|
||||
}
|
||||
|
||||
[data-app-bar][data-sticky] {
|
||||
[data-md-app-bar][data-md-sticky] {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/* `--app-bar-measured` is the bar's real height (a title can wrap), set by app-bar.js. */
|
||||
[data-app-bar][data-sticky]:is([data-variant="medium"], [data-variant="large"]) {
|
||||
[data-md-app-bar][data-md-sticky]:is([data-md-variant='medium'], [data-md-variant='large']) {
|
||||
top: calc(var(--app-bar-row) - var(--app-bar-measured, var(--app-bar-height)));
|
||||
}
|
||||
|
||||
[data-app-bar][data-scrolled] {
|
||||
[data-md-app-bar][data-md-scrolled] {
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
}
|
||||
|
||||
[data-app-bar-row] {
|
||||
[data-md-app-bar-row] {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
@@ -68,40 +95,62 @@
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
min-height: var(--app-bar-row);
|
||||
padding-inline: 0.25rem;
|
||||
padding-inline: var(--md-sys-measurement-space50);
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
[data-app-bar-leading],
|
||||
[data-app-bar-trailing] {
|
||||
[data-md-app-bar-leading],
|
||||
[data-md-app-bar-trailing] {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-app-bar-leading] {
|
||||
[data-md-app-bar-leading] {
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
[data-app-bar-trailing] {
|
||||
[data-md-app-bar-trailing] {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
[data-app-bar-headline] {
|
||||
/* `actions` as a list: every action is both an icon button and a menu item, and the width of
|
||||
the window says which one is drawn — up to two trailing icon buttons below `medium` and four
|
||||
from it, an overflow button counted among them. The wrappers stay out of the row's layout
|
||||
until a width hides them. */
|
||||
[data-md-app-bar-action],
|
||||
[data-md-app-bar-overflow] {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
[data-md-app-bar-action][data-md-overflows-compact],
|
||||
[data-md-app-bar-overflow='medium'] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 600px) {
|
||||
[data-md-app-bar-overflow='compact'] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-app-bar-headline] {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1 1 0%;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-inline: 0.75rem;
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
[data-app-bar-row]:has([data-app-bar-leading]) [data-app-bar-headline] {
|
||||
padding-inline-start: 0.25rem;
|
||||
[data-md-app-bar-row]:has([data-md-app-bar-leading]) [data-md-app-bar-headline] {
|
||||
padding-inline-start: var(--md-sys-measurement-space50);
|
||||
}
|
||||
|
||||
[data-app-bar-title] {
|
||||
[data-md-app-bar-title] {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -109,7 +158,7 @@
|
||||
letter-spacing: var(--md-sys-typescale-title-lg-tracking);
|
||||
}
|
||||
|
||||
[data-app-bar-subtitle] {
|
||||
[data-md-app-bar-subtitle] {
|
||||
overflow: hidden;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
text-overflow: ellipsis;
|
||||
@@ -118,70 +167,94 @@
|
||||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||||
}
|
||||
|
||||
/* Centred: the headline spans the row and centres its text between whatever is at either end. */
|
||||
[data-app-bar][data-variant="center"] [data-app-bar-headline] {
|
||||
position: absolute;
|
||||
inset-inline: 3.5rem;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
/* 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. */
|
||||
[data-md-app-bar][data-md-variant='center'] [data-md-app-bar-row] {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
}
|
||||
|
||||
/* The search bar fills the row. */
|
||||
[data-app-bar-search] {
|
||||
[data-md-app-bar][data-md-variant='center'] [data-md-app-bar-leading] {
|
||||
grid-column: 1;
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
[data-md-app-bar][data-md-variant='center'] [data-md-app-bar-headline] {
|
||||
grid-column: 2;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
justify-self: center;
|
||||
padding-inline: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-md-app-bar][data-md-variant='center'] [data-md-app-bar-trailing] {
|
||||
grid-column: 3;
|
||||
justify-self: end;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
/* 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. 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] {
|
||||
min-width: 0;
|
||||
max-width: calc(312px + (100% - 312px) / 2);
|
||||
flex: 1 1 0%;
|
||||
padding-inline: 0.25rem;
|
||||
margin-inline: auto;
|
||||
padding-inline: var(--md-sys-measurement-space50);
|
||||
}
|
||||
|
||||
/* Medium and large: the row's title is the collapsed one, shown once the big one has gone. */
|
||||
[data-app-bar]:is([data-variant="medium"], [data-variant="large"]) [data-app-bar-row] [data-app-bar-headline] {
|
||||
[data-md-app-bar]:is([data-md-variant='medium'], [data-md-variant='large']) [data-md-app-bar-row] [data-md-app-bar-headline] {
|
||||
opacity: 0;
|
||||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
transition-property: opacity;
|
||||
transition-duration: var(--md-sys-motion-effects-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
[data-app-bar][data-collapsed] [data-app-bar-row] [data-app-bar-headline] {
|
||||
[data-md-app-bar][data-md-collapsed] [data-md-app-bar-row] [data-md-app-bar-headline] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Heights from the tokens: the title (and subtitle) lines and the space under them fill what the
|
||||
row leaves — 36 + 12px medium, 36 + 20 + 16px with a subtitle; 44 + 12px large, 44 + 24 + 20px. */
|
||||
[data-app-bar-expanded] {
|
||||
[data-md-app-bar-expanded] {
|
||||
display: flex;
|
||||
min-height: calc(var(--app-bar-height) - var(--app-bar-row));
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
padding-inline: 1rem;
|
||||
padding-bottom: 0.75rem;
|
||||
padding-inline: var(--md-sys-measurement-space200);
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
[data-app-bar][data-subtitled] [data-app-bar-expanded] {
|
||||
padding-bottom: 1rem;
|
||||
[data-md-app-bar][data-md-subtitled] [data-md-app-bar-expanded] {
|
||||
padding-bottom: var(--md-sys-measurement-space200);
|
||||
}
|
||||
|
||||
[data-app-bar][data-variant="large"][data-subtitled] [data-app-bar-expanded] {
|
||||
padding-bottom: 1.25rem;
|
||||
[data-md-app-bar][data-md-variant='large'][data-md-subtitled] [data-md-app-bar-expanded] {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
[data-app-bar-expanded] [data-app-bar-title] {
|
||||
[data-md-app-bar-expanded] [data-md-app-bar-title] {
|
||||
white-space: normal;
|
||||
font: var(--md-sys-typescale-headline-md);
|
||||
letter-spacing: var(--md-sys-typescale-headline-md-tracking);
|
||||
}
|
||||
|
||||
[data-app-bar-expanded] [data-app-bar-subtitle] {
|
||||
[data-md-app-bar-expanded] [data-md-app-bar-subtitle] {
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
}
|
||||
|
||||
[data-app-bar][data-variant="large"] [data-app-bar-expanded] [data-app-bar-title] {
|
||||
[data-md-app-bar][data-md-variant='large'] [data-md-app-bar-expanded] [data-md-app-bar-title] {
|
||||
font: var(--md-sys-typescale-display-sm);
|
||||
letter-spacing: var(--md-sys-typescale-display-sm-tracking);
|
||||
}
|
||||
|
||||
[data-app-bar][data-variant="large"] [data-app-bar-expanded] [data-app-bar-subtitle] {
|
||||
[data-md-app-bar][data-md-variant='large'] [data-md-app-bar-expanded] [data-md-app-bar-subtitle] {
|
||||
font: var(--md-sys-typescale-title-md);
|
||||
letter-spacing: var(--md-sys-typescale-title-md-tracking);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* <x-badge>: M3's small and large badges, and the library's status label.
|
||||
*
|
||||
* BadgeTokens (androidx Compose Material 3, Apache-2.0): the small badge is a 6px dot
|
||||
* (`data-md-dot`), the large badge 16px tall and at least 16px wide with 4px either side of a
|
||||
* `label-small` count in tabular figures, both fully round and `error` by default. A floating
|
||||
* badge sits on M3's anchor geometry at the top-end corner of its `position: relative` parent: the
|
||||
* dot flush in the corner, the count 2px above it with its start 12px in from the parent's end.
|
||||
*
|
||||
* The status label (`data-md-variant`) is not an M3 badge: 24px tall, small corner, 8px either
|
||||
* side, 4px between an icon and its word, `label-medium`. `tonal` draws it in the colour's
|
||||
* container, `solid` in the colour itself, `outline` as a 1px `outline` edge around
|
||||
* `on-surface-variant` text — `outline` rather than `outline-variant`, because M3 asks a badge for
|
||||
* 3:1 against what is behind it and `outline-variant` is the decorative role dividers use.
|
||||
*
|
||||
* `data-md-color` picks the pair of roles; `neutral` has no hue of its own (`on-surface-variant`
|
||||
* and `surface`, or `surface-container-high` behind `on-surface-variant` text), and `plain` sets
|
||||
* no colour at all, so the caller's CSS paints it.
|
||||
*/
|
||||
|
||||
@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-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;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-dot] {
|
||||
inline-size: 6px;
|
||||
block-size: 6px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
}
|
||||
|
||||
[data-md-badge]:not([data-md-dot], [data-md-variant]) {
|
||||
min-inline-size: 16px;
|
||||
block-size: 16px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
padding-inline: var(--md-sys-measurement-space50);
|
||||
font: var(--md-sys-typescale-label-sm);
|
||||
letter-spacing: var(--md-sys-typescale-label-sm-tracking);
|
||||
font-variation-settings: normal;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-variant] {
|
||||
gap: var(--md-sys-measurement-space50);
|
||||
block-size: 24px;
|
||||
border-radius: var(--md-sys-shape-corner-sm);
|
||||
padding-inline: var(--md-sys-measurement-space100);
|
||||
font: var(--md-sys-typescale-label-md);
|
||||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-variant='outline'] {
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
[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);
|
||||
--md-badge-container: var(--md-sys-color-surface-container-high);
|
||||
--md-badge-on-container: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-md-badge]:not([data-md-color='plain']) {
|
||||
background-color: var(--md-badge-color);
|
||||
color: var(--md-badge-on-color);
|
||||
|
||||
&[data-md-variant='tonal'] {
|
||||
background-color: var(--md-badge-container);
|
||||
color: var(--md-badge-on-container);
|
||||
}
|
||||
|
||||
&[data-md-variant='outline'] {
|
||||
border-color: var(--md-sys-color-outline);
|
||||
background-color: transparent;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-floating] {
|
||||
position: absolute;
|
||||
inset-block-start: calc(-1 * var(--md-sys-measurement-space25));
|
||||
inset-inline-start: calc(100% - 12px);
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-floating][data-md-dot] {
|
||||
inset-block-start: 0;
|
||||
inset-inline: auto 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* `<x-bottom-sheet>`: M3's modal and standard bottom sheets (SheetBottomTokens.kt, androidx
|
||||
* Compose Material 3, Apache-2.0; docs/reference/m3/
|
||||
* components-actions-communication-containment.md § Bottom sheets → Specs).
|
||||
*
|
||||
* [data-md-bottom-sheet] the wrapper; data-md-open, data-md-standard
|
||||
* [data-md-bottom-sheet-scrim] modal only
|
||||
* [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
|
||||
* [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. 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/
|
||||
* drawer.css also uses). `data-md-preset` adds the height-between-stops transition on the spatial
|
||||
* spring, independent of the open/close slide.
|
||||
*
|
||||
* The handle's own 22px top/bottom padding (`SheetDefaults.kt`'s `DragHandleVerticalPadding`) is a
|
||||
* literal — 22 is not on the measurement scale — matching alert.css's 12px for the same reason.
|
||||
* The grip button draws the shared `md-focus-ring` and `md-touch-target` classes rather than a
|
||||
* hand-rolled ring: its own visible bar is not a smaller indicator drawn inside a bigger box those
|
||||
* classes cannot reach (unlike the datepicker's day) — the box they draw *is* the button's whole
|
||||
* hit area. The one refinement is the ring's offset, 4px rather than 2px, as the old handle drew
|
||||
* it: 2px from a 4px-tall bar leaves the ring hard to tell from the bar.
|
||||
*
|
||||
* The scrim fades out as it fades in, and the state rules match the root's own scrim and panel,
|
||||
* so a sheet nested in an open one (a menu's sheet at compact inside a sheet) keeps its own state.
|
||||
* Both keep `x-show`, and the view's `x-transition` holds their `display` through the exit rather
|
||||
* than `allow-discrete`, which Firefox does not honour for `display` (see drawer.css); Alpine holds
|
||||
* for the first `transition-duration` listed, so the closing slide is listed first.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@layer material.components {
|
||||
/* The scrim fades out while the view's `x-transition` holds its `display` (see drawer.css). */
|
||||
[data-md-bottom-sheet-scrim] {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 40;
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent);
|
||||
opacity: 0;
|
||||
transition-property: opacity;
|
||||
transition-duration: var(--md-sys-motion-effects-default-duration);
|
||||
transition-timing-function: var(--md-sys-motion-effects-default);
|
||||
}
|
||||
|
||||
[data-md-bottom-sheet][data-md-open] > [data-md-bottom-sheet-scrim] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
[data-md-bottom-sheet][data-md-open] > [data-md-bottom-sheet-scrim] {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-bottom-sheet-probe] {
|
||||
position: fixed;
|
||||
inset-inline-start: 0;
|
||||
top: 0;
|
||||
width: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-md-bottom-sheet-panel] {
|
||||
position: fixed;
|
||||
inset-inline: 0;
|
||||
bottom: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
margin-inline: auto;
|
||||
flex-direction: column;
|
||||
max-height: var(--sheet-max-height);
|
||||
touch-action: pan-y;
|
||||
border-radius: var(--md-sys-shape-corner-xl) var(--md-sys-shape-corner-xl) var(--md-sys-shape-corner-none) var(--md-sys-shape-corner-none);
|
||||
background-color: var(--md-sys-color-surface-container-low);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
box-shadow: var(--md-sys-elevation-1);
|
||||
padding-bottom: var(--material-safe-bottom, env(safe-area-inset-bottom));
|
||||
translate: 0 100%;
|
||||
transition-property: translate;
|
||||
transition-duration: var(--md-sys-motion-effects-default-duration);
|
||||
transition-timing-function: var(--md-sys-motion-easing-emphasized-accelerate);
|
||||
}
|
||||
|
||||
[data-md-bottom-sheet][data-md-open] > [data-md-bottom-sheet-panel] {
|
||||
translate: 0 0;
|
||||
transition-duration: var(--md-sys-motion-spatial-default-duration);
|
||||
transition-timing-function: var(--md-sys-motion-easing-emphasized-decelerate);
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
[data-md-bottom-sheet][data-md-open] > [data-md-bottom-sheet-panel] {
|
||||
translate: 0 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* With stops the panel is the height of its current one, moving between them on the spatial
|
||||
spring, independent of the translate that opens and closes the sheet. */
|
||||
[data-md-bottom-sheet-panel][data-md-preset] {
|
||||
height: var(--sheet-max-height);
|
||||
transition-property: translate, height;
|
||||
transition-duration: var(--md-sys-motion-effects-default-duration), var(--md-sys-motion-spatial-default-duration);
|
||||
transition-timing-function: var(--md-sys-motion-easing-emphasized-accelerate), var(--md-sys-motion-spatial-default);
|
||||
}
|
||||
|
||||
[data-md-bottom-sheet][data-md-open] > [data-md-bottom-sheet-panel][data-md-preset] {
|
||||
transition-duration: var(--md-sys-motion-spatial-default-duration), var(--md-sys-motion-spatial-default-duration);
|
||||
transition-timing-function: var(--md-sys-motion-easing-emphasized-decelerate), var(--md-sys-motion-spatial-default);
|
||||
}
|
||||
|
||||
[data-md-bottom-sheet-handle] {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
justify-content: center;
|
||||
padding-block: 22px;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
[data-md-bottom-sheet-handle]:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
[data-md-bottom-sheet-grip] {
|
||||
width: 32px;
|
||||
height: 4px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-md-bottom-sheet-grip]:focus-visible {
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
[data-md-bottom-sheet-body] {
|
||||
min-height: 0;
|
||||
flex: 1 1 0%;
|
||||
overflow-y: auto;
|
||||
padding-inline: var(--md-sys-measurement-space300);
|
||||
padding-bottom: var(--md-sys-measurement-space300);
|
||||
}
|
||||
|
||||
[data-md-bottom-sheet-title] {
|
||||
margin-bottom: var(--md-sys-measurement-space200);
|
||||
font: var(--md-sys-typescale-title-lg);
|
||||
letter-spacing: var(--md-sys-typescale-title-lg-tracking);
|
||||
}
|
||||
|
||||
[data-md-bottom-sheet-actions] {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
padding-inline: var(--md-sys-measurement-space300);
|
||||
padding-bottom: var(--md-sys-measurement-space300);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* <x-button-group>: related buttons in a row, and the shapes a button takes from its place among
|
||||
* its siblings. The connected form is also what `<x-group>` draws its segments with, which is why
|
||||
* its stylesheet imports this one. A group never wraps: M3's rule, and the press expansion only
|
||||
* reaches a neighbour on the same line anyway.
|
||||
*
|
||||
* Standard (`data-md-button-group="standard"`): the buttons stand apart — 18/12/8/8/8px by size
|
||||
* (ButtonGroupXSmall…XLargeTokens.BetweenSpace) — and pressing a label button widens it and
|
||||
* narrows its neighbours by the same amount, so the row keeps its length: Expressive's press
|
||||
* expansion (ButtonGroupDefaults.ExpandedRatio is 15%; this uses a fixed step per size, 4/6/8/16/
|
||||
* 20px, on the button's own padding of 16/16/24/48/64px). Icon buttons keep their width. A square
|
||||
* standard group has no inner edges, so it hands its buttons the square corner scale
|
||||
* `<x-button shape="square">` draws (12/12/16/28/28px, 8/8/12/16/16px under the finger), and a
|
||||
* selected button in it turns round.
|
||||
*
|
||||
* Connected (`data-md-button-group="connected"`): 2px apart, the outer corners full, the inner
|
||||
* corners small, smaller still while pressed, and a selected segment (`aria-pressed`, or a checked
|
||||
* input inside it) fully round — ConnectedButtonGroupXSmall…XLargeTokens and the M3 Expressive
|
||||
* connected button group spec: inner corners 4/8/8/16/20px, pressed 2/4/4/12/16px. At the two
|
||||
* smallest sizes a segment is 32px or 40px tall, and M3 names both numbers it must not go below:
|
||||
* "XS and S connected button groups have a 48dp target area and a 48dp minimum width".
|
||||
*
|
||||
* Shape (`data-md-shape="square"`, M3's "Default shape | Round, square" configuration):
|
||||
* `--md-button-group-outer` is what the two ends of a group round to — half the height for a round
|
||||
* group, and for a square one the corner its inner edges take, which is the square table M3
|
||||
* publishes for connected groups (4/8/8/16/20dp by size). Selection morphs the other way: M3 has a
|
||||
* toggle inside a group "swap shape square/round on selection", so a selected segment in a square
|
||||
* group turns round, which is why the selected rule keeps `--md-button-group-full`.
|
||||
*
|
||||
* "Full" is half the size's height (16/20/28/48/68px), never `--md-sys-shape-corner-full`'s
|
||||
* 9999px. One element mixes full outer corners with small inner ones, and when a box's radii add
|
||||
* up to more than its side, CSS scales every radius by the same factor: a 9999px corner beside an
|
||||
* 8px one shrank the 8px one to a hundredth of a pixel, so the inner corners drew square.
|
||||
*
|
||||
* <x-button>'s own corner rules carry no specificity, so these reshape the buttons in a group
|
||||
* whatever selector they use; a pressed or selected segment changes one custom property and the
|
||||
* outer corners stay put.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@layer material.components {
|
||||
[data-md-button-group] {
|
||||
--md-button-group-inner: var(--md-sys-shape-corner-sm);
|
||||
--md-button-group-inner-pressed: var(--md-sys-shape-corner-xs);
|
||||
--md-button-group-full: 20px;
|
||||
--md-button-group-square: var(--md-sys-shape-corner-md);
|
||||
--md-button-group-square-pressed: var(--md-sys-shape-corner-sm);
|
||||
--md-button-group-outer: var(--md-button-group-full);
|
||||
--md-button-group-pad: var(--md-sys-measurement-space200);
|
||||
--md-button-group-grow: 6px;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
[data-md-button-group][data-md-size='xs'] {
|
||||
--md-button-group-inner: var(--md-sys-shape-corner-xs);
|
||||
--md-button-group-inner-pressed: 2px;
|
||||
--md-button-group-full: 16px;
|
||||
--md-button-group-grow: 4px;
|
||||
}
|
||||
|
||||
[data-md-button-group][data-md-size='md'] {
|
||||
--md-button-group-full: 28px;
|
||||
--md-button-group-square: var(--md-sys-shape-corner-lg);
|
||||
--md-button-group-square-pressed: var(--md-sys-shape-corner-md);
|
||||
--md-button-group-pad: var(--md-sys-measurement-space300);
|
||||
--md-button-group-grow: 8px;
|
||||
}
|
||||
|
||||
[data-md-button-group][data-md-size='lg'] {
|
||||
--md-button-group-inner: var(--md-sys-shape-corner-lg);
|
||||
--md-button-group-inner-pressed: var(--md-sys-shape-corner-md);
|
||||
--md-button-group-full: 48px;
|
||||
--md-button-group-square: var(--md-sys-shape-corner-xl);
|
||||
--md-button-group-square-pressed: var(--md-sys-shape-corner-lg);
|
||||
--md-button-group-pad: var(--md-sys-measurement-space600);
|
||||
--md-button-group-grow: 16px;
|
||||
}
|
||||
|
||||
[data-md-button-group][data-md-size='xl'] {
|
||||
--md-button-group-inner: var(--md-sys-shape-corner-lg-increased);
|
||||
--md-button-group-inner-pressed: var(--md-sys-shape-corner-lg);
|
||||
--md-button-group-full: 68px;
|
||||
--md-button-group-square: var(--md-sys-shape-corner-xl);
|
||||
--md-button-group-square-pressed: var(--md-sys-shape-corner-lg);
|
||||
--md-button-group-pad: var(--md-sys-measurement-space800);
|
||||
--md-button-group-grow: 20px;
|
||||
}
|
||||
|
||||
[data-md-button-group='standard'] {
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-md-button-group='standard'][data-md-size='xs'] {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
[data-md-button-group='standard'][data-md-size='sm'] {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* A square standard group hands its buttons the square corner scale; a selected one rounds. */
|
||||
[data-md-button-group='standard'][data-md-shape='square'] > * {
|
||||
border-radius: var(--md-button-group-square);
|
||||
}
|
||||
|
||||
[data-md-button-group='standard'][data-md-shape='square'] > :active:not(:disabled, [aria-disabled='true']) {
|
||||
border-radius: var(--md-button-group-square-pressed);
|
||||
}
|
||||
|
||||
[data-md-button-group='standard'][data-md-shape='square'] > [aria-pressed='true'] {
|
||||
border-radius: var(--md-button-group-full);
|
||||
}
|
||||
|
||||
/* Press expansion: the pressed label button grows, the label buttons beside it give way. */
|
||||
[data-md-button-group='standard'] > :not([data-md-icon-button]):active:not(:disabled, [aria-disabled='true']) {
|
||||
padding-inline: calc(var(--md-button-group-pad) + var(--md-button-group-grow));
|
||||
}
|
||||
|
||||
[data-md-button-group='standard'] > :not([data-md-icon-button]):has(+ :not([data-md-icon-button]):active:not(:disabled, [aria-disabled='true'])) {
|
||||
padding-inline-end: calc(var(--md-button-group-pad) - var(--md-button-group-grow));
|
||||
}
|
||||
|
||||
[data-md-button-group='standard'] > :not([data-md-icon-button]):active:not(:disabled, [aria-disabled='true']) + :not([data-md-icon-button]) {
|
||||
padding-inline-start: calc(var(--md-button-group-pad) - var(--md-button-group-grow));
|
||||
}
|
||||
|
||||
/* A square group's ends take the corner its inner edges take. */
|
||||
[data-md-button-group][data-md-shape='square'] {
|
||||
--md-button-group-outer: var(--md-button-group-inner);
|
||||
}
|
||||
|
||||
[data-md-button-group='connected'] {
|
||||
gap: var(--md-sys-measurement-space25);
|
||||
}
|
||||
|
||||
[data-md-button-group='connected']:is([data-md-size='xs'], [data-md-size='sm']) > * {
|
||||
min-inline-size: var(--md-sys-measurement-space600);
|
||||
}
|
||||
|
||||
[data-md-button-group='connected'] > * {
|
||||
--md-button-group-corner: var(--md-button-group-inner);
|
||||
|
||||
border-radius: var(--md-button-group-corner);
|
||||
}
|
||||
|
||||
[data-md-button-group='connected'] > :active {
|
||||
--md-button-group-corner: var(--md-button-group-inner-pressed);
|
||||
}
|
||||
|
||||
[data-md-button-group='connected'] > :is([aria-pressed='true'], :has(:checked)) {
|
||||
--md-button-group-corner: var(--md-button-group-full);
|
||||
}
|
||||
|
||||
[data-md-button-group='connected'] > :first-child {
|
||||
border-start-start-radius: var(--md-button-group-outer);
|
||||
border-end-start-radius: var(--md-button-group-outer);
|
||||
}
|
||||
|
||||
[data-md-button-group='connected'] > :last-child {
|
||||
border-start-end-radius: var(--md-button-group-outer);
|
||||
border-end-end-radius: var(--md-button-group-outer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
/*
|
||||
* <x-button>: M3 Expressive's buttons — label button, icon button and toggle — and the page's
|
||||
* create action that is a FAB on a compact window.
|
||||
*
|
||||
* Geometry from androidx Compose Material 3's tokens (Apache-2.0), per `data-md-size`:
|
||||
*
|
||||
* size height label padding gap type icon (label / icon-only) outline square pressed
|
||||
* xs 32px 16px 8px label-lg 20 / 20px 1px md sm
|
||||
* sm 40px 16px 8px label-lg 20 / 24px 1px md sm
|
||||
* md 56px 24px 8px title-md 24 / 24px 1px lg md
|
||||
* 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.) 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
|
||||
* loading indicator that stands in for the glyph while the button's action runs.
|
||||
*
|
||||
* Shape: round (`--md-sys-shape-corner-full`) by default; `data-md-shape="square"` and a selected
|
||||
* button take the size's square corner, a selected square icon button turns round, and every one
|
||||
* squares off to the pressed corner under the finger. Those corner rules are wrapped in `:where()`,
|
||||
* so they carry no specificity at all: a group's stylesheet (button-group.css, split-button.css)
|
||||
* reshapes the buttons inside it with any selector, without a specificity fight.
|
||||
*
|
||||
* Colour: `data-md-color` picks the roles a variant draws with; the variants set
|
||||
* `--md-button-container`, `--md-button-label`, `--md-button-outline` and `--md-button-elevation`,
|
||||
* which one rule paints, so the disabled treatment (on-surface at 10% behind 38% text,
|
||||
* FilledButtonTokens.DisabledContainerOpacity and DisabledLabelTextOpacity) replaces them with a
|
||||
* 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 (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).
|
||||
*
|
||||
* The button renders the foundation's `md-state-layer` and `md-focus-ring`
|
||||
* (foundation/interaction.css), and, at `xs` and `sm` where it draws under 48px, `md-touch-target`
|
||||
* too; nothing here copies their rules.
|
||||
*
|
||||
* `data-md-compact-fab`: below `medium` (600px) the button is an extended FAB in the thumb zone —
|
||||
* fixed 16px from the end, 56px tall with 16px padding, the large corner, `title-md`,
|
||||
* primary-container at elevation 3 (ExtendedFabPrimaryTokens) — lifted clear of a bottom bar and
|
||||
* of a snackbar on screen (`--material-bottom-bar`, `--material-snackbar-height`), because M3 puts a
|
||||
* snackbar above a FAB and never over one. From `medium` it is the button it was written as.
|
||||
* `data-md-unavailable`, a `fab` given `disabled`, is not drawn below `medium`: M3 never shows a
|
||||
* disabled FAB, but removes one whose action is unavailable. From `medium` it is the disabled
|
||||
* button. `data-md-responsive` on the label hides it below `expanded` (840px).
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
@import './loading.css';
|
||||
@import './tooltip.css';
|
||||
@import './color.css';
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
/* Sizes. */
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
/* Shape, without specificity: a group reshapes the buttons in it. */
|
||||
: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);
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
/* 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);
|
||||
--md-button-on-tone-selected: var(--md-button-on-color);
|
||||
--md-button-quiet: var(--md-button-color);
|
||||
}
|
||||
|
||||
/* Variants. */
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
/* Disabled: no state layer, no shadow, M3's disabled colours over whatever the variant set. */
|
||||
[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;
|
||||
}
|
||||
|
||||
/* The glyph's place, and the loading indicator that stands in for it. */
|
||||
[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;
|
||||
}
|
||||
|
||||
/* M3 never disables a FAB: "if its action is unavailable, remove the FAB entirely"
|
||||
(docs/reference/m3/components-actions-communication-containment.md § FAB). Not drawn,
|
||||
it is out of the accessibility tree and the Tab order too. */
|
||||
[data-md-button][data-md-compact-fab][data-md-unavailable] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* <x-card>: M3's three card variants (ElevatedCardTokens.kt, FilledCardTokens.kt,
|
||||
* 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).
|
||||
*
|
||||
* 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
|
||||
* its ring; this file adds only what a card needs beyond them:
|
||||
*
|
||||
* - *no layer* on a card that is neither a row nor dragged: a card that does not open anything
|
||||
* answers no pointer, but any card can be carried, and the application sets `data-md-dragged`
|
||||
* from script long after the view rendered;
|
||||
* - per-state *elevation*, which the shared class has no opinion on: elevated 1dp rest → 2dp
|
||||
* hover → 1dp focus/pressed; filled and outlined 0dp rest → 1dp hover → 0dp focus/pressed
|
||||
* (the cross-check table's Hover/Focus/Pressed rows). Pressed and focus fall back to the rest
|
||||
* level, so the `:active` rule (declared after `:hover`, same specificity) resets it even
|
||||
* while still hovering;
|
||||
* - the *press/hover exclusion*: a card's own nested button (the "Copy link" in a card that is
|
||||
* also a row) should light only itself, never the row's tint or elevation. `material.components`
|
||||
* out-layers `material.base`, so these two rules win over the shared class's plain `:hover`/
|
||||
* `:active` without needing extra specificity;
|
||||
* - the *opener-focused ring*: a non-actionable row's focus never lands on the card itself (its
|
||||
* opener carries the tab stop), so `md-focus-ring`'s own `:focus-visible` never fires for it;
|
||||
* `:has([data-md-list-open]:focus-visible)` draws the same ring from here instead. An
|
||||
* actionable card (`tabindex="0"`) is focused directly, which `md-focus-ring` already covers.
|
||||
*
|
||||
* `data-md-dragged` (an application sets it while its drag is in progress) is the top of the
|
||||
* elevation scale — Level4 (8dp) elevated, Level3 (6dp) filled and outlined — under the shared
|
||||
* class's own 16% dragged tint, which already reads `data-md-dragged` without any help from here.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './divider.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-card] {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border-radius: var(--md-sys-shape-corner-md);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
[data-md-card='filled'] {
|
||||
background-color: var(--md-sys-color-surface-container-highest);
|
||||
}
|
||||
|
||||
[data-md-card='elevated'] {
|
||||
background-color: var(--md-sys-color-surface-container-low);
|
||||
box-shadow: var(--md-sys-elevation-1);
|
||||
}
|
||||
|
||||
[data-md-card='outlined'] {
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
background-color: var(--md-sys-color-surface);
|
||||
}
|
||||
|
||||
[data-md-card]:not([data-md-list-row], [data-md-dragged])::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-md-card][data-md-list-row] {
|
||||
cursor: pointer;
|
||||
transition-property: box-shadow;
|
||||
transition-duration: var(--md-sys-motion-spatial-default-duration);
|
||||
transition-timing-function: var(--md-sys-motion-spatial-default);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-md-card][data-md-list-row]:not([data-md-dragged]):hover:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-md-list-open]):hover)) {
|
||||
box-shadow: var(--md-sys-elevation-1);
|
||||
}
|
||||
|
||||
[data-md-card='elevated'][data-md-list-row]:not([data-md-dragged]):hover:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-md-list-open]):hover)) {
|
||||
box-shadow: var(--md-sys-elevation-2);
|
||||
}
|
||||
|
||||
[data-md-card][data-md-list-row]:not([data-md-dragged]):hover:has(:is(a, button, input, select, textarea, label, summary):not([data-md-list-open]):hover)::before {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-card][data-md-list-row]:not([data-md-dragged]):active:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-md-list-open]):active)) {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
[data-md-card='elevated'][data-md-list-row]:not([data-md-dragged]):active:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-md-list-open]):active)) {
|
||||
box-shadow: var(--md-sys-elevation-1);
|
||||
}
|
||||
|
||||
[data-md-card][data-md-list-row]:not([data-md-dragged]):has(:is(a, button, input, select, textarea, label, summary):not([data-md-list-open]):active)::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[data-md-card][data-md-dragged] {
|
||||
box-shadow: var(--md-sys-elevation-3);
|
||||
}
|
||||
|
||||
[data-md-card='elevated'][data-md-dragged] {
|
||||
box-shadow: var(--md-sys-elevation-4);
|
||||
}
|
||||
|
||||
[data-md-card][data-md-list-row]:has([data-md-list-open]:focus-visible) {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
[data-md-card][data-md-list-row] [data-md-list-open]:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-md-card-figure] {
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
|
||||
& > img {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-card-body] {
|
||||
display: flex;
|
||||
flex: 1 1 0%;
|
||||
flex-direction: column;
|
||||
gap: var(--md-sys-measurement-space200);
|
||||
padding: var(--md-sys-measurement-space200);
|
||||
}
|
||||
|
||||
[data-md-card-header-row] {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-md-card-heading] {
|
||||
min-width: 0;
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
[data-md-card-title] {
|
||||
font: var(--md-sys-typescale-title-md);
|
||||
letter-spacing: var(--md-sys-typescale-title-md-tracking);
|
||||
}
|
||||
|
||||
[data-md-card-subtitle] {
|
||||
margin-top: var(--md-sys-measurement-space25);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-md);
|
||||
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
||||
}
|
||||
|
||||
[data-md-card-menu] {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: var(--md-sys-measurement-space50);
|
||||
margin-block-start: calc(-1 * var(--md-sys-measurement-space100));
|
||||
margin-inline-end: calc(-1 * var(--md-sys-measurement-space100));
|
||||
}
|
||||
|
||||
[data-md-card-header] > [data-md-divider] {
|
||||
margin-top: var(--md-sys-measurement-space200);
|
||||
}
|
||||
|
||||
[data-md-card-content] {
|
||||
flex: 1 1 0%;
|
||||
font: var(--md-sys-typescale-body-md);
|
||||
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
||||
}
|
||||
|
||||
[data-md-card-actions] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* <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).
|
||||
*
|
||||
* [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
|
||||
* [data-md-carousel-label-text]
|
||||
*
|
||||
* The item renders the shared `md-focus-ring` class (foundation/interaction.css) rather than a
|
||||
* hand-rolled ring — the box the class draws *is* the item's own whole hit area — refined here to
|
||||
* an inset ring, 3px in, its own width (the old `focus-visible:-outline-offset-3`): items sit 8px
|
||||
* apart with the row clipping them, so the class's own 2px *outward* offset would be cut off at the
|
||||
* row's edges and crowd the next item.
|
||||
*
|
||||
* Full-screen: edge to edge, no corner, no mask — `--material-carousel-shift`/`-inset` go unread.
|
||||
* Multi-aspect: the view writes the item's own `aspect-ratio` inline (`aspect`, a prop, held
|
||||
* inside M3's 9:16–16:9 range); the extra-large corner still applies but nothing masks it, since
|
||||
* an Arrangement describes items of one size and a multi-aspect row has none. Selectors use `>`
|
||||
* throughout, matching carousel.css, so a carousel nested inside an item never inherits its
|
||||
* parent's layout rules.
|
||||
*
|
||||
* 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
|
||||
* (deliberately kept).
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@layer material.components {
|
||||
[data-md-carousel-item] {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
scroll-snap-align: start;
|
||||
scroll-snap-stop: always;
|
||||
}
|
||||
|
||||
[data-md-carousel-item]:focus-visible {
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
[data-md-carousel='full-screen'] > [data-md-carousel-scroller] > [data-md-carousel-item] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-md-carousel='multi-aspect'] > [data-md-carousel-scroller] > [data-md-carousel-item] {
|
||||
width: auto;
|
||||
border-radius: var(--md-sys-shape-corner-xl);
|
||||
}
|
||||
|
||||
[data-md-carousel]:not([data-md-carousel='full-screen']):not([data-md-carousel='multi-aspect']) > [data-md-carousel-scroller] > [data-md-carousel-item] {
|
||||
width: var(--material-carousel-slot);
|
||||
max-width: 100%;
|
||||
border-radius: var(--md-sys-shape-corner-xl);
|
||||
}
|
||||
|
||||
[data-md-carousel-item] > [data-md-carousel-surface] {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: var(--md-sys-color-surface-container-highest);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
[data-md-carousel='multi-aspect'] > [data-md-carousel-scroller] > [data-md-carousel-item] > [data-md-carousel-surface] {
|
||||
border-radius: var(--md-sys-shape-corner-xl);
|
||||
}
|
||||
|
||||
[data-md-carousel]:not([data-md-carousel='full-screen']):not([data-md-carousel='multi-aspect']) > [data-md-carousel-scroller] > [data-md-carousel-item] > [data-md-carousel-surface] {
|
||||
border-radius: var(--md-sys-shape-corner-xl);
|
||||
translate: var(--material-carousel-shift) 0;
|
||||
clip-path: inset(0 var(--material-carousel-inset, 0px) round var(--md-sys-shape-corner-xl));
|
||||
}
|
||||
|
||||
[data-md-carousel-surface] > [data-md-carousel-content] {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
& > img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-carousel-surface] > [data-md-carousel-label] {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
padding: var(--md-sys-measurement-space500) var(--md-sys-measurement-space200) var(--md-sys-measurement-space200);
|
||||
background: linear-gradient(to top, color-mix(in srgb, var(--md-sys-color-scrim) 60%, transparent), transparent);
|
||||
opacity: var(--material-carousel-label);
|
||||
}
|
||||
|
||||
[data-md-carousel-label] > [data-md-carousel-label-text] {
|
||||
overflow: hidden;
|
||||
color: white;
|
||||
font: var(--md-sys-typescale-title-md);
|
||||
letter-spacing: var(--md-sys-typescale-title-md-tracking);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
translate: var(--material-carousel-label-shift) 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* <x-carousel>: M3 Expressive's carousel — multi-browse, hero, uncontained, the uncontained
|
||||
* multi-aspect-ratio layout (added November 2025) and full-screen, on a native scroll container
|
||||
* with CSS scroll snap. resources/js/carousel.js ports Compose's keyline maths (Arrangement.kt,
|
||||
* Keylines.kt, KeylineList.kt, Strategy.kt, KeylineSnapPosition.kt, Carousel.kt, androidx Compose
|
||||
* Material 3 @ commit 7ac433e44e797de53af85226797862687f37735f, Apache-2.0) and writes the masks
|
||||
* 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).
|
||||
*
|
||||
* [data-md-carousel] the row wrapper; its value is the layout
|
||||
* [data-md-carousel-probe] hidden, measures the preferred item width for the script
|
||||
* [data-md-carousel-scroller] the scroll container (role="region")
|
||||
* [data-md-carousel-item] one slide — carousel-item.css
|
||||
* [data-md-carousel-controls] previous/next, "auto" (pointer: fine only) or "always"
|
||||
* [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); 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; 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
|
||||
* `<x-icon mirror-rtl>`: `<x-button icon>` has no prop to reach it, and flipping a round icon
|
||||
* button whole reads the same as flipping only its glyph.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './button.css';
|
||||
@import './carousel-item.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-carousel] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-md-carousel] > [data-md-carousel-probe] {
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
top: 0;
|
||||
height: 0;
|
||||
width: var(--material-carousel-item-width);
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-md-carousel] > [data-md-carousel-scroller] {
|
||||
display: flex;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
[data-md-carousel='full-screen'] > [data-md-carousel-scroller] {
|
||||
margin-inline: auto;
|
||||
height: var(--material-carousel-height);
|
||||
max-width: 840px;
|
||||
flex-direction: column;
|
||||
gap: var(--md-sys-measurement-space200);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior-y: contain;
|
||||
scroll-snap-type: y mandatory;
|
||||
}
|
||||
|
||||
[data-md-carousel]:not([data-md-carousel='full-screen']) > [data-md-carousel-scroller] {
|
||||
height: calc(var(--material-carousel-height) + 16px);
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
overscroll-behavior-x: contain;
|
||||
padding-block: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-md-carousel='multi-aspect'] > [data-md-carousel-scroller] {
|
||||
padding-inline-start: var(--material-carousel-pad);
|
||||
}
|
||||
|
||||
[data-md-carousel='multi-browse'] > [data-md-carousel-scroller],
|
||||
[data-md-carousel='hero'] > [data-md-carousel-scroller] {
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
|
||||
[data-md-carousel] > [data-md-carousel-controls] {
|
||||
display: none;
|
||||
margin-top: 12px;
|
||||
justify-content: flex-end;
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-md-carousel] > [data-md-carousel-controls='always'] {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media (pointer: fine) {
|
||||
[data-md-carousel] > [data-md-carousel-controls='auto'] {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-carousel]:not([data-md-carousel='full-screen']) > [data-md-carousel-controls] > [data-md-carousel-previous]:is([dir='rtl'], [dir='rtl'] *),
|
||||
[data-md-carousel]:not([data-md-carousel='full-screen']) > [data-md-carousel-controls] > [data-md-carousel-next]:is([dir='rtl'], [dir='rtl'] *) {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* <x-checkbox>: M3's checkbox (resources/views/components/checkbox.blade.php), on the row the
|
||||
* selection controls share (components/selection.css).
|
||||
*
|
||||
* CheckboxTokens (androidx Compose Material 3, Apache-2.0): an 18px box with a 2px on-surface-variant
|
||||
* outline and 2px corners, filled with primary and holding an on-primary tick (or dash, for the
|
||||
* mixed state) at the 18px icon size once it is ticked; the state layer a 40px circle around it,
|
||||
* 11px past each edge. The tick grows in and fades in on the fast spatial spring. With the server's
|
||||
* error the outline, the fill and the state layer are error and the tick on-error. Disabled, the
|
||||
* outline and the fill are on-surface at 38% and the tick is surface. Beside a hint the box drops
|
||||
* 3px, to sit on the label's first line.
|
||||
*
|
||||
* [data-md-checkbox] the root: the row and its errors; `class` and `style` land here
|
||||
* [data-md-selection-row]
|
||||
* [data-md-checkbox-box] the box: input, [data-md-checkbox-check], [data-md-checkbox-mixed]
|
||||
* [data-md-selection-support] the errors, 4px under the row
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './selection.css';
|
||||
@import './icon.css';
|
||||
|
||||
@layer material.components {
|
||||
/* In a flex row the root keeps its automatic minimum, the box and beside a label its longest
|
||||
word, so the text beside it wraps before the box is pushed out of the row (toggle.css). */
|
||||
[data-md-checkbox] {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
[data-md-checkbox-box] {
|
||||
position: relative;
|
||||
display: grid;
|
||||
flex: none;
|
||||
place-items: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
[data-md-checkbox-box] > * {
|
||||
grid-area: 1 / 1;
|
||||
}
|
||||
|
||||
[data-md-selection-row]:has([data-md-selection-hint]) [data-md-checkbox-box] {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
[data-md-checkbox-box]::before {
|
||||
inset: -11px;
|
||||
}
|
||||
|
||||
[data-md-checkbox-box]:has(input:focus-visible)::before {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
}
|
||||
|
||||
[data-md-checkbox-box] input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid var(--md-sys-color-on-surface-variant);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
[data-md-checkbox-box] input:is(:checked, :indeterminate) {
|
||||
border-color: var(--md-sys-color-primary);
|
||||
background-color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
[data-md-checkbox-box] :is([data-md-checkbox-check], [data-md-checkbox-mixed]) {
|
||||
color: var(--md-sys-color-on-primary);
|
||||
opacity: 0;
|
||||
scale: 0.5;
|
||||
pointer-events: none;
|
||||
transition-property: opacity, scale;
|
||||
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
||||
}
|
||||
|
||||
[data-md-checkbox-box] input:checked:not(:indeterminate) ~ [data-md-checkbox-check],
|
||||
[data-md-checkbox-box] input:indeterminate ~ [data-md-checkbox-mixed] {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
|
||||
[data-md-selection-row][data-md-invalid] [data-md-checkbox-box] input {
|
||||
border-color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
[data-md-selection-row][data-md-invalid] [data-md-checkbox-box] input:is(:checked, :indeterminate) {
|
||||
background-color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
[data-md-selection-row][data-md-invalid] [data-md-checkbox-box] :is([data-md-checkbox-check], [data-md-checkbox-mixed]) {
|
||||
color: var(--md-sys-color-on-error);
|
||||
}
|
||||
|
||||
[data-md-checkbox-box] input:disabled {
|
||||
border-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
}
|
||||
|
||||
[data-md-checkbox-box] input:disabled:is(:checked, :indeterminate) {
|
||||
border-color: transparent;
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
}
|
||||
|
||||
[data-md-checkbox-box] input:disabled ~ :is([data-md-checkbox-check], [data-md-checkbox-mixed]) {
|
||||
color: var(--md-sys-color-surface);
|
||||
}
|
||||
|
||||
[data-md-checkbox] [data-md-selection-support] {
|
||||
margin-top: var(--md-sys-measurement-space50);
|
||||
color: var(--md-sys-color-error);
|
||||
font: var(--md-sys-typescale-body-sm);
|
||||
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* <x-chip-set>: a set of M3 chips (resources/views/components/chip-set.blade.php), named for screen
|
||||
* readers as a group.
|
||||
*
|
||||
* The chips sit 8px apart (M3's chip spacing), in a row that wraps. Above it the label in
|
||||
* label-large on-surface-variant, 8px over the row; under it the hint, or the errors in its place,
|
||||
* body-small, 4px under the row.
|
||||
*
|
||||
* `data-md-scroll` keeps the chips on one line that scrolls sideways, as M3 lays chips out on a
|
||||
* narrow screen. M3's chips accessibility page asks that a row which overflows say so: the edge the
|
||||
* row can still scroll towards (`data-md-scroll-start`, `data-md-scroll-end`, set by
|
||||
* resources/js/chips.js) fades over 24px through a mask, black meaning only "opaque" there, and in a
|
||||
* right-to-left document the fade runs the other way. Where the pointer is fine, and so there is no
|
||||
* swipe, a 32px round button in surface-container-high at elevation 1 sits over each fading edge;
|
||||
* the row's scroll padding grows from 24px to 40px there, so a chip the keyboard reaches scrolls
|
||||
* clear of the button as well as the fade. The row reaches 6px and 8px past its box so a focused
|
||||
* chip's ring and an elevated chip's shadow are not clipped by the scroller.
|
||||
*
|
||||
* [data-md-chip-set] the group; data-md-scroll
|
||||
* [data-md-chip-set-label]
|
||||
* [data-md-chip-set-scroller] (scrolling) the row and its buttons
|
||||
* [data-md-chip-set-row] the chips; data-md-scroll-start, data-md-scroll-end
|
||||
* [data-md-chip-scroll="start|end"]
|
||||
* [data-md-chip-set-hint] | [data-md-chip-set-errors]
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './chip.css';
|
||||
@import './icon.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-chip-set] {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-md-chip-set-label] {
|
||||
margin-bottom: var(--md-sys-measurement-space100);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-chip-set-row] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-md-chip-set-hint],
|
||||
[data-md-chip-set-errors] > p {
|
||||
margin-top: var(--md-sys-measurement-space50);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-sm);
|
||||
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-chip-set-errors] > p {
|
||||
color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
/* ---- Scrolling --------------------------------------------------------------------------- */
|
||||
|
||||
[data-md-chip-set-scroller] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-md-chip-set-scroller] > [data-md-chip-set-row] {
|
||||
--chip-fade-start: 0px;
|
||||
--chip-fade-end: 0px;
|
||||
|
||||
flex-wrap: nowrap;
|
||||
margin: calc(-1 * var(--md-sys-measurement-space100)) calc(-1 * var(--md-sys-measurement-space75));
|
||||
padding: var(--md-sys-measurement-space100) var(--md-sys-measurement-space75);
|
||||
overflow-x: auto;
|
||||
scroll-padding-inline: var(--md-sys-measurement-space300);
|
||||
scrollbar-width: none;
|
||||
mask-image: linear-gradient(to right, transparent, black var(--chip-fade-start), black calc(100% - var(--chip-fade-end)), transparent);
|
||||
|
||||
&:is([dir='rtl'], [dir='rtl'] *) {
|
||||
mask-image: linear-gradient(to left, transparent, black var(--chip-fade-start), black calc(100% - var(--chip-fade-end)), transparent);
|
||||
}
|
||||
|
||||
&[data-md-scroll-start] {
|
||||
--chip-fade-start: var(--md-sys-measurement-space300);
|
||||
}
|
||||
|
||||
&[data-md-scroll-end] {
|
||||
--chip-fade-end: var(--md-sys-measurement-space300);
|
||||
}
|
||||
|
||||
@media (pointer: fine) {
|
||||
scroll-padding-inline: var(--md-sys-measurement-space500);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-chip-scroll] {
|
||||
position: absolute;
|
||||
inset-block: 0;
|
||||
display: none;
|
||||
place-items: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-block: auto;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: var(--md-sys-color-surface-container-high);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
box-shadow: var(--md-sys-elevation-1);
|
||||
}
|
||||
|
||||
[data-md-chip-scroll='start'] {
|
||||
inset-inline-start: 0;
|
||||
}
|
||||
|
||||
[data-md-chip-scroll='end'] {
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
|
||||
/* Only while the row can still scroll that way, and only where the pointer cannot swipe. */
|
||||
@media (pointer: fine) {
|
||||
[data-md-chip-set-row][data-md-scroll-start] ~ [data-md-chip-scroll='start'],
|
||||
[data-md-chip-set-row][data-md-scroll-end] ~ [data-md-chip-scroll='end'] {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
/*
|
||||
* <x-chip>: M3's assist, filter, input and suggestion chips (resources/views/components/chip.blade.php).
|
||||
*
|
||||
* From androidx Compose Material 3 (Chip.kt, AssistChipTokens, FilterChipTokens, InputChipTokens,
|
||||
* SuggestionChipTokens at androidx/androidx 27cf9a7, Apache-2.0): 32px tall, the small corner,
|
||||
* label-large, 18px icons and a 24px avatar; 16px padding beside a label and 8px beside an icon,
|
||||
* 8px between (an input chip: 12px, 8px, and 4px beside an avatar). Compose draws the 1px border
|
||||
* inside the chip; a CSS border takes room, so every padding beside it is 1px short of its token
|
||||
* (7px for 8, 11px for 12, 15px for 16).
|
||||
*
|
||||
* Colours by type and state, each one role (never an opacity but M3's disabled 12% and 38%,
|
||||
* tokens/state.css):
|
||||
*
|
||||
* flat outline-variant border; on-surface ink (assist) or on-surface-variant (the rest);
|
||||
* the border turns on-surface (assist) or on-surface-variant while keyboard-focused
|
||||
* elevated surface-container-low at elevation 1, 2 on hover, no border (not for input chips)
|
||||
* selected a filter chip that is checked or pressed, an input chip marked `data-md-selected`:
|
||||
* secondary-container with on-secondary-container ink, no border; a flat selected
|
||||
* filter chip rises to elevation 1 on hover
|
||||
* disabled on-surface ink at 38%; the border (flat) or the container (elevated, selected) at 12%
|
||||
*
|
||||
* Leading icons are primary on assist, suggestion and filter chips; on an input chip they are
|
||||
* on-surface-variant, primary on a selected chip and while a chip with its own action is hovered,
|
||||
* focused or pressed (`data-md-actionable`). A trailing icon is primary on assist and suggestion
|
||||
* chips. A disabled chip's icons take its ink.
|
||||
*
|
||||
* The state layer is the chip's own ::before in the chip's ink (an input chip draws it on its
|
||||
* action, since the remove button is a second control), at M3's hover, focus and press opacities;
|
||||
* the focus indicator is the package's secondary ring outside the chip. Both the chip (or an input
|
||||
* chip's action) and the remove button catch presses over 48×48 — M3 lets the target extend beyond
|
||||
* the visible container. The filter chip's check grows in on the fast spatial spring and fades in
|
||||
* on the slow effects one; it shrinks on default effects and fades on fast effects, as
|
||||
* AnimatingChipContent does.
|
||||
*
|
||||
* [data-md-chip="assist|filter|input|suggestion"] the chip; data-md-elevated, data-md-disabled,
|
||||
* data-md-selected, data-md-actionable,
|
||||
* data-md-icon-start, data-md-icon-end,
|
||||
* data-md-avatar, data-md-removable
|
||||
* [data-md-chip-check-slot] a filter chip's check, over its icon when it has one
|
||||
* [data-md-chip-icon], [data-md-chip-check]
|
||||
* [data-md-chip-action] an input chip's own button or link (a <span> without one)
|
||||
* [data-md-chip-avatar], [data-md-chip-icon], [data-md-chip-label]
|
||||
* [data-md-chip-remove] an input chip's remove button
|
||||
* [data-md-chip-label], [data-md-chip-icon], [data-md-chip-trailing]
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
@import './tooltip.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-chip] {
|
||||
--chip-disabled-container: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-container-opacity) * 100%), transparent);
|
||||
--chip-disabled-ink: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
height: 32px;
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
border-radius: var(--md-sys-shape-corner-sm);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
white-space: nowrap;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
transition-property: background-color, border-color, color, box-shadow;
|
||||
transition-duration: var(--md-sys-motion-effects-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
/* ---- 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']) {
|
||||
padding-inline: 15px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:has(:focus-visible)::before {
|
||||
opacity: var(--md-sys-state-focus-state-layer-opacity);
|
||||
}
|
||||
|
||||
&:is(:focus-visible, :has(:focus-visible)) {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&[data-md-disabled]::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-chip]:is([data-md-chip='filter'], [data-md-icon-start]):not([data-md-chip='input']) {
|
||||
padding-inline-start: 7px;
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-icon-end] {
|
||||
padding-inline-end: 7px;
|
||||
}
|
||||
|
||||
[data-md-chip='assist'] {
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
[data-md-chip='assist']:not([data-md-elevated], [data-md-disabled]):focus-visible {
|
||||
border-color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
:is([data-md-chip='suggestion'], [data-md-chip='filter']):not([data-md-elevated], [data-md-disabled], :has(:checked), [aria-pressed='true']):is(:focus-visible, :has(:focus-visible)) {
|
||||
border-color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-elevated] {
|
||||
border-color: transparent;
|
||||
background-color: var(--md-sys-color-surface-container-low);
|
||||
box-shadow: var(--md-sys-elevation-1);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-md-chip][data-md-elevated]:not([data-md-disabled]):hover {
|
||||
box-shadow: var(--md-sys-elevation-2);
|
||||
}
|
||||
}
|
||||
|
||||
/* Selected: a checked filter chip, or a pressed one. */
|
||||
[data-md-chip='filter']:is(:has(:checked), [aria-pressed='true']),
|
||||
[data-md-chip='input'][data-md-selected] {
|
||||
border-color: transparent;
|
||||
background-color: var(--md-sys-color-secondary-container);
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-md-chip='filter']:not([data-md-elevated], [data-md-disabled]):is(:has(:checked), [aria-pressed='true']):hover {
|
||||
box-shadow: var(--md-sys-elevation-1);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-disabled] {
|
||||
border-color: var(--chip-disabled-container);
|
||||
color: var(--chip-disabled-ink);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-disabled]:is([data-md-elevated], [data-md-selected], :has(:checked), [aria-pressed='true']) {
|
||||
border-color: transparent;
|
||||
background-color: var(--chip-disabled-container);
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-disabled]:not(a) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* A disabled link leaves the pointer too. */
|
||||
a[data-md-chip][aria-disabled='true'] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ---- Contents ------------------------------------------------------------------------------ */
|
||||
|
||||
[data-md-chip-label] {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:is([data-md-chip], [data-md-chip-action]) > [data-md-chip-icon] {
|
||||
margin-inline-end: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-md-chip-trailing] {
|
||||
margin-inline-start: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-md-chip]:not([data-md-chip='input'], [data-md-disabled]) [data-md-chip-icon],
|
||||
[data-md-chip='input'][data-md-selected]:not([data-md-disabled]) [data-md-chip-icon],
|
||||
:is([data-md-chip='assist'], [data-md-chip='suggestion']):not([data-md-disabled]) [data-md-chip-trailing] {
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-actionable] [data-md-chip-icon] {
|
||||
transition: color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-actionable]:is(:active, :has(:focus-visible)) [data-md-chip-icon] {
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-md-chip][data-md-actionable]:hover [data-md-chip-icon] {
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
/* The filter chip's check: from nothing to 18px as it is selected, or over the icon it replaces. */
|
||||
[data-md-chip-check-slot] {
|
||||
display: grid;
|
||||
flex-shrink: 0;
|
||||
width: 0;
|
||||
height: 18px;
|
||||
margin-inline-end: var(--md-sys-measurement-space100);
|
||||
overflow: hidden;
|
||||
transition: width var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-effects-default);
|
||||
}
|
||||
|
||||
[data-md-chip-check-slot] > * {
|
||||
grid-area: 1 / 1;
|
||||
}
|
||||
|
||||
[data-md-chip-check-slot]:has([data-md-chip-icon]) {
|
||||
width: 18px;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
[data-md-chip]:is(:has(:checked), [aria-pressed='true']) [data-md-chip-check-slot]:not(:has([data-md-chip-icon])) {
|
||||
width: 18px;
|
||||
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
||||
}
|
||||
|
||||
[data-md-chip-check-slot] [data-md-chip-icon] {
|
||||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
[data-md-chip-check] {
|
||||
opacity: 0;
|
||||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
[data-md-chip]:is(:has(:checked), [aria-pressed='true']) [data-md-chip-check-slot] [data-md-chip-icon] {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[data-md-chip]:is(:has(:checked), [aria-pressed='true']) [data-md-chip-check] {
|
||||
opacity: 1;
|
||||
transition-duration: var(--md-sys-motion-effects-slow-duration);
|
||||
transition-timing-function: var(--md-sys-motion-effects-slow);
|
||||
}
|
||||
|
||||
/* ---- Input chips: an action of their own, and a remove button ---------------------------- */
|
||||
|
||||
[data-md-chip='input'] {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
[data-md-chip='input']:not([data-md-selected], [data-md-disabled]):has([data-md-chip-action]:focus-visible) {
|
||||
border-color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-md-chip='input']:has([data-md-chip-action]:focus-visible) {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
[data-md-chip-action] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
padding-inline: 11px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-icon-start] [data-md-chip-action] {
|
||||
padding-inline-start: 7px;
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-avatar] [data-md-chip-action] {
|
||||
padding-inline-start: 3px;
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-removable] [data-md-chip-action] {
|
||||
padding-inline-end: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
:is(a, button)[data-md-chip-action] {
|
||||
cursor: pointer;
|
||||
|
||||
/* The 48px target and the state layer, across the whole chip. */
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
top: 50%;
|
||||
height: var(--md-sys-measurement-space600);
|
||||
translate: 0 -50%;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-chip]:not([data-md-disabled]) :is(a, button)[data-md-chip-action] {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
z-index: -1;
|
||||
border-radius: var(--md-sys-shape-corner-sm);
|
||||
background-color: currentColor;
|
||||
opacity: 0;
|
||||
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 {
|
||||
opacity: var(--md-sys-state-focus-state-layer-opacity);
|
||||
}
|
||||
|
||||
&:active::before {
|
||||
opacity: var(--md-sys-state-pressed-state-layer-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-disabled] :is(a, button)[data-md-chip-action] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-disabled] a[data-md-chip-action] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-md-chip-avatar] {
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-inline-end: var(--md-sys-measurement-space100);
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
}
|
||||
|
||||
span[data-md-chip-avatar] {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
font: var(--md-sys-typescale-label-sm);
|
||||
letter-spacing: var(--md-sys-typescale-label-sm-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
img[data-md-chip-avatar] {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-disabled] [data-md-chip-avatar] {
|
||||
opacity: var(--md-sys-state-disabled-content-opacity);
|
||||
}
|
||||
|
||||
/* An 18px icon in a 24px state layer, catching presses 15px out on every side: M3's 48px. */
|
||||
[data-md-chip-remove] {
|
||||
position: relative;
|
||||
display: grid;
|
||||
flex-shrink: 0;
|
||||
place-items: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-inline-end: 7px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -15px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:not(:disabled) {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -3px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: currentColor;
|
||||
opacity: 0;
|
||||
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 {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:focus-visible::before {
|
||||
opacity: var(--md-sys-state-focus-state-layer-opacity);
|
||||
}
|
||||
|
||||
&:active::before {
|
||||
opacity: var(--md-sys-state-pressed-state-layer-opacity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* <x-choices>: choosing from a list, as M3's filter chips or as a searchable field with a menu
|
||||
* (resources/views/components/choices.blade.php).
|
||||
*
|
||||
* Almost nothing of its own to draw: the chips are <x-chip-set>'s and <x-chip>'s, the searchable
|
||||
* field is <x-field>'s with its arrow turning over while the list is open (components/field.css),
|
||||
* and the open list is M3's menu, drawn by components/menu.css on `data-md-field-menu` and
|
||||
* `data-md-field-option`. What is left is the row that says nothing matches: body-medium in
|
||||
* on-surface-variant, padded like a menu row (16px across, 12px down).
|
||||
*
|
||||
* [data-md-choices] the root; data-md-searchable
|
||||
* [data-md-choices-empty] the list's "Nothing matches" row
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './field.css';
|
||||
@import './icon.css';
|
||||
@import './chip-set.css';
|
||||
@import './chip.css';
|
||||
@import './menu.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-choices-empty] {
|
||||
padding: 12px var(--md-sys-measurement-space200);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-md);
|
||||
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* <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, 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 —
|
||||
* `interpolate-size` (for `0 -> auto`) is Chrome's alone, and Firefox does not hold the content's
|
||||
* `content-visibility` through a close — so Firefox and Safari used to snap open and shut. While a
|
||||
* close runs, `open` is still set and `data-md-collapse-closing` turns the chevron back at its
|
||||
* start. Under reduced motion the duration token is zero (tokens/motion.css), and the section
|
||||
* snaps, as the browser draws it.
|
||||
*
|
||||
* `data-md-variant="filled"` is a surface-container tile with a large corner; the summary inherits
|
||||
* it (`border-radius: inherit`) so the shared `md-state-layer`'s own `::before`, which also
|
||||
* inherits, draws inside the same shape. `md-state-layer` and `md-focus-ring`
|
||||
* (foundation/interaction.css) sit on the summary; this file only refines their target — the
|
||||
* 48px summary height, the icon and chevron colour — never their opacities or ring.
|
||||
*
|
||||
* Icon-to-title and title-to-chevron gaps are a literal 12px: the measurement scale has no 12
|
||||
* (`space125` is 10px), as alert.css's gap. Every part is matched as its own `<details>`' child, so
|
||||
* a collapse nested in a filled one takes neither its padding nor its turned chevron.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-collapse][data-md-variant='filled'] {
|
||||
border-radius: var(--md-sys-shape-corner-lg);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
}
|
||||
|
||||
[data-md-collapse-summary] {
|
||||
display: flex;
|
||||
min-block-size: var(--md-sys-measurement-space600);
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-radius: inherit;
|
||||
cursor: pointer;
|
||||
list-style-type: none;
|
||||
font: var(--md-sys-typescale-title-md);
|
||||
letter-spacing: var(--md-sys-typescale-title-md-tracking);
|
||||
|
||||
&::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-collapse][data-md-variant='filled'] > [data-md-collapse-summary] {
|
||||
padding-inline: var(--md-sys-measurement-space200);
|
||||
}
|
||||
|
||||
[data-md-collapse-icon] {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-md-collapse-title] {
|
||||
min-width: 0;
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
[data-md-collapse-chevron] {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
transition: rotate var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast);
|
||||
}
|
||||
|
||||
[data-md-collapse][open]:not([data-md-collapse-closing]) > [data-md-collapse-summary] > [data-md-collapse-chevron] {
|
||||
rotate: 180deg;
|
||||
}
|
||||
|
||||
[data-md-collapse-body] {
|
||||
padding-block: var(--md-sys-measurement-space50) var(--md-sys-measurement-space200);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-md);
|
||||
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
||||
}
|
||||
|
||||
[data-md-collapse][data-md-variant='filled'] > [data-md-collapse-body] {
|
||||
padding-inline: var(--md-sys-measurement-space200);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,71 +1,93 @@
|
||||
/*
|
||||
* M3's date pickers — docked, modal and modal input (resources/views/components/datepicker.blade.php).
|
||||
*
|
||||
* One `<dialog>` wears both: docked it is a popover hung under the field by CSS anchor positioning
|
||||
* (large corner), modal it is opened with `showModal()` over a scrim (extra-large corner, with M3's
|
||||
* header: a title, the chosen date as the headline, and the switch between calendar and typing).
|
||||
* Both are 360px of surface-container-high at elevation 3.
|
||||
* One `<dialog>` wears all three: docked it is a popover hung under the field by CSS anchor
|
||||
* positioning (large corner), modal it is opened with `showModal()` over a scrim (extra-large
|
||||
* corner, with M3's header: a title, the chosen date as the headline, and the switch between
|
||||
* calendar and typing), and full — a `range` picker on a compact window — is that modal dialog
|
||||
* grown to the whole screen, M3's full-screen range picker. The first two are 360px of
|
||||
* surface-container-high at elevation 3; the third keeps the colour and drops the corner.
|
||||
*
|
||||
* Values from DatePickerModalTokens and DateInputModalTokens (androidx Compose Material 3, commit
|
||||
* 27cf9a7d5788aa0f5f2d8b6699ce279560daf326) and the layout of DatePicker.kt, DateRangePicker.kt and
|
||||
* DateInput.kt; the docked picker's from material-web's md-comp-date-picker-docked tokens (v0_192).
|
||||
* Apache-2.0.
|
||||
*
|
||||
* [data-datepicker] the root: the field, its support line, the dialog
|
||||
* [data-datepicker-support] the hint, a server error, or what is wrong with the typed date
|
||||
* [data-datepicker-picker] the <dialog>; data-presentation="docked" | "modal"
|
||||
* [data-datepicker-surface]
|
||||
* [data-datepicker-header] modal only: title, headline, switch
|
||||
* [data-datepicker-calendar]
|
||||
* [data-datepicker-nav] month and year: one menu button and arrows (modal),
|
||||
* or a month and a year stepper (docked, data-docked)
|
||||
* [data-datepicker-grid] the weekdays and six weeks of [data-datepicker-day] cells:
|
||||
* data-today, data-selected, data-start, data-end,
|
||||
* data-between, data-outside, data-blank, aria-disabled
|
||||
* [data-datepicker-years] the modal year grid
|
||||
* [data-datepicker-menu] a docked month or year list of [data-datepicker-option]s
|
||||
* [data-datepicker-entry] the text field(s) of the modal input
|
||||
* [data-datepicker-actions] Cancel and OK
|
||||
* [data-md-datepicker] the root: the field, its support line, the dialog
|
||||
* [data-md-datepicker-support] the hint, a server error, or what is wrong with the typed date
|
||||
* [data-md-datepicker-picker] the <dialog>; data-md-presentation="docked" | "modal" | "full"
|
||||
* [data-md-datepicker-surface]
|
||||
* [data-md-datepicker-header] modal only: title, headline, switch
|
||||
* [data-md-datepicker-full] full only: [data-md-datepicker-app-bar] (close, switch, Save)
|
||||
* and [data-md-datepicker-full-headline] (title, headline)
|
||||
* [data-md-datepicker-calendar] the scroller, full screen
|
||||
* [data-md-datepicker-nav] month and year: one menu button and arrows (modal),
|
||||
* or a month and a year stepper (docked, data-md-docked)
|
||||
* [data-md-datepicker-grid] the weekdays and six weeks of [data-md-datepicker-day]
|
||||
* cells: data-md-today, data-md-selected, data-md-start,
|
||||
* data-md-end, data-md-between, data-md-outside,
|
||||
* data-md-blank, aria-disabled — full screen, every month
|
||||
* of the window under a [data-md-datepicker-month-label] row
|
||||
* [data-md-datepicker-years] the modal year grid
|
||||
* [data-md-datepicker-menu] a docked month or year list of [data-md-datepicker-option]s
|
||||
* [data-md-datepicker-entry] the text field(s) of the modal input
|
||||
* [data-md-datepicker-actions] Cancel and OK
|
||||
*
|
||||
* Every state layer is the content colour at 8% on hover (only where a pointer can hover) and 10% on
|
||||
* focus and press; keyboard focus adds the package's 3px secondary ring.
|
||||
* The year, the menu buttons and the docked lists' options render the foundation's `md-state-layer`
|
||||
* and `md-focus-ring` (foundation/interaction.css): each draws its layer in its own ink and its
|
||||
* ring round itself, so the classes need one refinement only — an option's ring sits inside its
|
||||
* full-width row. Cancel, OK and the icon buttons are <x-button>, which renders the classes too.
|
||||
* The day alone keeps a state layer and ring of its own: the element focused and pressed is the
|
||||
* 48px cell, while both are drawn on the 40px round indicator inside it, which a class drawn on the
|
||||
* element it sits on cannot do, and `:focus-visible` never matches that child. Its layer takes
|
||||
* M3's state opacities (tokens/state.css) and is gone from a disabled or blank day.
|
||||
*
|
||||
* Lengths are px, and spacing is the measurement tokens: M3 gives every size here in dp. A
|
||||
* disabled day or option is on-surface at M3's 38% content opacity.
|
||||
*/
|
||||
|
||||
@layer components {
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './field.css';
|
||||
@import './icon.css';
|
||||
@import './button.css';
|
||||
@import './modal.css';
|
||||
|
||||
@layer material.components {
|
||||
/* ---- The support line under the field ------------------------------------------------------ */
|
||||
|
||||
/* The padding is on the lines, not the box, so a box with nothing to say takes no room. */
|
||||
[data-datepicker-support] {
|
||||
--datepicker-pad: 1rem;
|
||||
[data-md-datepicker-support] {
|
||||
--datepicker-pad: var(--md-sys-measurement-space200);
|
||||
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-sm);
|
||||
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
||||
}
|
||||
|
||||
[data-datepicker-support] > * {
|
||||
padding: 0.25rem var(--datepicker-pad) 0;
|
||||
[data-md-datepicker-support] > * {
|
||||
padding: var(--md-sys-measurement-space50) var(--datepicker-pad) 0;
|
||||
}
|
||||
|
||||
[data-datepicker-support][data-size="sm"] {
|
||||
--datepicker-pad: 0.75rem;
|
||||
[data-md-datepicker-support][data-md-size='sm'] {
|
||||
--datepicker-pad: 12px;
|
||||
}
|
||||
|
||||
[data-datepicker-support][data-size="xs"] {
|
||||
--datepicker-pad: 0.625rem;
|
||||
[data-md-datepicker-support][data-md-size='xs'] {
|
||||
--datepicker-pad: var(--md-sys-measurement-space125);
|
||||
}
|
||||
|
||||
[data-datepicker-error] {
|
||||
[data-md-datepicker-error] {
|
||||
color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
/* ---- The dialog ---------------------------------------------------------------------------- */
|
||||
|
||||
[data-datepicker-picker] {
|
||||
[data-md-datepicker-picker] {
|
||||
inset: auto;
|
||||
width: 22.5rem;
|
||||
max-width: calc(100vw - 1rem);
|
||||
max-height: calc(100dvh - 1rem);
|
||||
width: 360px;
|
||||
max-width: calc(100vw - var(--md-sys-measurement-space200));
|
||||
max-height: calc(100dvh - var(--md-sys-measurement-space200));
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
@@ -74,10 +96,10 @@
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
[data-datepicker-picker]:popover-open {
|
||||
[data-md-datepicker-picker]:popover-open {
|
||||
top: anchor(bottom);
|
||||
left: anchor(left);
|
||||
margin-block: 0.25rem;
|
||||
margin-block: var(--md-sys-measurement-space50);
|
||||
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
|
||||
opacity: 1;
|
||||
transition-property: opacity;
|
||||
@@ -86,33 +108,18 @@
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
[data-datepicker-picker]:popover-open {
|
||||
[data-md-datepicker-picker]:popover-open {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-datepicker-picker]:modal {
|
||||
/* 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-datepicker-picker]:modal {
|
||||
opacity: 0;
|
||||
scale: 0.95;
|
||||
}
|
||||
}
|
||||
|
||||
[data-datepicker-picker]:modal::backdrop {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent);
|
||||
}
|
||||
|
||||
[data-datepicker-surface] {
|
||||
[data-md-datepicker-surface] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: inherit;
|
||||
@@ -124,41 +131,122 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-datepicker-picker][data-presentation="modal"] [data-datepicker-surface] {
|
||||
[data-md-datepicker-picker][data-md-presentation='modal'] [data-md-datepicker-surface] {
|
||||
border-radius: var(--md-sys-shape-corner-xl);
|
||||
}
|
||||
|
||||
/* ---- The modal header ---------------------------------------------------------------------- */
|
||||
/* ---- The full-screen range picker (compact) ------------------------------------------------- */
|
||||
|
||||
[data-datepicker-header] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
/* M3 § Date Pickers: a range picker at a compact breakpoint is a full-screen dialog, "covering
|
||||
* the whole screen for readability and touch-target size", with a close (×) icon button and a
|
||||
* Save confirmation instead of Cancel and OK. Its anatomy is the modal picker's parts in a
|
||||
* different order: an app bar, the supporting text and the range as the headline, a divider, the
|
||||
* day-of-week labels, and a vertically scrolling list of months under them, each with its label.
|
||||
* Only the months scroll; everything above them stays where it is. */
|
||||
[data-md-datepicker-picker][data-md-presentation='full'] {
|
||||
inset: 0;
|
||||
width: 100vw;
|
||||
max-width: none;
|
||||
height: 100dvh;
|
||||
max-height: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[data-md-datepicker-picker][data-md-presentation='full'] [data-md-datepicker-surface] {
|
||||
height: 100%;
|
||||
max-height: none;
|
||||
padding-block: var(--material-safe-top, env(safe-area-inset-top)) var(--material-safe-bottom, env(safe-area-inset-bottom));
|
||||
overflow: hidden;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
[data-md-datepicker-full] {
|
||||
flex: none;
|
||||
min-height: 7.5rem;
|
||||
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-datepicker-header][data-range] {
|
||||
min-height: 8rem;
|
||||
[data-md-datepicker-app-bar] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
height: 64px;
|
||||
padding-inline: var(--md-sys-measurement-space50) 12px;
|
||||
}
|
||||
|
||||
[data-datepicker-title] {
|
||||
padding: 1rem 0.75rem 0 1.5rem;
|
||||
[data-md-datepicker-app-bar-actions] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--md-sys-measurement-space50);
|
||||
}
|
||||
|
||||
[data-md-datepicker-full-headline] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--md-sys-measurement-space50);
|
||||
padding: 0 var(--md-sys-measurement-space300) var(--md-sys-measurement-space200);
|
||||
}
|
||||
|
||||
/* The calendar is the only part that scrolls, with the weekday header held at its top. */
|
||||
[data-md-datepicker-picker][data-md-presentation='full'] [data-md-datepicker-calendar] {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
[data-md-datepicker-picker][data-md-presentation='full'] [data-md-datepicker-grid] thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
background-color: var(--md-sys-color-surface-container-high);
|
||||
}
|
||||
|
||||
[data-md-datepicker-picker][data-md-presentation='full'] [data-md-datepicker-day] {
|
||||
scroll-margin-block: 48px;
|
||||
}
|
||||
|
||||
[data-md-datepicker-month-label] {
|
||||
padding: 20px 0 var(--md-sys-measurement-space100);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-title-sm);
|
||||
letter-spacing: var(--md-sys-typescale-title-sm-tracking);
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
/* ---- The modal header ---------------------------------------------------------------------- */
|
||||
|
||||
[data-md-datepicker-header] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
flex: none;
|
||||
min-height: 120px;
|
||||
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-md-datepicker-header][data-md-range] {
|
||||
min-height: 128px;
|
||||
}
|
||||
|
||||
[data-md-datepicker-title] {
|
||||
padding: var(--md-sys-measurement-space200) 12px 0 var(--md-sys-measurement-space300);
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
}
|
||||
|
||||
[data-datepicker-headline-row] {
|
||||
[data-md-datepicker-headline-row] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding: 0 0.75rem 0.75rem 1.5rem;
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
padding: 0 12px 12px var(--md-sys-measurement-space300);
|
||||
}
|
||||
|
||||
[data-datepicker-headline] {
|
||||
[data-md-datepicker-headline] {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -167,129 +255,77 @@
|
||||
letter-spacing: var(--md-sys-typescale-headline-lg-tracking);
|
||||
}
|
||||
|
||||
[data-datepicker-header][data-range] [data-datepicker-headline] {
|
||||
[data-md-datepicker-header][data-md-range] [data-md-datepicker-headline] {
|
||||
font: var(--md-sys-typescale-title-lg);
|
||||
letter-spacing: var(--md-sys-typescale-title-lg-tracking);
|
||||
}
|
||||
|
||||
/* ---- Month and year navigation -------------------------------------------------------------- */
|
||||
|
||||
[data-datepicker-nav] {
|
||||
[data-md-datepicker-nav] {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 3.5rem;
|
||||
padding-inline: 0.75rem;
|
||||
height: 56px;
|
||||
padding-inline: 12px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-datepicker-nav][data-docked] {
|
||||
height: 4rem;
|
||||
padding-inline: 0.25rem;
|
||||
[data-md-datepicker-nav][data-md-docked] {
|
||||
height: 64px;
|
||||
padding-inline: var(--md-sys-measurement-space50);
|
||||
}
|
||||
|
||||
[data-datepicker-stepper],
|
||||
[data-datepicker-arrows] {
|
||||
[data-md-datepicker-stepper],
|
||||
[data-md-datepicker-arrows] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* While a year or month list is open the arrows keep their place but are gone, as androidx hides them. */
|
||||
[data-datepicker-arrows][data-concealed] {
|
||||
[data-md-datepicker-arrows][data-md-concealed] {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
[data-datepicker-menu-button] {
|
||||
position: relative;
|
||||
[data-md-datepicker-menu-button] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
height: 2.5rem;
|
||||
padding-inline: 0.75rem 0.5rem;
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
height: 40px;
|
||||
padding-inline: 12px var(--md-sys-measurement-space100);
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
[data-datepicker-nav][data-docked] [data-datepicker-menu-button] {
|
||||
padding-inline: 0.5rem 0.25rem;
|
||||
gap: 0.25rem;
|
||||
[data-md-datepicker-nav][data-md-docked] [data-md-datepicker-menu-button] {
|
||||
padding-inline: var(--md-sys-measurement-space100) var(--md-sys-measurement-space50);
|
||||
gap: var(--md-sys-measurement-space50);
|
||||
}
|
||||
|
||||
[data-datepicker-menu-arrow] {
|
||||
[data-md-datepicker-menu-arrow] {
|
||||
transition: rotate var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
[data-datepicker-menu-button][aria-expanded="true"] [data-datepicker-menu-arrow] {
|
||||
[data-md-datepicker-menu-button][aria-expanded='true'] [data-md-datepicker-menu-arrow] {
|
||||
rotate: 180deg;
|
||||
}
|
||||
|
||||
/* ---- State layers shared by days, years, menu buttons and list rows ------------------------ */
|
||||
|
||||
[data-datepicker-menu-button]::before,
|
||||
[data-datepicker-day] > span::before,
|
||||
[data-datepicker-year]::before,
|
||||
[data-datepicker-option]::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
border-radius: inherit;
|
||||
background-color: var(--datepicker-layer, currentColor);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-datepicker-menu-button]:hover::before,
|
||||
[data-datepicker-day]:not([aria-disabled="true"], [data-blank]):hover > span::before,
|
||||
[data-datepicker-year]:hover::before,
|
||||
[data-datepicker-option]:not([aria-disabled="true"]):hover::before {
|
||||
opacity: 0.08;
|
||||
}
|
||||
}
|
||||
|
||||
[data-datepicker-menu-button]:focus-visible::before,
|
||||
[data-datepicker-menu-button]:active::before,
|
||||
[data-datepicker-day]:focus-visible > span::before,
|
||||
[data-datepicker-day]:not([aria-disabled="true"], [data-blank]):active > span::before,
|
||||
[data-datepicker-year]:focus-visible::before,
|
||||
[data-datepicker-year]:active::before,
|
||||
[data-datepicker-option]:focus-visible::before,
|
||||
[data-datepicker-option]:not([aria-disabled="true"]):active::before {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
[data-datepicker-menu-button]:focus-visible,
|
||||
[data-datepicker-day]:focus-visible > span,
|
||||
[data-datepicker-year]:focus-visible {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
[data-datepicker-option]:focus-visible {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
/* ---- The days ----------------------------------------------------------------------------- */
|
||||
|
||||
[data-datepicker-grid] {
|
||||
width: calc(100% - 1.5rem);
|
||||
margin-inline: 0.75rem;
|
||||
[data-md-datepicker-grid] {
|
||||
width: calc(100% - 24px);
|
||||
margin-inline: 12px;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
[data-datepicker-grid] th {
|
||||
height: 3rem;
|
||||
[data-md-datepicker-grid] th {
|
||||
height: 48px;
|
||||
padding: 0;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
font: var(--md-sys-typescale-body-lg);
|
||||
@@ -297,11 +333,11 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-datepicker-day] {
|
||||
[data-md-datepicker-day] {
|
||||
--datepicker-layer: var(--md-sys-color-on-surface-variant);
|
||||
|
||||
position: relative;
|
||||
height: 3rem;
|
||||
height: 48px;
|
||||
padding: 0;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
font: var(--md-sys-typescale-body-lg);
|
||||
@@ -313,123 +349,153 @@
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
[data-datepicker-day] > span {
|
||||
/* The 40px indicator, its own stacking context so the state layer lies over a chosen day's
|
||||
primary fill rather than under it. */
|
||||
[data-md-datepicker-day] > span {
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: auto;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
isolation: isolate;
|
||||
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
[data-datepicker-day][data-blank] {
|
||||
/* The day's state layer and focus ring, on the indicator (see the header). */
|
||||
[data-md-datepicker-day] > span::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
border-radius: inherit;
|
||||
background-color: var(--datepicker-layer);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-md-datepicker-day]:hover > span::before {
|
||||
opacity: var(--md-sys-state-hover-state-layer-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-datepicker-day]:focus-visible > span::before {
|
||||
opacity: var(--md-sys-state-focus-state-layer-opacity);
|
||||
}
|
||||
|
||||
[data-md-datepicker-day]:active > span::before {
|
||||
opacity: var(--md-sys-state-pressed-state-layer-opacity);
|
||||
}
|
||||
|
||||
[data-md-datepicker-day]:is([aria-disabled='true'], [data-md-blank]) > span::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-md-datepicker-day]:focus-visible > span {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
[data-md-datepicker-day][data-md-blank] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* A docked picker shows the neighbouring months' days, quieter. */
|
||||
[data-datepicker-day][data-outside] {
|
||||
[data-md-datepicker-day][data-md-outside] {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-datepicker-day][data-today] {
|
||||
[data-md-datepicker-day][data-md-today] {
|
||||
--datepicker-layer: var(--md-sys-color-primary);
|
||||
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
[data-datepicker-day][data-today] > span {
|
||||
[data-md-datepicker-day][data-md-today] > span {
|
||||
box-shadow: inset 0 0 0 1px var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
/* A range's band: secondary-container behind the days between its ends, from the middle of each end. */
|
||||
[data-datepicker-day][data-between]::before,
|
||||
[data-datepicker-day][data-start]::before,
|
||||
[data-datepicker-day][data-end]::before {
|
||||
content: "";
|
||||
[data-md-datepicker-day][data-md-between]::before,
|
||||
[data-md-datepicker-day][data-md-start]::before,
|
||||
[data-md-datepicker-day][data-md-end]::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-block: 0.25rem;
|
||||
inset-block: var(--md-sys-measurement-space50);
|
||||
inset-inline: 0;
|
||||
z-index: -1;
|
||||
background-color: var(--md-sys-color-secondary-container);
|
||||
}
|
||||
|
||||
[data-datepicker-day][data-start]::before {
|
||||
[data-md-datepicker-day][data-md-start]::before {
|
||||
inset-inline-start: 50%;
|
||||
}
|
||||
|
||||
[data-datepicker-day][data-end]::before {
|
||||
[data-md-datepicker-day][data-md-end]::before {
|
||||
inset-inline-end: 50%;
|
||||
}
|
||||
|
||||
[data-datepicker-day][data-between] {
|
||||
[data-md-datepicker-day][data-md-between] {
|
||||
--datepicker-layer: var(--md-sys-color-on-secondary-container);
|
||||
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
[data-datepicker-day][data-selected] {
|
||||
[data-md-datepicker-day][data-md-selected] {
|
||||
--datepicker-layer: var(--md-sys-color-on-primary);
|
||||
|
||||
color: var(--md-sys-color-on-primary);
|
||||
}
|
||||
|
||||
[data-datepicker-day][data-selected] > span {
|
||||
[data-md-datepicker-day][data-md-selected] > span {
|
||||
background-color: var(--md-sys-color-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
[data-datepicker-day][aria-disabled="true"] {
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||||
[data-md-datepicker-day][aria-disabled='true'] {
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
[data-datepicker-day][aria-disabled="true"][data-today] > span {
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||||
[data-md-datepicker-day][aria-disabled='true'][data-md-today] > span {
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
}
|
||||
|
||||
/* ---- The modal year grid ----------------------------------------------------------------- */
|
||||
|
||||
[data-datepicker-years] {
|
||||
[data-md-datepicker-years] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
align-content: start;
|
||||
justify-items: center;
|
||||
row-gap: 1rem;
|
||||
height: 20.9375rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
row-gap: var(--md-sys-measurement-space200);
|
||||
height: 335px;
|
||||
padding: var(--md-sys-measurement-space100) 12px;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
[data-datepicker-year] {
|
||||
--datepicker-layer: var(--md-sys-color-on-surface-variant);
|
||||
|
||||
position: relative;
|
||||
width: 4.5rem;
|
||||
height: 2.25rem;
|
||||
[data-md-datepicker-year] {
|
||||
width: 72px;
|
||||
height: 36px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-lg);
|
||||
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
isolation: isolate;
|
||||
scroll-margin-block: 0.5rem;
|
||||
scroll-margin-block: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-datepicker-year][data-current] {
|
||||
--datepicker-layer: var(--md-sys-color-primary);
|
||||
|
||||
[data-md-datepicker-year][data-md-current] {
|
||||
color: var(--md-sys-color-primary);
|
||||
box-shadow: inset 0 0 0 1px var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
[data-datepicker-year][aria-selected="true"] {
|
||||
--datepicker-layer: var(--md-sys-color-on-primary);
|
||||
|
||||
[data-md-datepicker-year][aria-selected='true'] {
|
||||
background-color: var(--md-sys-color-primary);
|
||||
color: var(--md-sys-color-on-primary);
|
||||
box-shadow: none;
|
||||
@@ -437,72 +503,71 @@
|
||||
|
||||
/* ---- The docked month and year lists ------------------------------------------------------ */
|
||||
|
||||
[data-datepicker-menu] {
|
||||
height: 21.5rem;
|
||||
padding-block: 0.5rem;
|
||||
[data-md-datepicker-menu] {
|
||||
height: 344px;
|
||||
padding-block: var(--md-sys-measurement-space100);
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
border-top: 1px solid var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
[data-datepicker-option] {
|
||||
--datepicker-layer: var(--md-sys-color-on-surface);
|
||||
|
||||
position: relative;
|
||||
[data-md-datepicker-option] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
gap: var(--md-sys-measurement-space200);
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
padding-inline: 1rem;
|
||||
height: 48px;
|
||||
padding-inline: var(--md-sys-measurement-space200);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
font: var(--md-sys-typescale-body-lg);
|
||||
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
[data-datepicker-option][aria-selected="true"] {
|
||||
/* The foundation's ring, drawn inside the full-width row rather than round it. */
|
||||
[data-md-datepicker-option]:focus-visible {
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
[data-md-datepicker-option][aria-selected='true'] {
|
||||
background-color: var(--md-sys-color-surface-container-highest);
|
||||
}
|
||||
|
||||
[data-datepicker-option][aria-disabled="true"] {
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||||
[data-md-datepicker-option][aria-disabled='true'] {
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
[data-datepicker-check] {
|
||||
[data-md-datepicker-check] {
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
[data-datepicker-option]:not([aria-selected="true"]) [data-datepicker-check] {
|
||||
[data-md-datepicker-option]:not([aria-selected='true']) [data-md-datepicker-check] {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* ---- The modal input ------------------------------------------------------------------------ */
|
||||
|
||||
[data-datepicker-entry] {
|
||||
padding: 0.625rem 1.5rem 1rem;
|
||||
[data-md-datepicker-entry] {
|
||||
padding: var(--md-sys-measurement-space125) var(--md-sys-measurement-space300) var(--md-sys-measurement-space200);
|
||||
}
|
||||
|
||||
[data-datepicker-entry-fields][data-range] {
|
||||
[data-md-datepicker-entry-fields][data-md-range] {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
|
||||
/* ---- Cancel and OK ---------------------------------------------------------------------------- */
|
||||
|
||||
[data-datepicker-actions] {
|
||||
[data-md-datepicker-actions] {
|
||||
display: flex;
|
||||
flex: none;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 0.375rem 0.5rem 0.75rem;
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
padding: var(--md-sys-measurement-space50) var(--md-sys-measurement-space75) var(--md-sys-measurement-space100) 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* <x-divider>: M3's divider (DividerTokens.kt, androidx Compose Material 3, Apache-2.0) — a single
|
||||
* 1px outline-variant line, full-width by default, `data-md-inset` 16px from the start or
|
||||
* `data-md-middle` 16px from both ends (docs/reference/m3/components-actions-communication-containment.md
|
||||
* § Divider → Specs: "Divider inset left margin 16dp", "Divider middle-inset left/right margin
|
||||
* 16dp"). A vertical divider (`data-md-orientation="vertical"`) stretches the height of its row
|
||||
* instead of drawing a width.
|
||||
*
|
||||
* `data-md-divider-heading` draws the divider with a subheader, M3's way to head a group in a list
|
||||
* or a menu: the words lead, 4px of gap to the rule that runs on from them, 8px short of the end
|
||||
* and 8px above the next row (the same section's "Space between divider & supporting text 4dp",
|
||||
* "Divider right margin 8dp", "Divider bottom margin 8dp"). The label, `data-md-divider-text`, is
|
||||
* the rich tooltip's subhead type — title-small in on-surface-variant — since the divider's own
|
||||
* page names no type role for it.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@layer material.components {
|
||||
[data-md-divider] {
|
||||
flex-shrink: 0;
|
||||
background-color: var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
[data-md-divider][data-md-orientation='horizontal'] {
|
||||
width: auto;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
[data-md-divider][data-md-orientation='vertical'] {
|
||||
width: 1px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
[data-md-divider][data-md-orientation='horizontal'][data-md-inset] {
|
||||
margin-inline-start: var(--md-sys-measurement-space200);
|
||||
}
|
||||
|
||||
[data-md-divider][data-md-orientation='horizontal'][data-md-middle] {
|
||||
margin-inline: var(--md-sys-measurement-space200);
|
||||
}
|
||||
|
||||
[data-md-divider][data-md-orientation='vertical'][data-md-middle] {
|
||||
margin-block: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-md-divider-heading] {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: var(--md-sys-measurement-space50);
|
||||
padding-bottom: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-md-divider-heading][data-md-inset] {
|
||||
margin-inline-start: var(--md-sys-measurement-space200);
|
||||
}
|
||||
|
||||
[data-md-divider-heading][data-md-middle] {
|
||||
margin-inline: var(--md-sys-measurement-space200);
|
||||
}
|
||||
|
||||
[data-md-divider-text] {
|
||||
flex-shrink: 0;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-title-sm);
|
||||
letter-spacing: var(--md-sys-typescale-title-sm-tracking);
|
||||
}
|
||||
|
||||
[data-md-divider-heading] [data-md-divider] {
|
||||
flex: 1 1 0%;
|
||||
min-width: 0;
|
||||
margin-inline-end: var(--md-sys-measurement-space100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
/*
|
||||
* `<x-drawer>`: M3's side sheet, modal by default and co-planar as `standard` from 840px
|
||||
* (docs/reference/m3/components-actions-communication-containment.md § Side sheets → Specs; no
|
||||
* androidx Compose token file exists for it — Compose support is "Unavailable" per the site — so
|
||||
* the numbers come from the specs page and, for the standard variant's 0dp elevation,
|
||||
* material-components-android's `SideSheet.md`).
|
||||
*
|
||||
* [data-md-drawer] the wrapper; data-md-open, data-md-standard
|
||||
* [data-md-drawer-scrim] modal only (a standard sheet's is hidden from 840px)
|
||||
* [data-md-drawer-sheet] surface-container-low, elevation 1; data-md-side
|
||||
* [data-md-drawer-head] title/subtitle/close, and the separator's divider
|
||||
* [data-md-drawer-head-row], [data-md-drawer-heading], [data-md-drawer-title],
|
||||
* [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 — 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
|
||||
* anchored to an edge that overshot would open a gap, and leaves on emphasized accelerate: the
|
||||
* view keeps `x-show`, so `@starting-style` gives the entry its start and the view's `x-transition`
|
||||
* holds `display` through the exit, whose timing comes from the closed state's own rule (a
|
||||
* transition takes the after-change style's). Not `allow-discrete` on `display`: Firefox does not
|
||||
* transition `display`, and in the other engines it would hold a second time after Alpine's hold.
|
||||
* Alpine holds for the first `transition-duration` listed, so each exit's longest transition is
|
||||
* listed first. Reduced motion zeroes both duration tokens (tokens/motion.css).
|
||||
* Below `expanded` it slides, from whichever edge `data-md-side` names in the document's direction;
|
||||
* once `standard` makes it co-planar it only fades (an in-flow box does not need to travel). Every
|
||||
* state rule matches the sheet and scrim as the root's children, so a sheet nested in an open one
|
||||
* keeps its own.
|
||||
*
|
||||
* A `standard` sheet drops the scrim, the trap and the elevation, and swaps surface-container-low
|
||||
* for `surface` with an outline-variant rule down its inner edge, in place of the scrim
|
||||
* (its anatomy's "Divider (optional)"). Below `expanded` (840px) it is the modal sheet — M3 caps
|
||||
* a side sheet at 400dp, and a 600px window has too little room left beside one, so the switch
|
||||
* sits at `expanded` rather than `medium`. Opening it shrinks the body beside it and closing it
|
||||
* gives the room back (M3's side sheets, "Adaptive"): the root's `inline-size` springs from none to
|
||||
* the sheet's width — the sheet itself keeps its width, sits at the root's far edge and is clipped,
|
||||
* so it is uncovered from its inner edge — together with a negative margin as wide as the flex
|
||||
* parent's gap (`--md-drawer-gap`, which the view measures), so the content beside does not jump by
|
||||
* the gap when the root leaves the layout. `data-md-drawer-collapsed` (the view, while the window is
|
||||
* ≥ 840px and the sheet is closed) then takes the root out of the layout entirely, but only once the
|
||||
* exit has run: the view's `closing` holds it, where it once cut the fade and the resize off on
|
||||
* their first frame.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './button.css';
|
||||
@import './divider.css';
|
||||
|
||||
@layer material.components {
|
||||
/* The scrim fades in from `@starting-style` and back out while the view's `x-transition` holds
|
||||
its `display` for this duration. Not `allow-discrete` on `display`: Firefox does not transition
|
||||
`display`, so the scrim vanished there, and in the other engines it would hold a second time
|
||||
after Alpine's hold. */
|
||||
[data-md-drawer-scrim] {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 40;
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent);
|
||||
opacity: 0;
|
||||
transition-property: opacity;
|
||||
transition-duration: var(--md-sys-motion-effects-default-duration);
|
||||
transition-timing-function: var(--md-sys-motion-effects-default);
|
||||
}
|
||||
|
||||
[data-md-drawer][data-md-open] > [data-md-drawer-scrim] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
[data-md-drawer][data-md-open] > [data-md-drawer-scrim] {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Closed, the sheet waits one width past its own edge (`--sheet-offset`, mirrored in RTL) and
|
||||
slides there on emphasized accelerate; open, it comes back on emphasized decelerate. */
|
||||
[data-md-drawer-sheet] {
|
||||
--sheet-offset: 100%;
|
||||
|
||||
position: fixed;
|
||||
top: var(--material-safe-top, env(safe-area-inset-top));
|
||||
bottom: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
padding: var(--md-sys-measurement-space300);
|
||||
padding-block-end: calc(var(--md-sys-measurement-space300) + var(--material-safe-bottom, env(safe-area-inset-bottom)));
|
||||
background-color: var(--md-sys-color-surface-container-low);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
box-shadow: var(--md-sys-elevation-1);
|
||||
translate: var(--sheet-offset) 0;
|
||||
transition-property: translate;
|
||||
transition-duration: var(--md-sys-motion-effects-default-duration);
|
||||
transition-timing-function: var(--md-sys-motion-easing-emphasized-accelerate);
|
||||
}
|
||||
|
||||
[data-md-drawer-sheet][data-md-side='end'] {
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
|
||||
[data-md-drawer-sheet][data-md-side='start'] {
|
||||
--sheet-offset: -100%;
|
||||
|
||||
inset-inline-start: 0;
|
||||
}
|
||||
|
||||
[data-md-drawer-sheet][data-md-side='end']:is([dir='rtl'], [dir='rtl'] *) {
|
||||
--sheet-offset: -100%;
|
||||
}
|
||||
|
||||
[data-md-drawer-sheet][data-md-side='start']:is([dir='rtl'], [dir='rtl'] *) {
|
||||
--sheet-offset: 100%;
|
||||
}
|
||||
|
||||
[data-md-drawer][data-md-open] > [data-md-drawer-sheet] {
|
||||
translate: 0 0;
|
||||
transition-duration: var(--md-sys-motion-spatial-default-duration);
|
||||
transition-timing-function: var(--md-sys-motion-easing-emphasized-decelerate);
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
[data-md-drawer][data-md-open] > [data-md-drawer-sheet] {
|
||||
translate: var(--sheet-offset) 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 600px) {
|
||||
[data-md-drawer-sheet] {
|
||||
width: var(--sheet-width);
|
||||
max-width: calc(100vw - 64px);
|
||||
}
|
||||
|
||||
[data-md-drawer-sheet][data-md-side='end'] {
|
||||
border-start-start-radius: var(--md-sys-shape-corner-lg);
|
||||
border-end-start-radius: var(--md-sys-shape-corner-lg);
|
||||
}
|
||||
|
||||
[data-md-drawer-sheet][data-md-side='start'] {
|
||||
border-start-end-radius: var(--md-sys-shape-corner-lg);
|
||||
border-end-end-radius: var(--md-sys-shape-corner-lg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Co-planar from expanded: sticky beside the content, no scrim, no elevation, flat on
|
||||
`surface` with an outline-variant rule down the inner edge instead. It only fades here —
|
||||
an in-flow box does not travel like an overlay does. It sticks under the top safe area
|
||||
(`--drawer-top`), as tall as the window below it, and its padding keeps its foot clear of
|
||||
the bottom one, so a status bar never covers its head once the page scrolls. */
|
||||
@media (width >= 840px) {
|
||||
[data-md-drawer][data-md-standard] {
|
||||
--drawer-top: var(--material-safe-top, env(safe-area-inset-top));
|
||||
|
||||
position: sticky;
|
||||
top: var(--drawer-top);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
height: calc(100dvh - var(--drawer-top));
|
||||
flex-shrink: 0;
|
||||
align-self: flex-start;
|
||||
overflow: clip;
|
||||
inline-size: 0;
|
||||
margin-inline-start: calc(-1 * var(--md-drawer-gap, 0px));
|
||||
transition-property: inline-size, margin-inline;
|
||||
transition-duration: var(--md-sys-motion-effects-default-duration);
|
||||
transition-timing-function: var(--md-sys-motion-easing-emphasized-accelerate);
|
||||
}
|
||||
|
||||
/* In <x-scaffold>, a sticky `<x-app-bar>` in the `top` slot spans the content region, sheet
|
||||
and all, so the sheet sticks under it: a small, centre-aligned or search bar is its 64px
|
||||
row (AppBarSmallTokens.ContainerHeight) under the safe area it pads itself with; a medium
|
||||
or large bar collapses to that 64px row alone, which the safe area may still be taller
|
||||
than. */
|
||||
[data-md-scaffold-content]:has(> [data-md-app-bar][data-md-sticky]) [data-md-drawer][data-md-standard] {
|
||||
--drawer-top: calc(var(--material-safe-top, env(safe-area-inset-top)) + 64px);
|
||||
}
|
||||
|
||||
[data-md-scaffold-content]:has(> [data-md-app-bar][data-md-sticky]:is([data-md-variant='medium'], [data-md-variant='large'])) [data-md-drawer][data-md-standard] {
|
||||
--drawer-top: max(var(--material-safe-top, env(safe-area-inset-top)), 64px);
|
||||
}
|
||||
|
||||
/* A start sheet sits at the start edge, with the gap after it. */
|
||||
[data-md-drawer][data-md-standard]:where(:has(> [data-md-drawer-sheet][data-md-side='start'])) {
|
||||
justify-content: flex-start;
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: calc(-1 * var(--md-drawer-gap, 0px));
|
||||
}
|
||||
|
||||
[data-md-drawer][data-md-standard][data-md-open] {
|
||||
inline-size: min(var(--sheet-width), calc(100vw - 64px));
|
||||
margin-inline: 0;
|
||||
transition-duration: var(--md-sys-motion-spatial-default-duration);
|
||||
transition-timing-function: var(--md-sys-motion-easing-emphasized-decelerate);
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
[data-md-drawer][data-md-standard][data-md-open] {
|
||||
inline-size: 0;
|
||||
margin-inline-start: calc(-1 * var(--md-drawer-gap, 0px));
|
||||
}
|
||||
|
||||
[data-md-drawer][data-md-standard][data-md-open]:where(:has(> [data-md-drawer-sheet][data-md-side='start'])) {
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: calc(-1 * var(--md-drawer-gap, 0px));
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-drawer][data-md-standard][data-md-drawer-collapsed] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Before the view has settled, a sheet not rendered open is out of the layout already. The
|
||||
view writes `data-md-drawer-collapsed` and measures the row's gap only once Alpine runs,
|
||||
so until then a closed sheet would be a zero-wide flex item that still costs its row one
|
||||
gap, and the content beside it would widen when the script started. A sheet whose
|
||||
`wire:model` property is open is rendered `data-md-open` and stands at its width. */
|
||||
[data-md-drawer][data-md-standard]:not([data-md-open], [data-md-drawer-settled]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Until the view settles (`data-md-drawer-settled`, two frames after Alpine starts), the
|
||||
state the page loads with is drawn at once: a sheet that starts open does not grow in. */
|
||||
[data-md-drawer][data-md-standard]:not([data-md-drawer-settled]),
|
||||
[data-md-drawer][data-md-standard]:not([data-md-drawer-settled]) > [data-md-drawer-sheet] {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
[data-md-drawer][data-md-standard] > [data-md-drawer-scrim] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-md-drawer][data-md-standard] > [data-md-drawer-sheet] {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
top: 0;
|
||||
z-index: auto;
|
||||
height: 100%;
|
||||
translate: 0 0;
|
||||
border-radius: var(--md-sys-shape-corner-none);
|
||||
background-color: var(--md-sys-color-surface);
|
||||
box-shadow: none;
|
||||
opacity: 0;
|
||||
transition-property: opacity;
|
||||
transition-duration: var(--md-sys-motion-effects-default-duration);
|
||||
transition-timing-function: var(--md-sys-motion-easing-emphasized-accelerate);
|
||||
}
|
||||
|
||||
[data-md-drawer][data-md-standard][data-md-open] > [data-md-drawer-sheet] {
|
||||
opacity: 1;
|
||||
transition-duration: var(--md-sys-motion-spatial-default-duration);
|
||||
transition-timing-function: var(--md-sys-motion-easing-emphasized-decelerate);
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
[data-md-drawer][data-md-standard][data-md-open] > [data-md-drawer-sheet] {
|
||||
translate: 0 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-drawer][data-md-standard] > [data-md-drawer-sheet][data-md-side='end'] {
|
||||
border-inline-start: 1px solid var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
[data-md-drawer][data-md-standard] > [data-md-drawer-sheet][data-md-side='start'] {
|
||||
border-inline-end: 1px solid var(--md-sys-color-outline-variant);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-drawer-head] {
|
||||
margin-bottom: var(--md-sys-measurement-space200);
|
||||
}
|
||||
|
||||
[data-md-drawer-head-row] {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
[data-md-drawer-heading] {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-md-drawer-title] {
|
||||
font: var(--md-sys-typescale-title-lg);
|
||||
letter-spacing: var(--md-sys-typescale-title-lg-tracking);
|
||||
}
|
||||
|
||||
[data-md-drawer-subtitle] {
|
||||
margin-top: var(--md-sys-measurement-space50);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-md);
|
||||
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
||||
}
|
||||
|
||||
[data-md-drawer-close] {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
margin-top: calc(-1 * var(--md-sys-measurement-space100));
|
||||
margin-inline-end: -12px;
|
||||
}
|
||||
|
||||
[data-md-drawer-head] > [data-md-divider] {
|
||||
margin-top: var(--md-sys-measurement-space200);
|
||||
}
|
||||
|
||||
[data-md-drawer-body] {
|
||||
container-type: inline-size;
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
[data-md-drawer-actions] {
|
||||
display: flex;
|
||||
min-height: var(--md-sys-measurement-space900);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
padding-top: var(--md-sys-measurement-space200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* <x-empty-state>: "you have nothing here yet" — not an M3 component; built from M3 Expressive's
|
||||
* parts, a shape in secondary-container behind an icon in on-secondary-container, a title-large
|
||||
* title, body-medium description at 448px and a centred row of actions.
|
||||
*
|
||||
* The shape is `position: absolute`, so the icon needs its own stacking context
|
||||
* (`position: relative`) to paint above it in DOM order rather than losing to the shape's own —
|
||||
* an application's illustration slot replaces both and sizes itself.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
@import './shape.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-empty-state] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--md-sys-measurement-space200);
|
||||
padding-inline: var(--md-sys-measurement-space200);
|
||||
padding-block: var(--md-sys-measurement-space500);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-md-empty-state-illustration] {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 112px;
|
||||
height: 112px;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
[data-md-empty-state-shape] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--md-sys-color-secondary-container);
|
||||
}
|
||||
|
||||
[data-md-empty-state-icon] {
|
||||
position: relative;
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
[data-md-empty-state-title] {
|
||||
font: var(--md-sys-typescale-title-lg);
|
||||
letter-spacing: var(--md-sys-typescale-title-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-empty-state-description] {
|
||||
max-width: 448px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-md);
|
||||
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-empty-state-actions] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding-top: var(--md-sys-measurement-space100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* `errors::minimal`: the layout the package's 403/404/419/429/500/503 pages and the framework's
|
||||
* own 401/402 (which extend this layout, `src/Support/ErrorPage.php`'s docblock) render through.
|
||||
* A page's own `@section`s fill `title`, `code`, `headline`, `message`, `shape` and `actions`
|
||||
* (`resources/views/error-pages/errors/minimal.blade.php`'s header) — none of that is this file's
|
||||
* concern, only the geometry and colour of the parts that show them:
|
||||
*
|
||||
* body:has(> [data-md-error-page])
|
||||
* [data-md-error-page]
|
||||
* [data-md-error-art]
|
||||
* [data-md-error-shape] an <x-shape>, turning once a minute unless reduced motion asks
|
||||
* otherwise (its `svg` fills the art box and is coloured here,
|
||||
* since <x-shape size> only takes a fixed px, not "fill parent")
|
||||
* [data-md-error-code] the status, in display type over the shape
|
||||
* [data-md-error-headline] what happened
|
||||
* [data-md-error-message] one sentence on what to do, only when `headline` is its own
|
||||
* section (otherwise `message` doubles as the headline)
|
||||
* [data-md-error-actions]
|
||||
*
|
||||
* The page carries this file itself: `src/Support/ErrorPage.php` inlines it, with its imports, into
|
||||
* every error page (`Stylesheets::bundle()`) — beside the application's Vite tags when there is a
|
||||
* build (`layoutStyles()`), inside the whole fallback stylesheet when there is none
|
||||
* (`fallbackStyles()`) — so an application imports nothing for its error pages. The body is still
|
||||
* matched only when it holds the layout (`body:has(> [data-md-error-page])`): an application that
|
||||
* imports `all.css` has this file in its bundle beside every other page, where a bare `body` rule
|
||||
* would restyle them all.
|
||||
*
|
||||
* The page's font is the foundation's, `--md-ref-typeface-brand` on `html` (foundation/base.css),
|
||||
* 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 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;
|
||||
|
||||
@import './button.css';
|
||||
@import './shape.css';
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* <x-fab-menu-item>: one action in an `<x-fab-menu>`, a pill that rises into place as the menu
|
||||
* opens and sinks back as it closes.
|
||||
*
|
||||
* FabMenuBaselineTokens (androidx Compose Material 3, Apache-2.0): 56px tall, 24px either side,
|
||||
* 8px between the icon and the label, 24px icons, full corner, elevation 3, `data-md-color`'s
|
||||
* container. 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 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.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-fab-menu-item] {
|
||||
--md-fab-menu-item-container: var(--md-sys-color-primary-container);
|
||||
--md-fab-menu-item-on-container: var(--md-sys-color-on-primary-container);
|
||||
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
block-size: var(--md-sys-measurement-space700);
|
||||
padding-inline: var(--md-sys-measurement-space300);
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: var(--md-fab-menu-item-container);
|
||||
color: var(--md-fab-menu-item-on-container);
|
||||
box-shadow: var(--md-sys-elevation-3);
|
||||
white-space: nowrap;
|
||||
font: var(--md-sys-typescale-title-md);
|
||||
letter-spacing: var(--md-sys-typescale-title-md-tracking);
|
||||
font-variation-settings: normal;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
translate: 0 0;
|
||||
opacity: 1;
|
||||
transition-property: translate, opacity;
|
||||
transition-duration: var(--md-sys-motion-spatial-fast-duration), var(--md-sys-motion-effects-default-duration);
|
||||
transition-timing-function: var(--md-sys-motion-spatial-fast), var(--md-sys-motion-effects-default);
|
||||
|
||||
@starting-style {
|
||||
translate: 0 var(--md-sys-measurement-space100);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-fab-menu-item][data-md-color='secondary'] {
|
||||
--md-fab-menu-item-container: var(--md-sys-color-secondary-container);
|
||||
--md-fab-menu-item-on-container: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
[data-md-fab-menu-item][data-md-color='tertiary'] {
|
||||
--md-fab-menu-item-container: var(--md-sys-color-tertiary-container);
|
||||
--md-fab-menu-item-on-container: var(--md-sys-color-on-tertiary-container);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* <x-fab-menu>: an M3 Expressive FAB that opens into a short list of related actions.
|
||||
*
|
||||
* 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 (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.
|
||||
*
|
||||
* 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 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
|
||||
* item's one attribute — whichever order the two files are bundled in.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-fab-menu] {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
[data-md-fab-menu-anchor] {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
[data-md-fab-menu-trigger] {
|
||||
--md-fab-menu-container: var(--md-sys-color-primary-container);
|
||||
--md-fab-menu-on-container: var(--md-sys-color-on-primary-container);
|
||||
--md-fab-menu-color: var(--md-sys-color-primary);
|
||||
--md-fab-menu-on-color: var(--md-sys-color-on-primary);
|
||||
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
inline-size: var(--md-sys-measurement-space700);
|
||||
block-size: var(--md-sys-measurement-space700);
|
||||
border-radius: var(--md-sys-shape-corner-lg);
|
||||
background-color: var(--md-fab-menu-container);
|
||||
color: var(--md-fab-menu-on-container);
|
||||
box-shadow: var(--md-sys-elevation-3);
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
transition-property: border-radius, background-color, color, box-shadow;
|
||||
transition-duration:
|
||||
var(--md-sys-motion-spatial-default-duration), var(--md-sys-motion-effects-default-duration),
|
||||
var(--md-sys-motion-effects-default-duration), var(--md-sys-motion-effects-default-duration);
|
||||
transition-timing-function:
|
||||
var(--md-sys-motion-spatial-default), var(--md-sys-motion-effects-default),
|
||||
var(--md-sys-motion-effects-default), var(--md-sys-motion-effects-default);
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
box-shadow: var(--md-sys-elevation-4);
|
||||
}
|
||||
}
|
||||
|
||||
&[aria-expanded='true'] {
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: var(--md-fab-menu-color);
|
||||
color: var(--md-fab-menu-on-color);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-fab-menu-trigger][data-md-color='secondary'] {
|
||||
--md-fab-menu-container: var(--md-sys-color-secondary-container);
|
||||
--md-fab-menu-on-container: var(--md-sys-color-on-secondary-container);
|
||||
--md-fab-menu-color: var(--md-sys-color-secondary);
|
||||
--md-fab-menu-on-color: var(--md-sys-color-on-secondary);
|
||||
}
|
||||
|
||||
[data-md-fab-menu-trigger][data-md-color='tertiary'] {
|
||||
--md-fab-menu-container: var(--md-sys-color-tertiary-container);
|
||||
--md-fab-menu-on-container: var(--md-sys-color-on-tertiary-container);
|
||||
--md-fab-menu-color: var(--md-sys-color-tertiary);
|
||||
--md-fab-menu-on-color: var(--md-sys-color-on-tertiary);
|
||||
}
|
||||
|
||||
[data-md-fab-menu-close-icon] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-md-fab-menu-trigger][aria-expanded='true'] [data-md-fab-menu-icon] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-md-fab-menu-trigger][aria-expanded='true'] [data-md-fab-menu-close-icon] {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
[data-md-fab-menu-popover] {
|
||||
inset: auto;
|
||||
margin: 0;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: var(--md-sys-measurement-space50);
|
||||
max-block-size: calc(100dvh - 128px);
|
||||
overflow-y: auto;
|
||||
border-width: 0;
|
||||
background-color: transparent;
|
||||
padding: var(--md-sys-measurement-space100);
|
||||
|
||||
&:popover-open {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-fab-menu-popover][data-md-position='top-end'] {
|
||||
margin-inline: calc(-1 * var(--md-sys-measurement-space100));
|
||||
margin-block-start: calc(-1 * var(--md-sys-measurement-space100));
|
||||
align-items: flex-end;
|
||||
position-area: top span-left;
|
||||
}
|
||||
|
||||
[data-md-fab-menu-popover][data-md-position='top-start'] {
|
||||
margin-inline: calc(-1 * var(--md-sys-measurement-space100));
|
||||
margin-block-start: calc(-1 * var(--md-sys-measurement-space100));
|
||||
align-items: flex-start;
|
||||
position-area: top span-right;
|
||||
}
|
||||
|
||||
[data-md-fab-menu-popover][data-md-position='bottom-end'] {
|
||||
margin-inline: calc(-1 * var(--md-sys-measurement-space100));
|
||||
margin-block-end: calc(-1 * var(--md-sys-measurement-space100));
|
||||
align-items: flex-end;
|
||||
position-area: bottom span-left;
|
||||
}
|
||||
|
||||
[data-md-fab-menu-popover][data-md-position='bottom-start'] {
|
||||
margin-inline: calc(-1 * var(--md-sys-measurement-space100));
|
||||
margin-block-end: calc(-1 * var(--md-sys-measurement-space100));
|
||||
align-items: flex-start;
|
||||
position-area: bottom span-right;
|
||||
}
|
||||
|
||||
[data-md-fab-menu-popover]:not(:popover-open) > *,
|
||||
[data-md-fab-menu-popover][data-md-popover-closing] > * {
|
||||
translate: 0 var(--md-sys-measurement-space100);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* <x-fab>: M3 Expressive's floating action button and extended FAB.
|
||||
*
|
||||
* FabBaseline/Medium/LargeTokens and ExtendedFab*Tokens (androidx Compose Material 3, Apache-2.0),
|
||||
* per `data-md-size`:
|
||||
*
|
||||
* size FAB corner glyph extended: min width gap padding type
|
||||
* sm 56px lg (16px) 24px 80px 8px 16px title-md
|
||||
* md 80px lg-increased 28px 80px 16px 26px title-lg
|
||||
* lg 96px xl (28px) 32px 96px 20px 28px headline-sm
|
||||
*
|
||||
* A FAB and its extended form share the height and the corner. `data-md-color` is `primary`,
|
||||
* `secondary` or `tertiary`, drawn in its container (`data-md-variant="container"`) or in the
|
||||
* colour itself (`filled`), at elevation 3, 4 under a hovering pointer, the shadow and colours on
|
||||
* the fast effects spring. It renders the foundation's `md-state-layer` and `md-focus-ring`
|
||||
* (foundation/interaction.css); every size draws at 56px or more, so it needs no `md-touch-target`.
|
||||
* `data-md-fab` on the root is also the hook a place uses to draw a nested FAB its own way: a
|
||||
* navigation rail or a docked toolbar flattens it to elevation 0.
|
||||
*
|
||||
* `data-md-collapse-on-scroll` is M3's extended FAB that "can collapse to a FAB on scroll-down and
|
||||
* re-expand to extended on scroll-up — when switching between FAB↔extended FAB, shape changes, the
|
||||
* icon moves left, and the text label fades in/out" (resources/js/fab.js sets
|
||||
* `data-md-collapsed`). `width: auto` cannot transition, so the label sits in a one-column grid
|
||||
* whose track closes to zero; that track is what animates, and the button lays itself out around
|
||||
* it every frame. The gap and the minimum width go with it, all on the default spatial spring,
|
||||
* while the label fades on the effects spring. The collapsed width is the FAB's own size. The
|
||||
* extended padding already centres the glyph at `sm` (16 + 24 + 16) and `md` (26 + 28 + 26); at
|
||||
* `lg` its 28px is 4px short of the 32px that centres a 32px glyph in 96px, so that one moves. The
|
||||
* label stays in the accessibility tree — clipped and transparent, never `display: none` — so the
|
||||
* collapsed FAB keeps the name the extended one had. Under reduced motion tokens/motion.css takes
|
||||
* every duration to zero, so it swaps.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
@import './tooltip.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-fab] {
|
||||
--md-fab-color: var(--md-sys-color-primary);
|
||||
--md-fab-on-color: var(--md-sys-color-on-primary);
|
||||
--md-fab-container: var(--md-sys-color-primary-container);
|
||||
--md-fab-on-container: var(--md-sys-color-on-primary-container);
|
||||
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
block-size: var(--md-fab-size);
|
||||
border-radius: var(--md-fab-corner);
|
||||
background-color: var(--md-fab-container);
|
||||
color: var(--md-fab-on-container);
|
||||
box-shadow: var(--md-sys-elevation-3);
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
transition-property: box-shadow, background-color, color;
|
||||
transition-duration: var(--md-sys-motion-effects-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-effects-fast);
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
box-shadow: var(--md-sys-elevation-4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-fab][data-md-size='sm'] {
|
||||
--md-fab-size: 56px;
|
||||
--md-fab-corner: var(--md-sys-shape-corner-lg);
|
||||
--md-fab-extended-min: 80px;
|
||||
}
|
||||
|
||||
[data-md-fab][data-md-size='md'] {
|
||||
--md-fab-size: 80px;
|
||||
--md-fab-corner: var(--md-sys-shape-corner-lg-increased);
|
||||
--md-fab-extended-min: 80px;
|
||||
}
|
||||
|
||||
[data-md-fab][data-md-size='lg'] {
|
||||
--md-fab-size: 96px;
|
||||
--md-fab-corner: var(--md-sys-shape-corner-xl);
|
||||
--md-fab-extended-min: 96px;
|
||||
}
|
||||
|
||||
[data-md-fab][data-md-color='secondary'] {
|
||||
--md-fab-color: var(--md-sys-color-secondary);
|
||||
--md-fab-on-color: var(--md-sys-color-on-secondary);
|
||||
--md-fab-container: var(--md-sys-color-secondary-container);
|
||||
--md-fab-on-container: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
[data-md-fab][data-md-color='tertiary'] {
|
||||
--md-fab-color: var(--md-sys-color-tertiary);
|
||||
--md-fab-on-color: var(--md-sys-color-on-tertiary);
|
||||
--md-fab-container: var(--md-sys-color-tertiary-container);
|
||||
--md-fab-on-container: var(--md-sys-color-on-tertiary-container);
|
||||
}
|
||||
|
||||
[data-md-fab][data-md-variant='filled'] {
|
||||
background-color: var(--md-fab-color);
|
||||
color: var(--md-fab-on-color);
|
||||
}
|
||||
|
||||
[data-md-fab]:not([data-md-extended]) {
|
||||
inline-size: var(--md-fab-size);
|
||||
}
|
||||
|
||||
[data-md-fab][data-md-extended] {
|
||||
min-inline-size: var(--md-fab-extended-min);
|
||||
}
|
||||
|
||||
[data-md-fab][data-md-extended][data-md-size='sm'] {
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
padding-inline: var(--md-sys-measurement-space200);
|
||||
font: var(--md-sys-typescale-title-md);
|
||||
letter-spacing: var(--md-sys-typescale-title-md-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-fab][data-md-extended][data-md-size='md'] {
|
||||
gap: var(--md-sys-measurement-space200);
|
||||
padding-inline: 26px;
|
||||
font: var(--md-sys-typescale-title-lg);
|
||||
letter-spacing: var(--md-sys-typescale-title-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-fab][data-md-extended][data-md-size='lg'] {
|
||||
gap: 20px;
|
||||
padding-inline: 28px;
|
||||
font: var(--md-sys-typescale-headline-sm);
|
||||
letter-spacing: var(--md-sys-typescale-headline-sm-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
/* The extended FAB that collapses to a FAB while the page scrolls down. */
|
||||
[data-md-fab][data-md-collapse-on-scroll] {
|
||||
transition-property: min-inline-size, padding-inline, gap, box-shadow, background-color, color;
|
||||
transition-duration:
|
||||
var(--md-sys-motion-spatial-default-duration), var(--md-sys-motion-spatial-default-duration), var(--md-sys-motion-spatial-default-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-default), var(--md-sys-motion-spatial-default), var(--md-sys-motion-spatial-default),
|
||||
var(--md-sys-motion-effects-fast), var(--md-sys-motion-effects-fast), var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
[data-md-fab-label] {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
transition-property: grid-template-columns, opacity;
|
||||
transition-duration: var(--md-sys-motion-spatial-default-duration), var(--md-sys-motion-effects-default-duration);
|
||||
transition-timing-function: var(--md-sys-motion-spatial-default), var(--md-sys-motion-effects-default);
|
||||
|
||||
& > span {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-fab][data-md-collapse-on-scroll][data-md-collapsed] {
|
||||
min-inline-size: var(--md-fab-size);
|
||||
gap: 0;
|
||||
|
||||
& [data-md-fab-label] {
|
||||
grid-template-columns: 0fr;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-fab][data-md-collapse-on-scroll][data-md-size='lg'][data-md-collapsed] {
|
||||
padding-inline: var(--md-sys-measurement-space400);
|
||||
}
|
||||
}
|
||||
+195
-252
@@ -9,26 +9,44 @@
|
||||
* `<legend>`, carrying the label at its floated size, cuts the gap the label sits in, so the gap is
|
||||
* right on any background. The filled field has no outline: a surface-container-highest box with
|
||||
* extra-small top corners and an active indicator line, the label floating inside it
|
||||
* (FilledTextFieldTokens / OutlinedTextFieldTokens, androidx Compose Material 3, Apache-2.0).
|
||||
* (FilledTextFieldTokens / OutlinedTextFieldTokens, androidx Compose Material 3, Apache-2.0): 56px
|
||||
* tall, 16px padding, 24px icons 16px from the text, the label in body-small once it floats.
|
||||
* The `sm` (40px) and `xs` (32px) sizes are this package's, for an unlabelled field in a toolbar.
|
||||
* Disabled is on-surface at M3's 38% content and 12% container opacities (tokens/state.css); the
|
||||
* filled field's disabled container is on-surface at 4%, FilledTextFieldTokens'
|
||||
* DisabledContainerOpacity, which has no system token.
|
||||
*
|
||||
* Anatomy (resources/views/components/field.blade.php):
|
||||
*
|
||||
* .field the whole thing; `class` from the call site lands here; data-variant, data-size
|
||||
* .field-box the 56px row: icon, prefix, control, suffix, trailing
|
||||
* .field-control the <input>, <select> or <textarea>
|
||||
* .field-outline the fieldset and its legend (the filled field's indicator line)
|
||||
* .field-label the visible label
|
||||
* .field-support the hint, or the error in its place
|
||||
* [data-md-field] the whole thing; `class` from the call site lands here;
|
||||
* data-md-variant, data-md-size, data-md-invalid, data-md-floated,
|
||||
* data-md-mono, data-md-full, data-md-readonly
|
||||
* [data-md-field-box] the 56px row: icon, prefix, control, suffix, trailing
|
||||
* [data-md-field-icon] the leading icon
|
||||
* [data-md-field-affix] a prefix or suffix beside the value
|
||||
* [data-md-field-control] the <input>, <select> or <textarea>
|
||||
* [data-md-field-trailing] what ends the row; [data-md-field-error] is the error icon M3 asks
|
||||
* for as the error state's second indicator; [data-md-field-button]
|
||||
* a trailing icon button (clear, copy, reveal, open a picker)
|
||||
* [data-md-field-outline] the fieldset and its legend (the filled field's indicator line)
|
||||
* [data-md-field-label] the visible label
|
||||
* [data-md-field-support-row] the row under the field
|
||||
* [data-md-field-support] the hint, or the error in its place
|
||||
* [data-md-field-counter] M3's character counter, `n/max`, at the end of that row
|
||||
*
|
||||
* A select's open list is not part of the field: it is the dropdown menu in components/menu.css.
|
||||
*/
|
||||
|
||||
@layer components {
|
||||
.field {
|
||||
--field-height: 3.5rem;
|
||||
--field-pad: 1rem;
|
||||
--field-icon: 1.5rem;
|
||||
--field-gap: 1rem;
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-field] {
|
||||
--field-height: 56px;
|
||||
--field-pad: var(--md-sys-measurement-space200);
|
||||
--field-icon: 24px;
|
||||
--field-gap: var(--md-sys-measurement-space200);
|
||||
--field-start: var(--field-pad);
|
||||
--field-edge: var(--md-sys-color-outline);
|
||||
--field-ink: var(--md-sys-color-on-surface-variant);
|
||||
@@ -38,30 +56,45 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* M3 § Text Fields, Behaviour: "compact breakpoints can let a text field span full width;
|
||||
* medium/expanded should bound it with flexible margins/other containers — never let it span the
|
||||
* full width of a large screen." The site names no number, so 40rem is this package's: it is
|
||||
* under half the `large` breakpoint's 1200px, a little over half the `expanded` one's, and holds
|
||||
* about the 70 characters body-large reads best at — a measure of text, so it is in rem and grows
|
||||
* with the text. It is a ceiling, not a width — a narrower pane still gets a narrower field. A
|
||||
* width rule from the call site beats it, because an application's CSS outranks this layer, and
|
||||
* `full` takes it off for a field that really is the width of its pane (a search-and-filter row,
|
||||
* an editor). Below `medium` nothing is bounded. */
|
||||
@media (width >= 600px) {
|
||||
[data-md-field]:not([data-md-full]) {
|
||||
max-width: 40rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* A filled field's resting indicator line is on-surface-variant, where an outline is outline. */
|
||||
.field[data-variant="filled"] {
|
||||
[data-md-field][data-md-variant='filled'] {
|
||||
--field-edge: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
.field[data-size="sm"] {
|
||||
--field-height: 2.5rem;
|
||||
--field-pad: 0.75rem;
|
||||
--field-icon: 1.25rem;
|
||||
--field-gap: 0.5rem;
|
||||
[data-md-field][data-md-size='sm'] {
|
||||
--field-height: 40px;
|
||||
--field-pad: 12px;
|
||||
--field-icon: 20px;
|
||||
--field-gap: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
.field[data-size="xs"] {
|
||||
--field-height: 2rem;
|
||||
--field-pad: 0.625rem;
|
||||
--field-icon: 1rem;
|
||||
--field-gap: 0.375rem;
|
||||
[data-md-field][data-md-size='xs'] {
|
||||
--field-height: 32px;
|
||||
--field-pad: var(--md-sys-measurement-space125);
|
||||
--field-icon: 16px;
|
||||
--field-gap: var(--md-sys-measurement-space75);
|
||||
}
|
||||
|
||||
.field:has(.field-icon) {
|
||||
[data-md-field]:has([data-md-field-icon]) {
|
||||
--field-start: calc(var(--field-pad) + var(--field-icon) + var(--field-gap));
|
||||
}
|
||||
|
||||
.field-box {
|
||||
[data-md-field-box] {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -72,7 +105,7 @@
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.field-control {
|
||||
[data-md-field-control] {
|
||||
flex: 1 1 0%;
|
||||
min-width: 0;
|
||||
align-self: stretch;
|
||||
@@ -84,219 +117,115 @@
|
||||
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
||||
}
|
||||
|
||||
.field[data-size="xs"] .field-control {
|
||||
[data-md-field][data-md-size='xs'] [data-md-field-control] {
|
||||
font: var(--md-sys-typescale-body-md);
|
||||
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
||||
}
|
||||
|
||||
.field[data-mono] .field-control {
|
||||
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
||||
}
|
||||
|
||||
.field-control::placeholder {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
opacity: 1;
|
||||
[data-md-field][data-md-mono] [data-md-field-control] {
|
||||
font-family: var(--md-ref-typeface-mono);
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
.field-control:-webkit-autofill {
|
||||
[data-md-field-control]:-webkit-autofill {
|
||||
-webkit-text-fill-color: var(--md-sys-color-on-surface);
|
||||
transition: background-color 604800s, color 604800s;
|
||||
}
|
||||
|
||||
/* The browser draws the resize grip in the textarea's own corner, so the
|
||||
textarea reaches to just inside the outline's end and bottom — the grip sits
|
||||
in the field's corner, not on the bottom outline a padding in from it — and
|
||||
its padding puts the text back where it was. */
|
||||
textarea.field-control {
|
||||
--field-grip: 0.25rem;
|
||||
|
||||
/* The first half-line of the top offset is a margin, not padding: text scrolled up in a full
|
||||
textarea disappears under the edge instead of running through the label. */
|
||||
--textarea-clear: 0.5rem;
|
||||
--textarea-pad-top: calc((var(--field-height) - 1.5rem) / 2 - var(--textarea-clear));
|
||||
--textarea-pad-bottom: calc((var(--field-height) - 1.5rem) / 2 - var(--field-grip));
|
||||
|
||||
margin-block: var(--textarea-clear) var(--field-grip);
|
||||
margin-inline-end: calc(var(--field-grip) - var(--field-pad));
|
||||
padding-block: var(--textarea-pad-top) var(--textarea-pad-bottom);
|
||||
padding-inline-end: calc(var(--field-pad) - var(--field-grip));
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* `<x-textarea>` grows with what is typed, from `rows` lines up to `max-rows`. Where the browser
|
||||
cannot size a field to its content, resources/js/field.js sets the height instead. */
|
||||
textarea.field-control[data-autogrow] {
|
||||
field-sizing: content;
|
||||
min-block-size: calc(var(--field-rows, 3) * 1.5rem + var(--textarea-pad-top) + var(--textarea-pad-bottom));
|
||||
max-block-size: calc(var(--field-max-rows, 1000) * 1.5rem + var(--textarea-pad-top) + var(--textarea-pad-bottom));
|
||||
resize: none;
|
||||
}
|
||||
|
||||
/* A select covers its whole field — out over the padding and the leading icon at
|
||||
the start, under the arrow to the end — and its own padding puts the value back
|
||||
where it was. So a press anywhere on the field opens it, and the list, which
|
||||
the browser anchors to the select, is the field's width (components/menu.css). */
|
||||
select.field-control {
|
||||
--field-end: calc(var(--field-icon) + var(--field-gap) + var(--field-pad));
|
||||
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
margin-inline: calc(-1 * var(--field-start)) calc(-1 * var(--field-end));
|
||||
padding-inline: var(--field-start) var(--field-end);
|
||||
}
|
||||
|
||||
select.field-control:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Where the browser has a customizable select, the closed select is its own
|
||||
button: without the border, padding and arrow that button brings, it is the
|
||||
same field as before. While its list is open the field reads as focused —
|
||||
said from `:open`, because focus inside the list is in the top layer — and
|
||||
the arrow turns over, as the searchable choices' does. */
|
||||
@supports (appearance: base-select) {
|
||||
select.field-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 0;
|
||||
padding-block: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
select.field-control::picker-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.field-box:has(select.field-control:open) {
|
||||
--field-edge: var(--md-sys-color-primary);
|
||||
--field-ink: var(--md-sys-color-primary);
|
||||
|
||||
.field-outline {
|
||||
border-width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.field:has(select.field-control:open) .field-arrow {
|
||||
rotate: 180deg;
|
||||
}
|
||||
}
|
||||
|
||||
/* `<x-file>`: the browser's own "No file chosen" line is transparent (the caller shows what was
|
||||
chosen), and its button is a tonal pill — the thing to press. */
|
||||
input[type="file"].field-control {
|
||||
align-self: center;
|
||||
color: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="file"].field-control::file-selector-button {
|
||||
height: 2rem;
|
||||
margin-inline-end: 0.75rem;
|
||||
padding-inline: 1rem;
|
||||
border: 0;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: var(--md-sys-color-secondary-container);
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
cursor: pointer;
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
input[type="file"].field-control:hover::file-selector-button {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 8%, var(--md-sys-color-secondary-container));
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"].field-control:disabled::file-selector-button {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent);
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* A combobox's arrow turns over while its list is open, as a select's does. */
|
||||
.field:has(.field-control[aria-expanded="true"]) .field-arrow {
|
||||
[data-md-field]:has([data-md-field-control][aria-expanded='true']) [data-md-field-arrow] {
|
||||
rotate: 180deg;
|
||||
}
|
||||
|
||||
.field-arrow {
|
||||
[data-md-field-arrow] {
|
||||
transition: rotate var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
.field-icon,
|
||||
.field-trailing {
|
||||
[data-md-field-icon],
|
||||
[data-md-field-trailing] {
|
||||
flex: none;
|
||||
width: var(--field-icon);
|
||||
height: var(--field-icon);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
.field-icon,
|
||||
.field-arrow {
|
||||
/* The error's second indicator: M3 asks that the colour change never stand alone. */
|
||||
[data-md-field-error] {
|
||||
color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
[data-md-field-icon],
|
||||
[data-md-field-arrow] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* A trailing button — clear, copy, reveal — is an icon button: the icon in a 40px state layer. */
|
||||
.field-button {
|
||||
/* A trailing button — clear, copy, reveal, the datepicker's calendar and the timepicker's clock —
|
||||
is an icon button: the icon in a state layer that scales with the field's own icon size (40px
|
||||
at `md`, smaller at `sm`/`xs`), which the foundation's fixed-size classes cannot draw; the
|
||||
layer doubles as the touch target, since it already reaches 40px+ without a second pseudo. The
|
||||
hook stays `data-md-field-button` rather than a class for that reason alone now: every other
|
||||
component that draws one — input, password, datepicker, timepicker — selects it from here. */
|
||||
[data-md-field-button] {
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin-inline-end: -0.25rem;
|
||||
margin-inline-end: calc(-1 * var(--md-sys-measurement-space50));
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.field-button::before {
|
||||
content: "";
|
||||
[data-md-field-button]::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 50% auto auto 50%;
|
||||
width: calc(var(--field-icon) + 1rem);
|
||||
height: calc(var(--field-icon) + 1rem);
|
||||
width: calc(var(--field-icon) + var(--md-sys-measurement-space200));
|
||||
height: calc(var(--field-icon) + var(--md-sys-measurement-space200));
|
||||
translate: -50% -50%;
|
||||
border-radius: inherit;
|
||||
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.field-button:hover::before {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) 8%, transparent);
|
||||
[data-md-field-button]:hover::before {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.field-button:focus-visible::before {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) 10%, transparent);
|
||||
[data-md-field-button]:focus-visible::before {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-focus-state-layer-opacity) * 100%), transparent);
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.field-button:active::before {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) 10%, transparent);
|
||||
[data-md-field-button]:active::before {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-pressed-state-layer-opacity) * 100%), transparent);
|
||||
}
|
||||
|
||||
.field-affix {
|
||||
[data-md-field-button]:disabled::before,
|
||||
[data-md-field-button][aria-disabled='true']::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-md-field-affix] {
|
||||
flex: none;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-lg);
|
||||
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
||||
}
|
||||
|
||||
.field[data-size="xs"] .field-affix {
|
||||
[data-md-field][data-md-size='xs'] [data-md-field-affix] {
|
||||
font: var(--md-sys-typescale-body-md);
|
||||
}
|
||||
|
||||
.field-outline {
|
||||
/* The legend is body-small's 16px line less its 5px above the outline: the fieldset starts
|
||||
5px up so the notch's edge runs through the middle of the floated label. */
|
||||
[data-md-field-outline] {
|
||||
position: absolute;
|
||||
inset: -0.3125rem 0 0;
|
||||
margin: 0;
|
||||
padding: 0 calc(var(--field-pad) - 0.25rem);
|
||||
padding: 0 calc(var(--field-pad) - var(--md-sys-measurement-space50));
|
||||
border: 1px solid var(--field-edge);
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
@@ -304,7 +233,7 @@
|
||||
transition: border-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
.field-outline > legend {
|
||||
[data-md-field-outline] > legend {
|
||||
display: block;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
@@ -318,12 +247,12 @@
|
||||
transition: max-width var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
.field-outline > legend > span {
|
||||
[data-md-field-outline] > legend > span {
|
||||
display: inline-block;
|
||||
padding-inline: 0.25rem;
|
||||
padding-inline: var(--md-sys-measurement-space50);
|
||||
}
|
||||
|
||||
.field-label {
|
||||
[data-md-field-label] {
|
||||
position: absolute;
|
||||
inset-inline-start: var(--field-pad);
|
||||
top: 0;
|
||||
@@ -343,126 +272,157 @@
|
||||
|
||||
/* `required` is marked from the control itself, in the label and in the notch,
|
||||
so the gap is cut for the asterisk too. */
|
||||
.field:has(.field-control:required) .field-label::after,
|
||||
.field:has(.field-control:required) .field-outline > legend > span::after {
|
||||
content: " *";
|
||||
[data-md-field]:has([data-md-field-control]:required) [data-md-field-label]::after,
|
||||
[data-md-field]:has([data-md-field-control]:required) [data-md-field-outline] > legend > span::after {
|
||||
content: ' *';
|
||||
}
|
||||
|
||||
/* Resting: a label, an empty control, and no focus. The label sits where the
|
||||
text will go, at the text's size; the notch closes; the placeholder and the
|
||||
prefix wait, because the label is standing where they would be. A select and
|
||||
a date always hold a value, so they mark themselves `data-floated`. */
|
||||
.field:not([data-floated]):has(.field-label):has(.field-control:placeholder-shown):not(:focus-within) {
|
||||
.field-label {
|
||||
a date always hold a value, so they mark themselves `data-md-floated`. */
|
||||
[data-md-field]:not([data-md-floated]):has([data-md-field-label]):has([data-md-field-control]:placeholder-shown):not(:focus-within) {
|
||||
[data-md-field-label] {
|
||||
top: 50%;
|
||||
inset-inline-start: var(--field-start);
|
||||
font: var(--md-sys-typescale-body-lg);
|
||||
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
||||
}
|
||||
|
||||
.field-outline > legend {
|
||||
[data-md-field-outline] > legend {
|
||||
max-width: 0.01px;
|
||||
}
|
||||
|
||||
.field-affix {
|
||||
[data-md-field-affix] {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.field[data-size="xs"]:not([data-floated]):has(.field-label):has(.field-control:placeholder-shown):not(:focus-within) .field-label {
|
||||
[data-md-field][data-md-size='xs']:not([data-md-floated]):has([data-md-field-label]):has([data-md-field-control]:placeholder-shown):not(:focus-within) [data-md-field-label] {
|
||||
font: var(--md-sys-typescale-body-md);
|
||||
}
|
||||
|
||||
/* A textarea's label rests on its first line, not in the middle of the box. */
|
||||
.field:not([data-floated]):has(.field-label):has(textarea.field-control:placeholder-shown):not(:focus-within) .field-label {
|
||||
top: calc(var(--field-height) / 2);
|
||||
}
|
||||
|
||||
.field:has(.field-label):not(:focus-within) .field-control::placeholder {
|
||||
[data-md-field]:has([data-md-field-label]):not(:focus-within) [data-md-field-control]::placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
/* Clearing is offered only once there is something to clear. */
|
||||
.field:has(.field-control:placeholder-shown) .field-clear {
|
||||
[data-md-field]:has([data-md-field-control]:placeholder-shown) [data-md-field-clear] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.field-box:hover {
|
||||
--field-edge: var(--md-sys-color-on-surface);
|
||||
/* Hover only where a pointer can hover, and never on a disabled field: M3 gives a disabled
|
||||
control no state layer at all, and the disabled edge below is inherited, so a declaration
|
||||
here would beat it whatever the selector weighs. M3 layers focus over hover, so a focused
|
||||
field keeps its focus edge under the pointer: the condition is weightless (`:where()`, where
|
||||
`:not(:has(…))` would weigh as much as its argument), and each state drawn over hover
|
||||
outweighs it — focus (0,3,0) over hover (0,2,0), the error's focus (0,4,0) over the error's
|
||||
hover (0,3,0), and select.css's open select (0,3,1) over both. */
|
||||
@media (hover: hover) {
|
||||
:where([data-md-field]:not(:has([data-md-field-control]:disabled))) [data-md-field-box]:hover {
|
||||
--field-edge: var(--md-sys-color-on-surface);
|
||||
}
|
||||
}
|
||||
|
||||
.field-box:focus-within {
|
||||
[data-md-field] [data-md-field-box]:focus-within {
|
||||
--field-edge: var(--md-sys-color-primary);
|
||||
--field-ink: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.field-box:focus-within .field-outline {
|
||||
[data-md-field-box]:focus-within [data-md-field-outline] {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.field[data-invalid] {
|
||||
[data-md-field][data-md-invalid] {
|
||||
--field-edge: var(--md-sys-color-error);
|
||||
--field-ink: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
.field[data-invalid] .field-box:hover {
|
||||
--field-edge: var(--md-sys-color-on-error-container);
|
||||
@media (hover: hover) {
|
||||
:where([data-md-field]:not(:has([data-md-field-control]:disabled)))[data-md-invalid] [data-md-field-box]:hover {
|
||||
--field-edge: var(--md-sys-color-on-error-container);
|
||||
}
|
||||
}
|
||||
|
||||
.field[data-invalid] .field-box:focus-within {
|
||||
[data-md-field][data-md-invalid] [data-md-field-box]:focus-within {
|
||||
--field-edge: var(--md-sys-color-error);
|
||||
--field-ink: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
/* Dashed for a field the caller made read-only — marked on the field, not read off the control,
|
||||
because a date picker makes its own input read-only too. */
|
||||
.field[data-readonly] .field-outline {
|
||||
border-style: dashed;
|
||||
}
|
||||
/* `data-md-readonly` marks a field the caller made read-only — on the field, not read off the
|
||||
control, because a date picker makes its own input read-only too. Nothing here draws it
|
||||
differently: M3 says a read-only field keeps "the same visual style as an editable field, but
|
||||
clearly labeled read-only", and the native `readonly` attribute is what carries that. The hook
|
||||
stays for an application that wants a mark of its own. */
|
||||
|
||||
.field:has(.field-control:disabled) {
|
||||
--field-edge: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent);
|
||||
--field-ink: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||||
[data-md-field]:has([data-md-field-control]:disabled) {
|
||||
--field-edge: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-container-opacity) * 100%), transparent);
|
||||
--field-ink: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
|
||||
.field-box {
|
||||
[data-md-field-box] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.field-control,
|
||||
.field-icon,
|
||||
.field-affix,
|
||||
.field-trailing {
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||||
[data-md-field-control],
|
||||
[data-md-field-icon],
|
||||
[data-md-field-affix],
|
||||
[data-md-field-trailing] {
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.field-support {
|
||||
padding: 0.25rem var(--field-pad) 0;
|
||||
/* The hint or the error, and — when the field has a maximum — M3's character counter at the far
|
||||
end of the same row (specs: "Padding between supporting text and counter | 16dp", which the
|
||||
two paddings between them already make). Supporting text sits 4px under the field. */
|
||||
[data-md-field-support-row] {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
[data-md-field-support] {
|
||||
min-width: 0;
|
||||
padding: var(--md-sys-measurement-space50) var(--field-pad) 0;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-sm);
|
||||
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
||||
}
|
||||
|
||||
.field[data-invalid] .field-support {
|
||||
[data-md-field][data-md-invalid] [data-md-field-support] {
|
||||
color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
[data-md-field-counter] {
|
||||
flex: none;
|
||||
margin-inline-start: auto;
|
||||
padding-block-start: var(--md-sys-measurement-space50);
|
||||
padding-inline-end: var(--field-pad);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-sm);
|
||||
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Past the maximum the count is an error, and says so in the error colour. */
|
||||
[data-md-field-counter][data-md-over] {
|
||||
color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
/* ---- The filled text field ---------------------------------------------------------------- */
|
||||
|
||||
.field[data-variant="filled"] .field-box {
|
||||
[data-md-field][data-md-variant='filled'] [data-md-field-box] {
|
||||
border-radius: var(--md-sys-shape-corner-xs) var(--md-sys-shape-corner-xs) 0 0;
|
||||
background-color: var(--md-sys-color-surface-container-highest);
|
||||
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.field[data-variant="filled"] .field-box:hover {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, var(--md-sys-color-surface-container-highest));
|
||||
[data-md-field][data-md-variant='filled'] [data-md-field-box]:hover {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), var(--md-sys-color-surface-container-highest));
|
||||
}
|
||||
}
|
||||
|
||||
/* The outline becomes the active indicator: one line along the bottom. */
|
||||
.field[data-variant="filled"] .field-outline {
|
||||
[data-md-field][data-md-variant='filled'] [data-md-field-outline] {
|
||||
inset: auto 0 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
@@ -470,51 +430,34 @@
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.field[data-variant="filled"] .field-outline > legend {
|
||||
[data-md-field][data-md-variant='filled'] [data-md-field-outline] > legend {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.field[data-variant="filled"] .field-box:focus-within .field-outline {
|
||||
[data-md-field][data-md-variant='filled'] [data-md-field-box]:focus-within [data-md-field-outline] {
|
||||
border-block-end-width: 2px;
|
||||
}
|
||||
|
||||
/* The label floats inside the box, above the value, and the value sits below it. */
|
||||
.field[data-variant="filled"] .field-label {
|
||||
top: 1rem;
|
||||
[data-md-field][data-md-variant='filled'] [data-md-field-label] {
|
||||
top: var(--md-sys-measurement-space200);
|
||||
inset-inline-start: var(--field-start);
|
||||
max-width: calc(100% - var(--field-start) - var(--field-pad));
|
||||
}
|
||||
|
||||
.field[data-variant="filled"]:has(.field-label) .field-control {
|
||||
padding-block: 1.5rem 0.5rem;
|
||||
[data-md-field][data-md-variant='filled']:has([data-md-field-label]) [data-md-field-control] {
|
||||
padding-block: var(--md-sys-measurement-space300) var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
.field[data-variant="filled"]:has(.field-label) textarea.field-control {
|
||||
--textarea-clear: 1.5rem;
|
||||
--textarea-pad-top: 0rem;
|
||||
--textarea-pad-bottom: calc(0.5rem - var(--field-grip));
|
||||
|
||||
padding-block: var(--textarea-pad-top) var(--textarea-pad-bottom);
|
||||
}
|
||||
|
||||
/* A file picker's button is taller than a line of text: it sits lower, and a little smaller. */
|
||||
.field[data-variant="filled"]:has(.field-label) input[type="file"].field-control {
|
||||
padding-block: 1.375rem 0.375rem;
|
||||
}
|
||||
|
||||
.field[data-variant="filled"] input[type="file"].field-control::file-selector-button {
|
||||
height: 1.75rem;
|
||||
}
|
||||
|
||||
.field[data-variant="filled"]:has(.field-label) .field-affix {
|
||||
[data-md-field][data-md-variant='filled']:has([data-md-field-label]) [data-md-field-affix] {
|
||||
align-self: stretch;
|
||||
padding-block: 1.5rem 0.5rem;
|
||||
padding-block: var(--md-sys-measurement-space300) var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
.field[data-variant="filled"]:has(.field-control:disabled) {
|
||||
--field-edge: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||||
[data-md-field][data-md-variant='filled']:has([data-md-field-control]:disabled) {
|
||||
--field-edge: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
|
||||
.field-box {
|
||||
[data-md-field-box] {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 4%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* <x-file>: the browser's file input in the text field's chrome
|
||||
* (resources/views/components/file.blade.php).
|
||||
*
|
||||
* The chrome is the field's (components/field.css); this file restyles only what the browser
|
||||
* draws. Its "No file chosen" line is transparent, because it can be neither truncated nor wrapped
|
||||
* and the caller shows what was chosen; its `::file-selector-button` is a tonal pill — a 32px
|
||||
* button in secondary-container with label-large, 16px padding, 12px before the line — so it reads
|
||||
* as the thing to press, with M3's hover state layer and disabled opacities (tokens/state.css). In
|
||||
* a filled field the button sits under the floating label, 28px tall.
|
||||
*
|
||||
* The field's root carries `data-md-file`.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './field.css';
|
||||
|
||||
@layer material.components {
|
||||
/* `<x-file>`: the browser's own "No file chosen" line is transparent (the caller shows what was
|
||||
chosen), and its button is a tonal pill — the thing to press. */
|
||||
input[type='file'][data-md-field-control] {
|
||||
align-self: center;
|
||||
color: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type='file'][data-md-field-control]::file-selector-button {
|
||||
height: 32px;
|
||||
margin-inline-end: 12px;
|
||||
padding-inline: var(--md-sys-measurement-space200);
|
||||
border: 0;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: var(--md-sys-color-secondary-container);
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
cursor: pointer;
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
input[type='file'][data-md-field-control]:hover::file-selector-button {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-secondary-container) calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), var(--md-sys-color-secondary-container));
|
||||
}
|
||||
}
|
||||
|
||||
input[type='file'][data-md-field-control]:disabled::file-selector-button {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-container-opacity) * 100%), transparent);
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* A file picker's button is taller than a line of text: it sits lower, and a little smaller. */
|
||||
[data-md-field][data-md-variant='filled']:has([data-md-field-label]) input[type='file'][data-md-field-control] {
|
||||
padding-block: 22px var(--md-sys-measurement-space75);
|
||||
}
|
||||
|
||||
[data-md-field][data-md-variant='filled'] input[type='file'][data-md-field-control]::file-selector-button {
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* <x-form>: its fields in one column, and its actions at the foot
|
||||
* (resources/views/components/form.blade.php).
|
||||
*
|
||||
* The column is a one-track grid whose track is `minmax(0, 1fr)`: a bare grid's implicit column
|
||||
* floors at its content's min-content, so on a phone a wide field would push the form past its
|
||||
* pane. Rows take their content's height. 16px (`space200`) between fields, because a text field
|
||||
* floats its label half above its outline and any less lets the hint under one field run into the
|
||||
* label of the next. A button written among the fields keeps its label's width at the start edge,
|
||||
* where the grid would stretch it across: M3 keeps a button's width "dynamic to fit label" and
|
||||
* says not to "stretch buttons into long flat shapes"
|
||||
* (docs/reference/m3/components-actions-communication-containment.md § Buttons).
|
||||
*
|
||||
* The actions wrap, end-aligned, 8px (`space100`) apart — M3's gap between buttons in a row. A
|
||||
* caller's class on the `actions` slot lands on the row and outranks this layer.
|
||||
*
|
||||
* [data-md-form] the <form>
|
||||
* [data-md-form-actions] the row of actions, under the divider when `separator` asks for one
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './divider.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-form] {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-auto-rows: min-content;
|
||||
gap: var(--md-sys-measurement-space200);
|
||||
}
|
||||
|
||||
[data-md-form] > [data-md-button] {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
[data-md-form-actions] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* <x-group>: a choice of a few options drawn as M3 Expressive's connected button group, the
|
||||
* successor of the segmented button — native radios (checkboxes with `data-md-multiple`) under
|
||||
* segments that share the row, or hug their labels with `data-md-inline`.
|
||||
*
|
||||
* The segments are M3's buttons at the group's `data-md-size` (Button*Tokens: 32/40/56/96/136px
|
||||
* tall, 16/16/24/48/64px either side, 8/8/8/12/16px between icon and label, label-large, label-
|
||||
* large, title-medium, headline-small, headline-large), with the connected shape from
|
||||
* button-group.css: 2px apart, small inner corners, the chosen segment fully round. At `xs` and
|
||||
* `sm` a segment carries M3's 48px target and a 48px minimum width, which M3 asks for by name.
|
||||
*
|
||||
* `data-md-variant` is the toggle button's colours: `tonal` (secondary-container, chosen in
|
||||
* secondary), `filled` (surface-container behind on-surface-variant, chosen in primary) or
|
||||
* `outlined` (an outline-variant edge, chosen in inverse-surface). A checked input picks the
|
||||
* chosen colours (`:has(:checked)`), a disabled one the disabled treatment — on-surface at 10%
|
||||
* behind 38% text. Corners move on the fast spatial spring and colours on the fast effects spring
|
||||
* beside it.
|
||||
*
|
||||
* A segment renders the foundation's `md-state-layer` (foundation/interaction.css) on the label
|
||||
* itself, which reaches the same `:hover`/`:active` as the class expects; below `medium`'s smallest
|
||||
* two sizes it renders `md-touch-target` too. The real control is the `<input>` inside, not the
|
||||
* label, so keyboard focus and the disabled state are read off it with `:has()` — the one
|
||||
* difference the class cannot draw, kept here as a refinement.
|
||||
*
|
||||
* The legend is label-large and the hint body-small, both in on-surface-variant; a validation
|
||||
* message takes the hint's place in error. `hint-class` lands on the hint, where a caller's class
|
||||
* outranks the colour here.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
@import './button-group.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-group] {
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
[data-md-group-legend] {
|
||||
margin-block-end: var(--md-sys-measurement-space100);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-group] > [data-md-button-group] {
|
||||
display: flex;
|
||||
align-items: normal;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
[data-md-group][data-md-inline] > [data-md-button-group] {
|
||||
inline-size: fit-content;
|
||||
}
|
||||
|
||||
[data-md-group-segment] {
|
||||
--md-group-container: var(--md-sys-color-secondary-container);
|
||||
--md-group-label: var(--md-sys-color-on-secondary-container);
|
||||
|
||||
display: flex;
|
||||
flex: 1 1 0%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-inline-size: 0;
|
||||
background-color: var(--md-group-container);
|
||||
color: var(--md-group-label);
|
||||
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);
|
||||
|
||||
/* Focus lands on the input inside, not the label, so it is read with :has(); the state
|
||||
layer's own opacity joins the ring, as the foundation class draws for a focused control. */
|
||||
&:has(:focus-visible) {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
|
||||
&::before {
|
||||
opacity: var(--md-sys-state-focus-state-layer-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
/* Disabled is read off the input too: the class's own :disabled/aria-disabled rule never
|
||||
fires on the label it never reaches. */
|
||||
&:has(:disabled)::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 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] {
|
||||
flex: initial;
|
||||
}
|
||||
|
||||
[data-md-group]:is([data-md-size='xs'], [data-md-size='sm']) [data-md-group-segment] {
|
||||
min-inline-size: var(--md-sys-measurement-space600);
|
||||
}
|
||||
|
||||
[data-md-group][data-md-size='xs'] [data-md-group-segment] {
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
block-size: 32px;
|
||||
padding-inline: var(--md-sys-measurement-space200);
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-group][data-md-size='sm'] [data-md-group-segment] {
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
block-size: 40px;
|
||||
padding-inline: var(--md-sys-measurement-space200);
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-group][data-md-size='md'] [data-md-group-segment] {
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
block-size: 56px;
|
||||
padding-inline: var(--md-sys-measurement-space300);
|
||||
font: var(--md-sys-typescale-title-md);
|
||||
letter-spacing: var(--md-sys-typescale-title-md-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-group][data-md-size='lg'] [data-md-group-segment] {
|
||||
gap: 12px;
|
||||
block-size: 96px;
|
||||
padding-inline: var(--md-sys-measurement-space600);
|
||||
font: var(--md-sys-typescale-headline-sm);
|
||||
letter-spacing: var(--md-sys-typescale-headline-sm-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-group][data-md-size='xl'] [data-md-group-segment] {
|
||||
gap: var(--md-sys-measurement-space200);
|
||||
block-size: 136px;
|
||||
padding-inline: var(--md-sys-measurement-space800);
|
||||
font: var(--md-sys-typescale-headline-lg);
|
||||
letter-spacing: var(--md-sys-typescale-headline-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-group-segment]:has(:checked) {
|
||||
--md-group-container: var(--md-sys-color-secondary);
|
||||
--md-group-label: var(--md-sys-color-on-secondary);
|
||||
}
|
||||
|
||||
[data-md-group][data-md-variant='filled'] [data-md-group-segment] {
|
||||
--md-group-container: var(--md-sys-color-surface-container);
|
||||
--md-group-label: var(--md-sys-color-on-surface-variant);
|
||||
|
||||
&:has(:checked) {
|
||||
--md-group-container: var(--md-sys-color-primary);
|
||||
--md-group-label: var(--md-sys-color-on-primary);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-group][data-md-variant='outlined'] [data-md-group-segment] {
|
||||
--md-group-container: transparent;
|
||||
--md-group-label: var(--md-sys-color-on-surface-variant);
|
||||
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
|
||||
&:has(:checked) {
|
||||
--md-group-container: var(--md-sys-color-inverse-surface);
|
||||
--md-group-label: var(--md-sys-color-inverse-on-surface);
|
||||
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-group-segment]:has(:disabled) {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
[data-md-group-label] {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-md-group-hint],
|
||||
[data-md-group-error] {
|
||||
margin-block-start: var(--md-sys-measurement-space50);
|
||||
font: var(--md-sys-typescale-body-sm);
|
||||
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-group-hint] {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-md-group-error] {
|
||||
color: var(--md-sys-color-error);
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
* Button groups and split buttons: shapes that depend on a button's place among its siblings.
|
||||
*
|
||||
* Unlayered on purpose. Each <x-button> draws its own corners and padding as utilities, and a
|
||||
* rule in any @layer loses to a utility whatever its specificity; the group has to win.
|
||||
*
|
||||
* Standard group (`<x-button-group>`): pressing a label button widens it and narrows its
|
||||
* neighbours by the same amount, so the row keeps its length — Expressive's press expansion
|
||||
* (ButtonGroupDefaults.ExpandedRatio is 15%; this uses a fixed step per size). Icon buttons keep
|
||||
* their width.
|
||||
*
|
||||
* Connected group (`<x-button-group connected>`, `<x-group>`): 2px apart, the outer corners
|
||||
* full, the inner corners small, smaller still while pressed, and a selected segment fully round
|
||||
* (ConnectedButtonGroupSmallTokens and the M3 Expressive connected button group spec).
|
||||
*
|
||||
* Split button (`<x-split-button>`): the same idea for two halves; the trailing half turns
|
||||
* round and its chevron turns over while its menu is open (SplitButton*Tokens).
|
||||
*/
|
||||
|
||||
[data-button-group] {
|
||||
--group-inner: var(--md-sys-shape-corner-sm);
|
||||
--group-inner-pressed: var(--md-sys-shape-corner-xs);
|
||||
}
|
||||
|
||||
[data-button-group][data-size='xs'] { --group-pad: 0.75rem; --group-grow: 4px; --group-inner: var(--md-sys-shape-corner-xs); --group-inner-pressed: 2px; }
|
||||
[data-button-group][data-size='sm'] { --group-pad: 1rem; --group-grow: 6px; }
|
||||
[data-button-group][data-size='md'] { --group-pad: 1.5rem; --group-grow: 8px; }
|
||||
[data-button-group][data-size='lg'] { --group-pad: 3rem; --group-grow: 16px; --group-inner: var(--md-sys-shape-corner-lg); --group-inner-pressed: var(--md-sys-shape-corner-md); }
|
||||
[data-button-group][data-size='xl'] { --group-pad: 4rem; --group-grow: 20px; --group-inner: var(--md-sys-shape-corner-lg-increased); --group-inner-pressed: var(--md-sys-shape-corner-lg); }
|
||||
|
||||
[data-button-group='standard'] > :not([data-icon-button]):active:not(:disabled, [aria-disabled='true']) {
|
||||
padding-inline: calc(var(--group-pad) + var(--group-grow));
|
||||
}
|
||||
|
||||
[data-button-group='standard'] > :not([data-icon-button]):has(+ :not([data-icon-button]):active:not(:disabled, [aria-disabled='true'])) {
|
||||
padding-inline-end: calc(var(--group-pad) - var(--group-grow));
|
||||
}
|
||||
|
||||
[data-button-group='standard'] > :not([data-icon-button]):active:not(:disabled, [aria-disabled='true']) + :not([data-icon-button]) {
|
||||
padding-inline-start: calc(var(--group-pad) - var(--group-grow));
|
||||
}
|
||||
|
||||
/*
|
||||
* Connected segments and split halves take their inner corner from `--group-corner`, so pressing
|
||||
* or selecting changes one variable and the rounded outer corners stay put.
|
||||
*/
|
||||
[data-button-group='connected'] > *,
|
||||
[data-split] {
|
||||
--group-corner: var(--group-inner);
|
||||
border-start-start-radius: var(--group-corner);
|
||||
border-end-start-radius: var(--group-corner);
|
||||
border-start-end-radius: var(--group-corner);
|
||||
border-end-end-radius: var(--group-corner);
|
||||
}
|
||||
|
||||
[data-button-group='connected'] > :active,
|
||||
[data-split]:active {
|
||||
--group-corner: var(--group-inner-pressed);
|
||||
}
|
||||
|
||||
[data-button-group='connected'] > :is([aria-pressed='true'], :has(:checked)),
|
||||
[data-split='trailing'][aria-expanded='true'] {
|
||||
--group-corner: var(--md-sys-shape-corner-full);
|
||||
}
|
||||
|
||||
[data-button-group='connected'] > :first-child,
|
||||
[data-split='leading'] {
|
||||
border-start-start-radius: var(--md-sys-shape-corner-full);
|
||||
border-end-start-radius: var(--md-sys-shape-corner-full);
|
||||
}
|
||||
|
||||
[data-button-group='connected'] > :last-child,
|
||||
[data-split='trailing'] {
|
||||
border-start-end-radius: var(--md-sys-shape-corner-full);
|
||||
border-end-end-radius: var(--md-sys-shape-corner-full);
|
||||
}
|
||||
|
||||
[data-split='trailing'] svg {
|
||||
transition: rotate var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast);
|
||||
}
|
||||
|
||||
[data-split='trailing'][aria-expanded='true'] svg {
|
||||
rotate: 180deg;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* <x-icon>: a Material Symbol at the size it is drawn.
|
||||
*
|
||||
* 24px unless `size` sets `--md-icon-size` (resources/views/components/icon.blade.php), which is
|
||||
* M3's default icon size (docs/reference/m3/styles.md § Icons). The symbol never shrinks in a flex
|
||||
* row, so a long label beside it cannot squeeze it. `data-md-mirror-rtl` flips a directional
|
||||
* symbol in a right-to-left document (docs/reference/m3/foundations.md § Layout → bidirectionality).
|
||||
*/
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* <x-input>: M3's one-line text field (resources/views/components/input.blade.php).
|
||||
*
|
||||
* Nothing of its own to draw: the outlined and filled chrome, the floating label, the prefix and
|
||||
* suffix, the clear and copy buttons and the character counter are the field's
|
||||
* (components/field.css), and its icons are <x-icon>s. The field's root carries `data-md-input`
|
||||
* for an application that wants to tell a one-line field from the others.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './field.css';
|
||||
@import './icon.css';
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
* <x-list-item>: a headline with what leads it, what supports it and what trails it, one- two- or
|
||||
* three-line by `data-md-lines` (56/72/88px, `ListTokens.kt:180,323,347`, androidx Compose
|
||||
* 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).
|
||||
*
|
||||
* `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
|
||||
* the hover 8% and focus/press 10% state tokens — and a 3px secondary inset ring; excluded on
|
||||
* `[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.
|
||||
* `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. `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.
|
||||
*
|
||||
* `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),
|
||||
* 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.
|
||||
*
|
||||
* `data-md-dividers` on the list insets its rule 16px from both ends (`DividerLeadingSpace`/
|
||||
* `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
|
||||
* thumbnail; `data-md-list-item-video` is M3's leading video in its two sizes,
|
||||
* `data-md-size="sm"` 100×56px (a two-line item) or `"lg"` 114×64px (a three-line item),
|
||||
* `LeadingVideoSmall`/`LeadingVideoLarge`.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-list-item] {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--md-sys-measurement-space200);
|
||||
padding-inline: var(--md-sys-measurement-space200);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
[data-md-list-item][data-md-lines='1'] {
|
||||
min-height: 56px;
|
||||
padding-block: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-md-list-item][data-md-lines='2'] {
|
||||
min-height: 72px;
|
||||
padding-block: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-md-list-item][data-md-lines='3'] {
|
||||
min-height: 88px;
|
||||
padding-block: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
/* A row's fill, under its state layer. Declared on every row at no weight, so a row nested in a
|
||||
filled one starts from nothing rather than inheriting its parent's fill. */
|
||||
:where([data-md-list-row]) {
|
||||
--md-list-row-fill: transparent;
|
||||
}
|
||||
|
||||
/* M3 Expressive's segmented list gives each item its own container: ListTokens.kt:201,
|
||||
`ItemSegmentedContainerColor get() = ColorSchemeKeyTokens.Surface`. One tone up from that,
|
||||
because the token reads against a Compose scaffold and this package paints the page
|
||||
`surface` itself (foundation/base.css): a surface tile on a surface page is a list nobody
|
||||
can see. `surface-container` is the step M3 names for a container that has to read against
|
||||
the page, and what 1.x drew. Before the selected rule, which outranks it on the same
|
||||
weight. */
|
||||
[data-md-list='segmented'] > [data-md-list-item] {
|
||||
--md-list-row-fill: var(--md-sys-color-surface-container);
|
||||
|
||||
background-color: var(--md-list-row-fill);
|
||||
}
|
||||
|
||||
[data-md-list-item][data-md-selected] {
|
||||
--md-list-row-fill: var(--md-sys-color-secondary-container);
|
||||
|
||||
background-color: var(--md-list-row-fill);
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
/* A row written by hand — list-rows.js's contract, not an `<x-list-item>` — selected the same
|
||||
way. The state layers below are mixed over this fill, as they are over an item's. */
|
||||
[data-md-list-row][data-md-selected]:not([data-md-card], [data-md-list-item]) {
|
||||
--md-list-row-fill: var(--md-sys-color-secondary-container);
|
||||
|
||||
background-color: var(--md-list-row-fill);
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
[data-md-list-item][aria-disabled='true'] {
|
||||
pointer-events: none;
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
}
|
||||
|
||||
[data-md-list-item]:not([data-md-selected], [aria-disabled='true']) :is([data-md-list-item-overline], [data-md-list-item-description], [data-md-list-item-trailing], [data-md-list-item-icon]) {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-md-list-item-leading],
|
||||
[data-md-list-item-end] {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
[data-md-list-item-end] {
|
||||
gap: var(--md-sys-measurement-space50);
|
||||
}
|
||||
|
||||
[data-md-list-item-video] {
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
border-radius: var(--md-sys-shape-corner-sm);
|
||||
background-color: var(--md-sys-color-surface-container-highest);
|
||||
|
||||
& > * {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-list-item-video][data-md-size='sm'] {
|
||||
width: 100px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
[data-md-list-item-video][data-md-size='lg'] {
|
||||
width: 114px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
[data-md-list-item-avatar] {
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
span[data-md-list-item-avatar] {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
font: var(--md-sys-typescale-title-md);
|
||||
letter-spacing: var(--md-sys-typescale-title-md-tracking);
|
||||
}
|
||||
|
||||
[data-md-list-item-image] {
|
||||
flex-shrink: 0;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: var(--md-sys-shape-corner-sm);
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
[data-md-list-item-content] {
|
||||
min-width: 0;
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
[data-md-list-item-overline] {
|
||||
font: var(--md-sys-typescale-label-sm);
|
||||
letter-spacing: var(--md-sys-typescale-label-sm-tracking);
|
||||
}
|
||||
|
||||
[data-md-list-item-title] {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
outline: none;
|
||||
font: var(--md-sys-typescale-body-lg);
|
||||
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-md-list-item-description] {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
font: var(--md-sys-typescale-body-md);
|
||||
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
||||
}
|
||||
|
||||
[data-md-list-item-trailing] {
|
||||
flex-shrink: 0;
|
||||
font: var(--md-sys-typescale-label-sm);
|
||||
letter-spacing: var(--md-sys-typescale-label-sm-tracking);
|
||||
}
|
||||
|
||||
[data-md-list][data-md-dividers] > [data-md-list-item]:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline: var(--md-sys-measurement-space200);
|
||||
bottom: 0;
|
||||
height: 1px;
|
||||
background-color: var(--md-sys-color-outline-variant);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-md-list-row] {
|
||||
cursor: pointer;
|
||||
transition-property: background-color, border-radius;
|
||||
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);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-md-list-row]:not([data-md-card]):hover:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-md-list-open]):hover)) {
|
||||
background-color: color-mix(in srgb, currentColor calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), var(--md-list-row-fill, transparent));
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-list-row]:not([data-md-card]):has([data-md-list-open]:focus-visible) {
|
||||
background-color: color-mix(in srgb, currentColor calc(var(--md-sys-state-focus-state-layer-opacity) * 100%), var(--md-list-row-fill, transparent));
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
[data-md-list-row]:not([data-md-card]):active:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-md-list-open]):active)) {
|
||||
background-color: color-mix(in srgb, currentColor calc(var(--md-sys-state-pressed-state-layer-opacity) * 100%), var(--md-list-row-fill, transparent));
|
||||
}
|
||||
|
||||
[data-md-list-row] [data-md-list-open]:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-md-list='segmented'] > [data-md-list-item] {
|
||||
border-radius: var(--md-sys-shape-corner-xs);
|
||||
}
|
||||
|
||||
[data-md-list='segmented'] > [data-md-list-item]:first-child {
|
||||
border-start-start-radius: var(--md-sys-shape-corner-lg);
|
||||
border-start-end-radius: var(--md-sys-shape-corner-lg);
|
||||
}
|
||||
|
||||
[data-md-list='segmented'] > [data-md-list-item]:last-child {
|
||||
border-end-start-radius: var(--md-sys-shape-corner-lg);
|
||||
border-end-end-radius: var(--md-sys-shape-corner-lg);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-md-list='segmented'] > [data-md-list-item][data-md-list-row]:hover {
|
||||
border-radius: var(--md-sys-shape-corner-md);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-list='segmented'] > [data-md-list-item]:is([data-md-selected], [data-md-list-row]:active, [data-md-list-row]:has([data-md-list-open]:focus-visible)) {
|
||||
border-radius: var(--md-sys-shape-corner-lg);
|
||||
}
|
||||
}
|
||||
@@ -1,101 +1,27 @@
|
||||
/*
|
||||
* Rows a person can go into (`data-list-row`, resources/js/list-rows.js), and M3 Expressive lists.
|
||||
* <x-list>: `<x-list-item>`s, one under another.
|
||||
*
|
||||
* A row answers a pointer with M3's state layer — hover 8%, press and focus 10% — but not while the
|
||||
* pointer is on one of its own controls, which light only themselves. Hover only under
|
||||
* `(hover: hover)`, because a touch screen keeps the last hover after a tap. The opener draws no
|
||||
* focus ring inside a row; the row draws it, inset, so keyboard focus shows which row Enter opens.
|
||||
* `data-selected` is M3's selected list item, in secondary-container.
|
||||
* Plain by default; `data-md-list="segmented"` is M3 Expressive's list, each item its own surface
|
||||
* tile `space25` (2px) apart (`SegmentedGap`, `ListTokens.kt`, androidx Compose Material 3,
|
||||
* Apache-2.0) — the item's own background, corner and interaction states are list-item.css's,
|
||||
* since they are the item's to draw, not the list's.
|
||||
*
|
||||
* Unlayered where a component paints its fill as a utility (`<x-card>`): anything in a @layer loses
|
||||
* to a utility whatever its specificity.
|
||||
* `selectable`/`selection` switch the container's role between `list` and `listbox`
|
||||
* (list.blade.php; docs/reference/m3/components-actions-communication-containment.md
|
||||
* § Lists → Accessibility) — a role takes no CSS of its own.
|
||||
*/
|
||||
|
||||
@layer components {
|
||||
:where([data-list-row]) {
|
||||
cursor: pointer;
|
||||
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast),
|
||||
border-radius var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast);
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './list-item.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-list] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
:where([data-list-row]:not([data-card]):hover:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-list-open]):hover))) {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
:where([data-list-row]:not([data-card]):has([data-list-open]:focus-visible)) {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
:where([data-list-row]:not([data-card]):active:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-list-open]):active))) {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
|
||||
[data-md-list='segmented'] {
|
||||
gap: var(--md-sys-measurement-space25);
|
||||
}
|
||||
}
|
||||
|
||||
[data-list-row] [data-list-open]:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-list='segmented'] > [data-list-item] {
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
}
|
||||
|
||||
:is([data-list-row], [data-list-item])[data-selected]:not([data-card]) {
|
||||
background-color: var(--md-sys-color-secondary-container);
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
/* A card that opens answers with its container one tone up and its corner opening a step. */
|
||||
[data-card][data-list-row] {
|
||||
transition-property: border-radius, background-color, box-shadow;
|
||||
transition-duration: var(--md-sys-motion-spatial-default-duration);
|
||||
transition-timing-function: var(--md-sys-motion-spatial-default);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-card][data-list-row]:hover:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-list-open]):hover)) {
|
||||
border-radius: var(--md-sys-shape-corner-lg);
|
||||
box-shadow: inset 0 0 0 100vmax color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
[data-card][data-list-row]:active:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-list-open]):active)) {
|
||||
box-shadow: inset 0 0 0 100vmax color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
|
||||
}
|
||||
|
||||
[data-card][data-list-row]:has([data-list-open]:focus-visible) {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/*
|
||||
* M3 Expressive's segmented list (`<x-list segmented>`): each item its own surface, 2px apart,
|
||||
* extra-small corners that open to large at the ends, and to large while hovered, pressed or
|
||||
* selected (ListTokens, androidx Compose Material 3, Apache-2.0).
|
||||
*/
|
||||
[data-list='segmented'] > [data-list-item] {
|
||||
border-radius: var(--md-sys-shape-corner-xs);
|
||||
}
|
||||
|
||||
[data-list='segmented'] > [data-list-item]:first-child {
|
||||
border-start-start-radius: var(--md-sys-shape-corner-lg);
|
||||
border-start-end-radius: var(--md-sys-shape-corner-lg);
|
||||
}
|
||||
|
||||
[data-list='segmented'] > [data-list-item]:last-child {
|
||||
border-end-start-radius: var(--md-sys-shape-corner-lg);
|
||||
border-end-end-radius: var(--md-sys-shape-corner-lg);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-list='segmented'] > [data-list-item][data-list-row]:hover {
|
||||
border-radius: var(--md-sys-shape-corner-md);
|
||||
}
|
||||
}
|
||||
|
||||
[data-list='segmented'] > [data-list-item]:is([data-selected], [data-list-row]:active) {
|
||||
border-radius: var(--md-sys-shape-corner-lg);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* <x-loading>: M3 Expressive's loading indicator, a shape that morphs through seven Expressive
|
||||
* shapes while it turns, for a wait with no known length.
|
||||
*
|
||||
* The drawing is resources/svg/loading-indicator/, ported from androidx Compose Material 3's
|
||||
* LoadingIndicator in bin/loading-indicator.mjs (Apache-2.0): LoadingIndicatorTokens' 38px
|
||||
* indicator in a 48px container, which is the size here unless `size` (`--md-loading-size`, 24 to
|
||||
* 240px, M3's responsive range) or the caller's CSS sizes it; the SVG scales with its box, so the
|
||||
* container and the shape keep their ratio at every size. It is
|
||||
* drawn in the text colour, `primary` (ActiveIndicatorColor) unless the caller colours it;
|
||||
* `contained` puts it on a `primary-container` circle in `on-primary-container`
|
||||
* (ContainedContainerColor, ContainedIndicatorColor), for a spinner over content.
|
||||
*
|
||||
* The animated drawing is SMIL, so it runs without script. Under `prefers-reduced-motion` the
|
||||
* still drawing of the same shape takes its place: the view renders both, and this file shows one.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* <x-menu-group>: a labelled or gapped cluster of items in an `<x-menu>`
|
||||
* (menu-group.blade.php).
|
||||
*
|
||||
* Without `gap`, a plain 4px of vertical padding around the cluster, 0 at either end of the list
|
||||
* (the popover's own padding already gives that room). `data-md-gap` draws M3 Expressive's
|
||||
* "Grouped" layout instead: items 2px apart (`SegmentedMenuTokens.SegmentedGap`, androidx Compose
|
||||
* Material 3, Apache-2.0), clusters themselves 8px apart — the same 8px `<x-menu-separator>`
|
||||
* keeps above and below its line, so a menu is the same height whichever it uses
|
||||
* (docs/reference/m3/components-actions-communication-containment.md § Menus → Specification).
|
||||
*
|
||||
* The label is label-large in on-surface-variant, `aria-hidden` beside the `role="group"`
|
||||
* `aria-label` a screen reader reads instead (`SegmentedMenuTokens.GroupLabelTextFont`).
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@layer material.components {
|
||||
[data-md-menu-group]:not([data-md-gap]) {
|
||||
padding-block: var(--md-sys-measurement-space50);
|
||||
}
|
||||
|
||||
[data-md-menu-group]:not([data-md-gap]):first-child {
|
||||
padding-block-start: 0;
|
||||
}
|
||||
|
||||
[data-md-menu-group]:not([data-md-gap]):last-child {
|
||||
padding-block-end: 0;
|
||||
}
|
||||
|
||||
[data-md-menu-group][data-md-gap]:not(:first-child) {
|
||||
margin-block-start: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-md-menu-group-label] {
|
||||
padding-inline: var(--md-sys-measurement-space200);
|
||||
padding-block: var(--md-sys-measurement-space100) var(--md-sys-measurement-space50);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-menu-group-items] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--md-sys-measurement-space25);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* <x-menu-item>: one row in an `<x-menu>` — an action, a link, or a choice — and, with `submenu`,
|
||||
* 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`), 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,
|
||||
* because M3 keeps it reachable, but the class already withholds the layer from `aria-disabled`.
|
||||
*
|
||||
* `submenu` turns the row into the WAI-ARIA menu button for a second `role="menu"` popover beside
|
||||
* it, anchored to its end and flipping to the start where the window has no room
|
||||
* (resources/js/menu.js). The popover shares its container colour with the menu it opens from
|
||||
* through `--material-menu-surface`/`--material-menu-ink` (menu.css defines them on
|
||||
* `[data-md-menu-popover]`; the fallbacks here are the standard menu's, for a submenu inside some
|
||||
* other list, e.g. a FAB menu's). Inside an `<x-menu sheet-at-compact>` sheet the same markup
|
||||
* opens in place instead — menu.css restyles `[data-md-submenu]` and `[data-md-submenu-chevron]`
|
||||
* there.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
@import './badge.css';
|
||||
@import './menu.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-menu-item] {
|
||||
--md-menu-item-ink: var(--md-sys-color-on-surface-variant);
|
||||
|
||||
display: flex;
|
||||
inline-size: 100%;
|
||||
min-block-size: var(--md-sys-measurement-space600);
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-inline: var(--md-sys-measurement-space200);
|
||||
border-radius: var(--md-sys-shape-corner-xs);
|
||||
background-color: transparent;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
transition-property: border-radius, background-color, color;
|
||||
transition-duration:
|
||||
var(--md-sys-motion-spatial-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-effects-fast), var(--md-sys-motion-effects-fast);
|
||||
|
||||
/* The row sits edge to edge in the list, so the ring reads inward, not past the item. */
|
||||
&:focus-visible {
|
||||
outline-offset: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-menu-item]:first-child {
|
||||
border-start-start-radius: var(--md-sys-shape-corner-md);
|
||||
border-start-end-radius: var(--md-sys-shape-corner-md);
|
||||
}
|
||||
|
||||
[data-md-menu-item]:last-child {
|
||||
border-end-start-radius: var(--md-sys-shape-corner-md);
|
||||
border-end-end-radius: var(--md-sys-shape-corner-md);
|
||||
}
|
||||
|
||||
[data-md-menu-item][data-md-description] {
|
||||
padding-block: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-md-menu-item][aria-checked='true'] {
|
||||
--md-menu-item-ink: var(--md-sys-color-on-tertiary-container);
|
||||
|
||||
border-radius: var(--md-sys-shape-corner-md);
|
||||
background-color: var(--md-sys-color-tertiary-container);
|
||||
color: var(--md-sys-color-on-tertiary-container);
|
||||
}
|
||||
|
||||
[data-md-menu-item][aria-current='page']:not([aria-checked='true']) {
|
||||
--md-menu-item-ink: var(--md-sys-color-on-secondary-container);
|
||||
|
||||
border-radius: var(--md-sys-shape-corner-md);
|
||||
background-color: var(--md-sys-color-secondary-container);
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
[data-md-menu-item][aria-disabled='true'] {
|
||||
--md-menu-item-ink: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
|
||||
pointer-events: none;
|
||||
color: var(--md-menu-item-ink);
|
||||
}
|
||||
|
||||
[data-md-menu-item] [data-md-icon] {
|
||||
color: var(--md-menu-item-ink);
|
||||
}
|
||||
|
||||
[data-md-menu-item][aria-disabled='true'] [data-md-icon] {
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent) !important;
|
||||
}
|
||||
|
||||
[data-md-menu-item-text] {
|
||||
min-inline-size: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
[data-md-menu-item-label] {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font: var(--md-sys-typescale-body-lg);
|
||||
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-menu-item-description] {
|
||||
display: block;
|
||||
color: var(--md-menu-item-ink);
|
||||
font: var(--md-sys-typescale-body-md);
|
||||
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-menu-item-shortcut] {
|
||||
flex-shrink: 0;
|
||||
color: var(--md-menu-item-ink);
|
||||
font: var(--md-sys-typescale-label-sm);
|
||||
letter-spacing: var(--md-sys-typescale-label-sm-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
/* The submenu popover: a menu of its own, sharing the container colour of the list it opens
|
||||
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] {
|
||||
margin: 0 var(--md-sys-measurement-space50);
|
||||
transform-origin: top;
|
||||
position-area: inline-end span-block-end;
|
||||
position-try-fallbacks: flip-inline;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* <x-menu-separator>: a line between groups of items in a menu — 1px in `outline-variant`, with
|
||||
* M3's 8px above and below it, inset to the 16px the items keep either side (M3's menu page:
|
||||
* "dividers are more subtle and are the right choice for scrollable menus"; SegmentedMenuTokens'
|
||||
* divider spacing).
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@layer material.components {
|
||||
[data-md-menu-separator] {
|
||||
block-size: 1px;
|
||||
margin-block: var(--md-sys-measurement-space100);
|
||||
margin-inline: var(--md-sys-measurement-space200);
|
||||
border-width: 0;
|
||||
background-color: var(--md-sys-color-outline-variant);
|
||||
}
|
||||
}
|
||||
+389
-143
@@ -1,189 +1,435 @@
|
||||
/*
|
||||
* The exposed dropdown menu — the list a field drops open.
|
||||
* The menus: `<x-menu>` and what a form's own dropdowns wear too, so a menu and a form read as
|
||||
* one family.
|
||||
*
|
||||
* Two lists wear it, so a form reads as one family:
|
||||
* Three lists wear it:
|
||||
*
|
||||
* - `<x-menu>`'s own popover (`[data-md-menu-popover]`) and, inside it, a submenu
|
||||
* (`[data-md-submenu]`, menu-item.css).
|
||||
* - `<x-select>`'s: the native <select>, opted into the browser's customizable select
|
||||
* (`appearance: base-select`). `::picker(select)` is the menu and each <option> a row, while the
|
||||
* keyboard, type-to-find, the screen reader and the form value stay the browser's own.
|
||||
* - `<x-choices searchable>`'s listbox, which Alpine draws, as `.field-menu` and `.field-option`.
|
||||
* (`appearance: base-select`). `::picker(select)` is the menu and each <option> a row, while
|
||||
* the keyboard, type-to-find, the screen reader and the form value stay the browser's own.
|
||||
* - `<x-choices searchable>`'s listbox, which Alpine draws, as `[data-md-field-menu]` and
|
||||
* `[data-md-field-option]`.
|
||||
*
|
||||
* M3's menu as its tokens have it: surface-container at elevation 2, extra-small corner, 48px rows in
|
||||
* body-large, the chosen row in secondary-container with a tick at its end (so it is not told by colour
|
||||
* alone), and the state layer on hover (only where a pointer can hover) and focus.
|
||||
* M3's menu as its tokens have it (StandardMenuTokens/VibrantMenuTokens/SegmentedMenuTokens,
|
||||
* androidx Compose Material 3, Apache-2.0): surface-container-low at elevation 2, large corner,
|
||||
* 48px rows in body-large, the chosen row in secondary-container with a tick at its end (so it is
|
||||
* not told by colour alone), and the state layer on hover (only where a pointer can hover) and
|
||||
* focus.
|
||||
*
|
||||
* Unlayered, as the rest of the package's component CSS, so a utility on the call site cannot half-undo
|
||||
* a row.
|
||||
* `[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, 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).
|
||||
*
|
||||
* `sheet-at-compact`'s bottom sheet is `<x-bottom-sheet>` (bottom-sheet.css): `[data-md-menu-sheet]`
|
||||
* cancels its 24px padding and restyles a submenu to open in place under its item instead of
|
||||
* beside it (M3 calls submenus "best suited to large screens").
|
||||
*
|
||||
* Where the browser has no customizable select, the select keeps its native list, which
|
||||
* `color-scheme` themes. Every rule for the select's own rows is inside `@supports`: a browser that
|
||||
* paints option colours into its native list would otherwise show a half-painted one.
|
||||
* `color-scheme` themes. Every rule for the select's own rows is inside `@supports`: a browser
|
||||
* that paints option colours into its native list would otherwise show a half-painted one.
|
||||
*/
|
||||
|
||||
.field-menu {
|
||||
max-block-size: 18rem;
|
||||
overflow-y: auto;
|
||||
padding-block: 0.5rem;
|
||||
border-radius: var(--md-sys-shape-corner-xs);
|
||||
background: var(--md-sys-color-surface-container);
|
||||
box-shadow: var(--md-sys-elevation-2);
|
||||
}
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
/* As a popover (`<x-choices searchable>`), it hangs under its field, as wide, and goes above only
|
||||
when there is no room below. */
|
||||
.field-menu[popover] {
|
||||
inset: auto;
|
||||
top: anchor(bottom);
|
||||
left: anchor(left);
|
||||
width: anchor-size(width);
|
||||
margin: 0.25rem 0;
|
||||
border: 0;
|
||||
padding-inline: 0;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
position-try-fallbacks: flip-block;
|
||||
}
|
||||
@import './icon.css';
|
||||
@import './bottom-sheet.css';
|
||||
|
||||
.field-option[aria-disabled="true"] {
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.field-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
min-block-size: 3rem;
|
||||
padding-inline: 1rem;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
font: var(--md-sys-typescale-body-lg);
|
||||
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* `data-active` is the row the arrow keys or the pointer are on. */
|
||||
.field-option[data-active] {
|
||||
background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
|
||||
}
|
||||
|
||||
.field-option[aria-selected="true"] {
|
||||
background: var(--md-sys-color-secondary-container);
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
.field-option[aria-selected="true"][data-active] {
|
||||
background: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 8%, var(--md-sys-color-secondary-container));
|
||||
}
|
||||
|
||||
.field-check {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.field-option:not([aria-selected="true"]) .field-check {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
@supports (appearance: base-select) {
|
||||
select.field-control,
|
||||
select.field-control::picker(select) {
|
||||
appearance: base-select;
|
||||
@layer material.components {
|
||||
[data-md-menu] {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/* The menu. The browser anchors it to the select; the select covers its whole
|
||||
field (field.css), so the menu is at least the field's width. It hangs under
|
||||
the field and goes above only when it does not fit there — the browser's own
|
||||
order tries the roomier side first, which opened a menu upwards with room to
|
||||
spare under it. It opens with a fade and a grow on the spatial spring and
|
||||
closes with a fade, as `<x-menu>` does. */
|
||||
select.field-control::picker(select) {
|
||||
position-try-order: normal;
|
||||
max-block-size: 18rem;
|
||||
max-inline-size: calc(100vw - 2rem);
|
||||
margin-block: 0.25rem;
|
||||
padding-block: 0.5rem;
|
||||
border: 0;
|
||||
border-radius: var(--md-sys-shape-corner-xs);
|
||||
background: var(--md-sys-color-surface-container);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
[data-md-menu-trigger] {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
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);
|
||||
opacity: 0;
|
||||
transform-origin: top;
|
||||
transition-property: opacity, display, overlay;
|
||||
transition-duration: var(--md-sys-motion-effects-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-effects-fast);
|
||||
transition-behavior: allow-discrete;
|
||||
}
|
||||
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);
|
||||
|
||||
select.field-control:open::picker(select) {
|
||||
opacity: 1;
|
||||
transition-property: opacity, scale, display, overlay;
|
||||
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
||||
}
|
||||
&:popover-open:not([data-md-popover-closing]) {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
select.field-control:open::picker(select) {
|
||||
opacity: 0;
|
||||
scale: 0.95;
|
||||
@starting-style {
|
||||
&:popover-open {
|
||||
opacity: 0;
|
||||
scale: 0.95;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
select.field-control option {
|
||||
[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;
|
||||
}
|
||||
|
||||
[data-md-menu-popover][data-md-vibrant] {
|
||||
--material-menu-surface: var(--md-sys-color-tertiary-container);
|
||||
--material-menu-ink: var(--md-sys-color-on-tertiary-container);
|
||||
}
|
||||
|
||||
[data-md-menu-popover][data-md-position='bottom-start'] {
|
||||
transform-origin: top;
|
||||
position-area: bottom span-right;
|
||||
}
|
||||
|
||||
[data-md-menu-popover][data-md-position='bottom-end'] {
|
||||
transform-origin: top;
|
||||
position-area: bottom span-left;
|
||||
}
|
||||
|
||||
[data-md-menu-popover][data-md-position='top-start'] {
|
||||
transform-origin: bottom;
|
||||
position-area: top span-right;
|
||||
}
|
||||
|
||||
[data-md-menu-popover][data-md-position='top-end'] {
|
||||
transform-origin: bottom;
|
||||
position-area: top span-left;
|
||||
}
|
||||
|
||||
/*
|
||||
* `<x-menu filter>`: M3's menu as a filtering surface. The field stays put while the list
|
||||
* scrolls under it and takes the menu's own container, so a vibrant menu's field is vibrant
|
||||
* too. It is a plain <input> with no field chrome — M3's menus embed a text field, not a text
|
||||
* field component.
|
||||
*/
|
||||
[data-md-menu-filter] {
|
||||
position: sticky;
|
||||
inset-block-start: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
min-block-size: 3rem;
|
||||
padding-block: 0;
|
||||
padding-inline: 1rem;
|
||||
background: transparent;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
gap: 12px;
|
||||
min-block-size: var(--md-sys-measurement-space600);
|
||||
padding-inline: var(--md-sys-measurement-space200);
|
||||
border-block-end: 1px solid var(--md-sys-color-outline-variant);
|
||||
background-color: var(--material-menu-surface, var(--md-sys-color-surface-container-low));
|
||||
}
|
||||
|
||||
[data-md-menu-filter] input {
|
||||
flex: 1;
|
||||
min-inline-size: 0;
|
||||
border-width: 0;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
font: var(--md-sys-typescale-body-lg);
|
||||
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
select.field-control option:hover {
|
||||
background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
|
||||
[data-md-menu-filter] input::placeholder {
|
||||
color: color-mix(in srgb, var(--material-menu-ink, var(--md-sys-color-on-surface)) 70%, transparent);
|
||||
}
|
||||
|
||||
[data-md-menu-list] {
|
||||
padding: var(--md-sys-measurement-space50);
|
||||
}
|
||||
|
||||
[data-md-menu-empty] {
|
||||
padding-inline: var(--md-sys-measurement-space200);
|
||||
padding-block: 12px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-md);
|
||||
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
/* The filter hides what the query leaves out with the `hidden` attribute; a row's own
|
||||
`display: flex` would otherwise beat the user agent's `[hidden] { display: none }`. */
|
||||
:is([data-md-menu-popover], [data-md-menu-sheet]) [hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* `data-md-active` is the row the arrow keys are on while the focus stays in the field — the
|
||||
state layer the roving focus would have drawn, without taking the focus off the field. A
|
||||
chosen or current row keeps its own colour. */
|
||||
:is([data-md-menu-popover], [data-md-menu-sheet]) [role^="menuitem"][data-md-active]:not([aria-checked="true"], [aria-current="page"]) {
|
||||
background-color: color-mix(in srgb, var(--material-menu-ink, var(--md-sys-color-on-surface)) 8%, transparent);
|
||||
}
|
||||
|
||||
/*
|
||||
* `<x-menu sheet-at-compact>`: the copy of the list in the bottom sheet a compact window opens
|
||||
* instead of the popover. The sheet is surface-container-low, as the standard menu is, so the
|
||||
* fallbacks above already paint its field; the field reaches both edges of the sheet and holds
|
||||
* its icon where the rows hold theirs, 24px in (the list is 8px in, a row's own padding 16px).
|
||||
* `-24px` cancels the bottom sheet's own padding.
|
||||
*/
|
||||
[data-md-menu-sheet] {
|
||||
margin-inline: calc(-1 * var(--md-sys-measurement-space300));
|
||||
padding-inline: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-md-menu-sheet]:has(> [data-md-menu-filter]) {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
[data-md-menu-sheet] [data-md-menu-filter] {
|
||||
padding-inline: var(--md-sys-measurement-space300);
|
||||
}
|
||||
|
||||
[data-md-menu-sheet] [data-md-menu-list] {
|
||||
padding-inline: var(--md-sys-measurement-space100);
|
||||
padding-block: var(--md-sys-measurement-space50) 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* A submenu there opens in place: its list stands under the item, inset by 16px, instead of in
|
||||
* a popover beside it, which a sheet has no room for (M3 calls submenus "best suited to large
|
||||
* screens", docs/reference/m3/components-actions-communication-containment.md § Menus). The
|
||||
* markup is the popover's, never shown as one: the user agent's `[popover]` box is undone, the
|
||||
* list is shown while its item says `aria-expanded="true"`, and it fades in on the effects
|
||||
* track.
|
||||
*/
|
||||
[data-md-menu-sheet] [data-md-submenu] {
|
||||
position: static;
|
||||
inset: auto;
|
||||
inline-size: auto;
|
||||
max-inline-size: none;
|
||||
block-size: auto;
|
||||
max-block-size: none;
|
||||
margin: 0;
|
||||
padding-block: var(--md-sys-measurement-space25);
|
||||
padding-inline: var(--md-sys-measurement-space200) 0;
|
||||
overflow: visible;
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
box-shadow: none;
|
||||
opacity: 1;
|
||||
scale: none;
|
||||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
[data-md-menu-sheet] [aria-expanded="true"] + [data-md-submenu] {
|
||||
display: block;
|
||||
|
||||
@starting-style {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
select.field-control option:focus-visible {
|
||||
background: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
|
||||
/* The chevron turns from the way a popover would open to the way this list did: down, which
|
||||
is a quarter turn the other way on the chevron `mirror-rtl` mirrors too. */
|
||||
[data-md-menu-sheet] [data-md-submenu-chevron] {
|
||||
transition: rotate var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast);
|
||||
}
|
||||
|
||||
select.field-control option:checked {
|
||||
background: var(--md-sys-color-secondary-container);
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
[data-md-menu-sheet] [aria-expanded="true"] > [data-md-submenu-chevron] {
|
||||
rotate: 90deg;
|
||||
|
||||
@media (hover: hover) {
|
||||
select.field-control option:checked:hover {
|
||||
background: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 8%, var(--md-sys-color-secondary-container));
|
||||
&:is([dir='rtl'], [dir='rtl'] *) {
|
||||
rotate: -90deg;
|
||||
}
|
||||
}
|
||||
|
||||
select.field-control option:checked:focus-visible {
|
||||
background: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 10%, var(--md-sys-color-secondary-container));
|
||||
[data-md-field-menu] {
|
||||
max-block-size: 18rem;
|
||||
overflow-y: auto;
|
||||
padding-block: var(--md-sys-measurement-space100);
|
||||
border-radius: var(--md-sys-shape-corner-xs);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
box-shadow: var(--md-sys-elevation-2);
|
||||
}
|
||||
|
||||
select.field-control option:disabled {
|
||||
background: transparent;
|
||||
/* As a popover (`<x-choices searchable>`), it hangs under its field, as wide, and goes above
|
||||
only when there is no room below. */
|
||||
[data-md-field-menu][popover] {
|
||||
inset: auto;
|
||||
top: anchor(bottom);
|
||||
left: anchor(left);
|
||||
width: anchor-size(width);
|
||||
margin: var(--md-sys-measurement-space50) 0;
|
||||
border-width: 0;
|
||||
padding-inline: 0;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
position-try-fallbacks: flip-block;
|
||||
}
|
||||
|
||||
[data-md-field-option][aria-disabled="true"] {
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* The tick, at the row's end: the browser draws it before the label, and hides
|
||||
it on every row but the chosen one. The Material Symbol, as a mask, so it
|
||||
takes the row's ink. */
|
||||
select.field-control option::checkmark {
|
||||
content: "";
|
||||
order: 1;
|
||||
flex: none;
|
||||
inline-size: 1.5rem;
|
||||
block-size: 1.5rem;
|
||||
[data-md-field-option] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-block-size: var(--md-sys-measurement-space600);
|
||||
padding-inline: var(--md-sys-measurement-space200);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
font: var(--md-sys-typescale-body-lg);
|
||||
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* `data-md-active` is the row the arrow keys or the pointer are on. */
|
||||
[data-md-field-option][data-md-active] {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
|
||||
}
|
||||
|
||||
[data-md-field-option][aria-selected="true"] {
|
||||
background-color: var(--md-sys-color-secondary-container);
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
[data-md-field-option][aria-selected="true"][data-md-active] {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 8%, var(--md-sys-color-secondary-container));
|
||||
}
|
||||
|
||||
[data-md-field-check] {
|
||||
margin-inline-start: auto;
|
||||
background-color: currentColor;
|
||||
mask: url("../../svg/symbols/outlined/check.svg") center / contain no-repeat;
|
||||
}
|
||||
|
||||
[data-md-field-option]:not([aria-selected="true"]) [data-md-field-check] {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
@supports (appearance: base-select) {
|
||||
/* The select itself opts in from select.css, after its own `appearance: none`: both sit in
|
||||
`material.components` with the same specificity, and select.css's block always comes
|
||||
after this file, which it imports. */
|
||||
select[data-md-field-control]::picker(select) {
|
||||
appearance: base-select;
|
||||
}
|
||||
|
||||
/* The menu. The browser anchors it to the select; the select covers its whole
|
||||
field (field.css), so the menu is at least the field's width. It hangs under
|
||||
the field and goes above only when it does not fit there — the browser's own
|
||||
order tries the roomier side first, which opened a menu upwards with room to
|
||||
spare under it. It opens with a fade and a grow on the spatial spring and
|
||||
closes with a fade, as `<x-menu>` does. */
|
||||
select[data-md-field-control]::picker(select) {
|
||||
position-try-order: normal;
|
||||
max-block-size: 18rem;
|
||||
max-inline-size: calc(100vw - 32px);
|
||||
margin-block: var(--md-sys-measurement-space50);
|
||||
padding-block: var(--md-sys-measurement-space100);
|
||||
border-width: 0;
|
||||
border-radius: var(--md-sys-shape-corner-xs);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
box-shadow: var(--md-sys-elevation-2);
|
||||
opacity: 0;
|
||||
transform-origin: top;
|
||||
transition-property: opacity, display, overlay;
|
||||
transition-duration: var(--md-sys-motion-effects-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-effects-fast);
|
||||
transition-behavior: allow-discrete;
|
||||
}
|
||||
|
||||
select[data-md-field-control]:open::picker(select) {
|
||||
opacity: 1;
|
||||
transition-property: opacity, scale, display, overlay;
|
||||
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
select[data-md-field-control]:open::picker(select) {
|
||||
opacity: 0;
|
||||
scale: 0.95;
|
||||
}
|
||||
}
|
||||
|
||||
select[data-md-field-control] option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-block-size: var(--md-sys-measurement-space600);
|
||||
padding-block: 0;
|
||||
padding-inline: var(--md-sys-measurement-space200);
|
||||
background-color: transparent;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
font: var(--md-sys-typescale-body-lg);
|
||||
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
select[data-md-field-control] option:hover {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
select[data-md-field-control] option:focus-visible {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
|
||||
}
|
||||
|
||||
select[data-md-field-control] option:checked {
|
||||
background-color: var(--md-sys-color-secondary-container);
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
select[data-md-field-control] option:checked:hover {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 8%, var(--md-sys-color-secondary-container));
|
||||
}
|
||||
}
|
||||
|
||||
select[data-md-field-control] option:checked:focus-visible {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 10%, var(--md-sys-color-secondary-container));
|
||||
}
|
||||
|
||||
select[data-md-field-control] option:disabled {
|
||||
background-color: transparent;
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* The tick, at the row's end: the browser draws it before the label, and hides
|
||||
it on every row but the chosen one. The Material Symbol, as a mask, so it
|
||||
takes the row's ink. */
|
||||
select[data-md-field-control] option::checkmark {
|
||||
content: "";
|
||||
order: 1;
|
||||
flex: none;
|
||||
inline-size: var(--md-sys-measurement-space300);
|
||||
block-size: var(--md-sys-measurement-space300);
|
||||
margin-inline-start: auto;
|
||||
background-color: currentColor;
|
||||
mask: url("../../svg/symbols/outlined/check.svg") center / contain no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
/*
|
||||
* `<x-modal>`: M3's basic and full-screen dialogs (DialogTokens.kt, FullScreenDialogTokens.kt,
|
||||
* androidx Compose Material 3, Apache-2.0; docs/reference/m3/
|
||||
* components-actions-communication-containment.md § Dialogs → Specs).
|
||||
*
|
||||
* [data-md-modal] the <dialog>; data-md-fullscreen while `fullscreen` holds
|
||||
* [data-md-modal-box] surface-container-high, extra-large corner, elevation 3
|
||||
* [data-md-modal-bar] the 56px phone-only header bar (fullscreen, below 600px)
|
||||
* [data-md-modal-head] title/subtitle/icon, pinned
|
||||
* [data-md-modal-body] the only part that scrolls; [data-md-modal-content] wraps the slot
|
||||
* [data-md-modal-actions] pinned, trailing-aligned
|
||||
*
|
||||
* 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
|
||||
* (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): 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
|
||||
* always do while a body exists (structural: whichever follows or precedes it), the phone bar
|
||||
* only when it is the row actually sitting above the body there (computed in the view, because a
|
||||
* subtitle or an icon can keep the head on screen too, in which case the bar must not draw a rule
|
||||
* the head already owns); `data-md-separator` (the `separator` prop) draws the rule whatever the
|
||||
* scroll. The rule is a pseudo-element over the row's own edge, so showing it moves nothing.
|
||||
*
|
||||
* Every rule that reads the full-screen flag or the head's icon and subtitle goes through
|
||||
* `> [data-md-modal-box] >`, as the divider marks do: a basic dialog opened from inside a
|
||||
* full-screen dialog's body is a descendant of it, and must keep its own title and padding.
|
||||
*
|
||||
* `[data-md-modal-body]:focus-visible` is the scrolling body catching the dialog's first focus when
|
||||
* nothing inside can take it — natively in Chrome, whose scroll containers are focusable, and
|
||||
* through `materialShowModal()` (resources/js/dialog.js) in Firefox and WebKit — M3's 3px secondary
|
||||
* indicator, drawn inside the edge because the box's rounded, overflow-hidden corner would clip one
|
||||
* drawn outside.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
@import './button.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-modal] {
|
||||
margin: auto;
|
||||
max-width: 560px;
|
||||
min-width: 280px;
|
||||
width: calc(100vw - 48px);
|
||||
max-height: calc(100dvh - 48px);
|
||||
overflow: visible;
|
||||
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;
|
||||
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
[data-md-modal],
|
||||
[data-md-timepicker-dialog],
|
||||
[data-md-datepicker-picker]:modal {
|
||||
opacity: 0;
|
||||
scale: 0.95;
|
||||
}
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
[data-md-modal][data-md-fullscreen] {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
min-width: 0;
|
||||
height: 100dvh;
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-modal-box] {
|
||||
display: flex;
|
||||
max-height: inherit;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border-radius: var(--md-sys-shape-corner-xl);
|
||||
background-color: var(--md-sys-color-surface-container-high);
|
||||
box-shadow: var(--md-sys-elevation-3);
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
[data-md-modal][data-md-fullscreen] > [data-md-modal-box] {
|
||||
height: 100%;
|
||||
border-radius: var(--md-sys-shape-corner-none);
|
||||
padding-top: var(--material-safe-top, env(safe-area-inset-top));
|
||||
}
|
||||
}
|
||||
|
||||
/* The phone-only header bar: M3's full-screen dialog header, 56dp. */
|
||||
[data-md-modal-bar] {
|
||||
display: flex;
|
||||
height: var(--md-sys-measurement-space700);
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: var(--md-sys-measurement-space50);
|
||||
padding-inline: var(--md-sys-measurement-space50);
|
||||
}
|
||||
|
||||
@media (width >= 600px) {
|
||||
[data-md-modal-bar] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-modal-bar-title] {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font: var(--md-sys-typescale-title-lg);
|
||||
letter-spacing: var(--md-sys-typescale-title-lg-tracking);
|
||||
}
|
||||
|
||||
[data-md-modal-head] {
|
||||
flex-shrink: 0;
|
||||
padding-inline: var(--md-sys-measurement-space300);
|
||||
padding-top: var(--md-sys-measurement-space300);
|
||||
}
|
||||
|
||||
[data-md-modal-head]:has(+ [data-md-modal-body]) {
|
||||
padding-bottom: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-md-modal-head]:has(> [data-md-icon]) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* On a phone the bar already names a plain dialog; the head hides unless an icon or a
|
||||
subtitle keeps it there (the bar cannot hold either). */
|
||||
@media (width < 600px) {
|
||||
[data-md-modal][data-md-fullscreen] > [data-md-modal-box] > [data-md-modal-head]:not(:has(> [data-md-icon], > [data-md-modal-subtitle])) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-modal-head] > [data-md-icon] {
|
||||
margin-inline: auto;
|
||||
margin-bottom: var(--md-sys-measurement-space200);
|
||||
color: var(--md-sys-color-secondary);
|
||||
}
|
||||
|
||||
[data-md-modal-title] {
|
||||
font: var(--md-sys-typescale-headline-sm);
|
||||
letter-spacing: var(--md-sys-typescale-headline-sm-tracking);
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
[data-md-modal][data-md-fullscreen] > [data-md-modal-box] > [data-md-modal-head]:not(:has(> [data-md-icon])) > [data-md-modal-title] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-modal-subtitle] {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-md);
|
||||
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
||||
}
|
||||
|
||||
[data-md-modal-title] ~ [data-md-modal-subtitle],
|
||||
[data-md-modal-head] > [data-md-icon] ~ [data-md-modal-subtitle] {
|
||||
margin-top: var(--md-sys-measurement-space200);
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
[data-md-modal][data-md-fullscreen] > [data-md-modal-box] > [data-md-modal-head]:not(:has(> [data-md-icon])) > [data-md-modal-subtitle] {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-modal-body] {
|
||||
min-height: 0;
|
||||
flex: 1 1 0%;
|
||||
overflow-y: auto;
|
||||
padding-inline: var(--md-sys-measurement-space300);
|
||||
padding-top: var(--md-sys-measurement-space300);
|
||||
padding-bottom: var(--md-sys-measurement-space300);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-md);
|
||||
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-md-modal-body]:focus-visible {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
[data-md-modal-head] ~ [data-md-modal-body] {
|
||||
padding-top: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
[data-md-modal][data-md-fullscreen] > [data-md-modal-box] > [data-md-modal-head]:not(:has(> [data-md-icon], > [data-md-modal-subtitle])) ~ [data-md-modal-body] {
|
||||
padding-top: var(--md-sys-measurement-space200);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-modal-body]:has(~ [data-md-modal-actions]) {
|
||||
padding-bottom: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-md-modal-actions] {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
padding-inline: var(--md-sys-measurement-space300);
|
||||
padding-top: var(--md-sys-measurement-space300);
|
||||
padding-bottom: var(--md-sys-measurement-space300);
|
||||
}
|
||||
|
||||
[data-md-modal-body] ~ [data-md-modal-actions] {
|
||||
padding-top: var(--md-sys-measurement-space200);
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
[data-md-modal][data-md-fullscreen] > [data-md-modal-box] > [data-md-modal-actions] {
|
||||
min-height: var(--md-sys-measurement-space700);
|
||||
padding-top: var(--md-sys-measurement-space100);
|
||||
padding-bottom: var(--md-sys-measurement-space100);
|
||||
}
|
||||
}
|
||||
|
||||
/* The pinned-edge dividers: a pseudo-element over the row's own padding, drawn only while
|
||||
something is scrolled past it (or `separator` says always). */
|
||||
[data-md-modal-head],
|
||||
[data-md-modal-bar],
|
||||
[data-md-modal-actions] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-md-modal-head]::after,
|
||||
[data-md-modal-bar]::after,
|
||||
[data-md-modal-actions]::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
height: 1px;
|
||||
background-color: var(--md-sys-color-outline-variant);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
[data-md-modal-head]::after,
|
||||
[data-md-modal-bar]::after {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
[data-md-modal-actions]::before {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
dialog[data-md-overflow-top] > [data-md-modal-box] > [data-md-modal-head][data-md-modal-divider]::after,
|
||||
dialog[data-md-overflow-top] > [data-md-modal-box] > [data-md-modal-bar][data-md-modal-divider]::after,
|
||||
dialog[data-md-overflow-bottom] > [data-md-modal-box] > [data-md-modal-actions][data-md-modal-divider]::before,
|
||||
[data-md-modal-head][data-md-modal-divider][data-md-separator]::after,
|
||||
[data-md-modal-bar][data-md-modal-divider][data-md-separator]::after,
|
||||
[data-md-modal-actions][data-md-modal-divider][data-md-separator]::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* `<x-navigation-bar-item>`: one destination in an `<x-navigation-bar>`
|
||||
* (NavigationBarVerticalItemTokens.kt, NavigationBarHorizontalItemTokens.kt, androidx Compose
|
||||
* Material 3, Apache-2.0).
|
||||
*
|
||||
* [data-md-navigation-bar-item] data-md-active; the link or button itself
|
||||
* [data-md-navigation-pill] icon and label; becomes the indicator from 600px
|
||||
* [data-md-navigation-indicator] the 56×32 indicator around the icon below 600px
|
||||
* [data-md-navigation-icon] the floating badge's anchor (navigation-item.css)
|
||||
* [data-md-navigation-label]
|
||||
*
|
||||
* Below 600px (the bar's own width — a container query, so a bar in a narrow column keeps this
|
||||
* layout) the icon sits in a 56×32 `CornerFull` indicator over a label-medium label, both equally
|
||||
* 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). 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.
|
||||
*
|
||||
* The indicator's growing fill and its state layer are shared with the rail's item
|
||||
* (navigation-item.css); only this file's focus ring and the display swap between the two
|
||||
* indicator shapes are the bar's own.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
@import './badge.css';
|
||||
@import './navigation-item.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-navigation-bar-item] {
|
||||
--navigation-layer: 0;
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1 1 0%;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-block: 6px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
[data-md-navigation-bar-item][data-md-active] {
|
||||
color: var(--md-sys-color-secondary);
|
||||
}
|
||||
|
||||
[data-md-navigation-bar-item] [data-md-navigation-pill] {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
font: var(--md-sys-typescale-label-md);
|
||||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||||
}
|
||||
|
||||
[data-md-navigation-bar-item] [data-md-navigation-label] {
|
||||
max-width: 100%;
|
||||
padding-inline: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-md-navigation-bar-item] [data-md-navigation-indicator] {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 56px;
|
||||
height: 32px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
}
|
||||
|
||||
/* Vertical layout (below 600px, or a tall bar at any width): the indicator fills behind the
|
||||
icon; the pill draws no layer of its own. */
|
||||
[data-md-navigation-bar-item][data-md-active] [data-md-navigation-indicator] {
|
||||
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-bar-item] [data-md-navigation-pill]::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-md-navigation-bar-item]:focus-visible [data-md-navigation-indicator] {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Horizontal layout, from 600px: the pill itself fills and takes the layer; the indicator
|
||||
stops drawing either. Only the short bar (a tall bar keeps the vertical layout). */
|
||||
@container (width >= 600px) {
|
||||
[data-md-navigation-bar]:not([data-md-tall]) [data-md-navigation-bar-item] {
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
[data-md-navigation-bar]:not([data-md-tall]) [data-md-navigation-bar-item] [data-md-navigation-pill] {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
flex-direction: row;
|
||||
height: 40px;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
[data-md-navigation-bar]:not([data-md-tall]) [data-md-navigation-bar-item] [data-md-navigation-label] {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
[data-md-navigation-bar]:not([data-md-tall]) [data-md-navigation-bar-item][data-md-active] {
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
[data-md-navigation-bar]:not([data-md-tall]) [data-md-navigation-bar-item][data-md-active] [data-md-navigation-pill] {
|
||||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||||
}
|
||||
|
||||
[data-md-navigation-bar]:not([data-md-tall]) [data-md-navigation-bar-item][data-md-active] [data-md-navigation-indicator] {
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
[data-md-navigation-bar]:not([data-md-tall]) [data-md-navigation-bar-item] [data-md-navigation-pill]::before {
|
||||
display: block;
|
||||
}
|
||||
|
||||
[data-md-navigation-bar]:not([data-md-tall]) [data-md-navigation-bar-item] [data-md-navigation-indicator]::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-md-navigation-bar]:not([data-md-tall]) [data-md-navigation-bar-item]:focus-visible [data-md-navigation-indicator] {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-md-navigation-bar]:not([data-md-tall]) [data-md-navigation-bar-item]:focus-visible [data-md-navigation-pill] {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* `<x-navigation-bar>`: M3 Expressive's flexible navigation bar, three to five destinations
|
||||
* pinned to the bottom of a compact or medium window (NavigationBarTokens.kt, androidx Compose
|
||||
* Material 3, Apache-2.0, and the layout of ShortNavigationBar.kt;
|
||||
* docs/reference/m3/components-navigation-selection-inputs.md § Navigation Bar).
|
||||
*
|
||||
* [data-md-navigation-bar] 64px in surface-container; data-md-tall (80px),
|
||||
* data-md-hide-on-scroll and data-md-hidden
|
||||
* [data-md-navigation-bar-items] equal widths below 600px; centred with Compose's
|
||||
* calculateCenteredContentHorizontalPadding from it
|
||||
* [data-md-navigation-bar-item] navigation-bar-item.css
|
||||
*
|
||||
* `data-md-tall` is `NavigationBarTokens.TallContainerHeight`, 80px against the short bar's 64
|
||||
* (`ContainerHeight`) — Missing until this rewrite. It keeps the vertical, icon-over-label item
|
||||
* layout at every width, so every rule below that only the short bar's horizontal layout needs
|
||||
* says `:not([data-md-tall])` first.
|
||||
*
|
||||
* `data-md-hide-on-scroll` is M3's scrolling behaviour, also Missing until now: "hides on
|
||||
* scroll-down, reappears on scroll-up… never hide it while a screen reader is active"
|
||||
* (§ Navigation Bar/Behaviour). resources/js/navigation.js drives `data-md-hidden` and never hides
|
||||
* the bar while a snackbar, a bottom sheet or a drawer rests on its edge — those are anchored to
|
||||
* its bottom edge and would slide with it — and focus reaching the bar brings it back, the web's
|
||||
* nearest equivalent of "a screen reader is active". It slides on the default spatial spring,
|
||||
* which reduced motion zeroes along with every other duration token.
|
||||
*
|
||||
* The item's own indicator, pill and state layer (shared with the rail item) are
|
||||
* navigation-bar-item.css and navigation-item.css.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@layer material.components {
|
||||
[data-md-navigation-bar] {
|
||||
container-type: inline-size;
|
||||
padding-inline: var(--material-safe-left, env(safe-area-inset-left)) var(--material-safe-right, env(safe-area-inset-right));
|
||||
padding-bottom: var(--material-safe-bottom, env(safe-area-inset-bottom));
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-md-navigation-bar-items] {
|
||||
display: flex;
|
||||
min-height: 64px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
[data-md-navigation-bar][data-md-tall] [data-md-navigation-bar-items] {
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
[data-md-navigation-bar][data-md-hide-on-scroll] {
|
||||
transition: translate var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default);
|
||||
}
|
||||
|
||||
[data-md-navigation-bar][data-md-hide-on-scroll][data-md-hidden] {
|
||||
translate: 0 100%;
|
||||
}
|
||||
|
||||
/* From 600px (M3's medium window, the bar's own width): icon and label side by side in a
|
||||
40px pill, the items gathered in the middle with Centered arrangement's own padding. The
|
||||
short bar only — a tall bar is the vertical layout everywhere. */
|
||||
@container (width >= 600px) {
|
||||
[data-md-navigation-bar]:not([data-md-tall]) [data-md-navigation-bar-items] {
|
||||
width: calc(10% * (var(--navigation-bar-count, 7) + 3));
|
||||
min-width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
[data-md-navigation-bar]:not([data-md-tall]) [data-md-navigation-bar-items]:has(> :last-child:nth-child(1)) { --navigation-bar-count: 1; }
|
||||
[data-md-navigation-bar]:not([data-md-tall]) [data-md-navigation-bar-items]:has(> :last-child:nth-child(2)) { --navigation-bar-count: 2; }
|
||||
[data-md-navigation-bar]:not([data-md-tall]) [data-md-navigation-bar-items]:has(> :last-child:nth-child(3)) { --navigation-bar-count: 3; }
|
||||
[data-md-navigation-bar]:not([data-md-tall]) [data-md-navigation-bar-items]:has(> :last-child:nth-child(4)) { --navigation-bar-count: 4; }
|
||||
[data-md-navigation-bar]:not([data-md-tall]) [data-md-navigation-bar-items]:has(> :last-child:nth-child(5)) { --navigation-bar-count: 5; }
|
||||
[data-md-navigation-bar]:not([data-md-tall]) [data-md-navigation-bar-items]:has(> :last-child:nth-child(6)) { --navigation-bar-count: 6; }
|
||||
}
|
||||
|
||||
/*
|
||||
* A bar that has slid off the bottom of the window is no longer there to clear, so the offset
|
||||
* everything pinned to the bottom reads drops to the bottom safe area and whatever the
|
||||
* application has docked on the bar — a `fab` button, the snackbar and the page's own bottom
|
||||
* padding all follow it down and come back up with it. `<x-scaffold>` publishes
|
||||
* `--material-bottom-bar` itself, in `material.layout` (layout/scaffold.css), a layer this
|
||||
* file's own `material.components` always outranks by declaration order alone — the same
|
||||
* reason toolbar.css's and fab.css's overrides only have to beat another `material.components`
|
||||
* rule. Keyed on `data-md-scaffold`, the hook the scaffold renders on its root.
|
||||
*/
|
||||
[data-md-scaffold]:has([data-md-navigation-bar][data-md-hide-on-scroll][data-md-hidden]) {
|
||||
--material-bottom-bar: calc(var(--material-safe-bottom, env(safe-area-inset-bottom)) + var(--material-bottom-extra, 0px));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Shared between `<x-navigation-bar-item>` and `<x-navigation-rail-item>` (imported by both
|
||||
* navigation-bar-item.css and navigation-rail-item.css): the destination indicator's growing fill
|
||||
* and its state layer, on androidx's one stated colour for a navigation item's layer.
|
||||
*
|
||||
* `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. 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`
|
||||
* cannot draw on a different element than the one it decorates, and `:focus-visible` never
|
||||
* matches a child (the same reason datepicker.css keeps the day's own layer).
|
||||
*
|
||||
* The active indicator itself is a secondary-container fill painted as a background image so it
|
||||
* can grow out of its centre without stretching the icon: when a page arrives through
|
||||
* wire:navigate, resources/js/navigation.js starts it at zero width for a moment and it springs
|
||||
* open on the default spatial spring, as Compose's indicator does when the selection changes
|
||||
* (NavigationItem.kt). A full page load draws it at once; reduced motion zeroes the transition.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@layer material.components {
|
||||
[data-md-navigation-bar-item] :is([data-md-navigation-indicator], [data-md-navigation-pill]),
|
||||
[data-md-navigation-rail-item],
|
||||
[data-md-navigation-rail-item] [data-md-navigation-indicator] {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
transition: background-size var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
:is([data-md-navigation-bar-item], [data-md-navigation-rail-item]):hover {
|
||||
--navigation-layer: var(--md-sys-state-hover-state-layer-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
:is([data-md-navigation-bar-item], [data-md-navigation-rail-item]):focus-visible {
|
||||
--navigation-layer: var(--md-sys-state-focus-state-layer-opacity);
|
||||
}
|
||||
|
||||
:is([data-md-navigation-bar-item], [data-md-navigation-rail-item]):active {
|
||||
--navigation-layer: var(--md-sys-state-pressed-state-layer-opacity);
|
||||
}
|
||||
|
||||
[data-md-navigation-bar-item] [data-md-navigation-indicator]::before,
|
||||
[data-md-navigation-bar-item] [data-md-navigation-pill]::before,
|
||||
[data-md-navigation-rail-item]::before,
|
||||
[data-md-navigation-rail-item] [data-md-navigation-indicator]::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
border-radius: inherit;
|
||||
background-color: var(--md-sys-color-on-secondary-container);
|
||||
opacity: var(--navigation-layer, 0);
|
||||
pointer-events: none;
|
||||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
/* The wrapper a floating badge anchors to: tight around the glyph alone, not the wider
|
||||
indicator or pill around it, so the badge sits at the icon's own corner (badge.css). */
|
||||
[data-md-navigation-icon] {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* `<x-navigation-rail-item>`: one destination in an `<x-navigation-rail>`, drawn in whichever
|
||||
* shape the rail has (NavigationRailVerticalItemTokens.kt, NavigationRailHorizontalItemTokens.kt,
|
||||
* NavigationRailBaselineItemTokens.kt, NavigationRailColorTokens.kt, androidx Compose Material 3,
|
||||
* Apache-2.0).
|
||||
*
|
||||
* [data-md-navigation-rail-item] data-md-active; the link or button itself
|
||||
* [data-md-navigation-indicator] the 56×32 indicator, collapsed only
|
||||
* [data-md-navigation-icon] the floating badge's anchor (navigation-item.css)
|
||||
* [data-md-navigation-badge-icon] the small/large badge, collapsed only
|
||||
* [data-md-navigation-label]
|
||||
* [data-md-navigation-badge-end] the large badge at the label's end, expanded only
|
||||
*
|
||||
* Expanded, a 56px full-width pill: the icon, the label-large label beside it (up to two lines,
|
||||
* collapsed) and a count at its end. Collapsed, the icon sits in a 56×32 `CornerFull` indicator
|
||||
* over a label-medium label, with the count on the icon instead — badges move from the icon
|
||||
* (collapsed) to the label end (expanded), M3's stated rule.
|
||||
*
|
||||
* The item's own state layer is split in two: while expanded the layer paints the whole item (the
|
||||
* 56px pill *is* the indicator); while collapsed it paints only the 56×32 box. A CSS variant
|
||||
* selector cannot follow a pseudo-element, so `--navigation-item-layer`/`--navigation-indicator-layer`
|
||||
* hand `--navigation-layer` (navigation-item.css) on through variables, and each rule below
|
||||
* outweighs navigation-item.css's shared one with a doubled `[data-md-navigation-rail-item]`
|
||||
* attribute, the same specificity trick `navigation-rail.css` uses for the header's FAB.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
@import './badge.css';
|
||||
@import './navigation-item.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-navigation-rail-item] {
|
||||
--navigation-layer: 0;
|
||||
--navigation-item-layer: var(--navigation-layer);
|
||||
--navigation-indicator-layer: 0;
|
||||
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 56px;
|
||||
margin-inline: 20px;
|
||||
padding-inline: 16px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
text-align: start;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item][data-md-navigation-rail-item]::before {
|
||||
background-color: var(--md-sys-color-on-secondary-container);
|
||||
opacity: var(--navigation-item-layer);
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item][data-md-navigation-rail-item] [data-md-navigation-indicator]::before {
|
||||
background-color: var(--md-sys-color-on-secondary-container);
|
||||
opacity: var(--navigation-indicator-layer);
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item][data-md-active] {
|
||||
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] [data-md-navigation-indicator] {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item] [data-md-navigation-label] {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item]:focus-visible {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* The large badge at the label's end, expanded only — collapsed, the badge moves onto the
|
||||
icon instead (`[data-md-navigation-badge-icon]`, `display: contents` there). */
|
||||
[data-md-navigation-badge-end] {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-md-navigation-badge-icon] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ---- Collapsed: icon over label, the 56×32 indicator, badge back on the icon. ---- */
|
||||
|
||||
[data-md-navigation-rail-item] {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
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;
|
||||
--navigation-item-layer: 0;
|
||||
--navigation-indicator-layer: var(--navigation-layer);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item] [data-md-navigation-label] {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
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] {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
background-image: none;
|
||||
color: var(--md-sys-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item] [data-md-navigation-indicator] {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
width: 56px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item][data-md-active] [data-md-navigation-indicator] {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item]:focus-visible {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item]:focus-visible [data-md-navigation-indicator] {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-badge-end],
|
||||
[data-md-navigation-badge-icon] {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-badge-icon] {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* `<x-navigation-rail-section>`: a group of `<x-navigation-rail-item>`s under a heading, in an
|
||||
* `<x-navigation-rail>`. M3 draws a section header only while the rail is expanded; collapsed, a
|
||||
* little space sets the group apart instead, and the heading keeps naming the group for a screen
|
||||
* reader either way (`role="group"`/`aria-labelledby`, rendered by the view).
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@layer material.components {
|
||||
[data-md-navigation-rail-section] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-section]:not(:first-child) {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-heading] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 48px;
|
||||
padding-inline: 36px 20px;
|
||||
overflow: hidden;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-title-sm);
|
||||
letter-spacing: var(--md-sys-typescale-title-sm-tracking);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,612 @@
|
||||
/*
|
||||
* `<x-navigation-rail>`: M3 Expressive's navigation rail, collapsed (96px, icon over label) or
|
||||
* expanded (a full-width pill, icon beside label) down the start edge of a `medium` or wider
|
||||
* window (NavigationRailCollapsedTokens.kt, NavigationRailExpandedTokens.kt, androidx Compose
|
||||
* Material 3, Apache-2.0, and the layout of WideNavigationRail.kt;
|
||||
* docs/reference/m3/components-navigation-selection-inputs.md § Navigation Rail).
|
||||
*
|
||||
* [data-md-navigation-rail="collapsed|expanded|collapsible|modal|adaptive"] data-md-open
|
||||
* data-md-closing="sheet|scrim", while it exits
|
||||
* [data-md-navigation-rail-scrim] modal and adaptive rails, open or closing only
|
||||
* [data-md-navigation-rail-panel] the <nav>
|
||||
* [data-md-navigation-rail-header] never scrolls
|
||||
* [data-md-navigation-rail-menu-row] the menu button and the brand
|
||||
* [data-md-navigation-rail-menu] md-state-layer md-focus-ring md-touch-target
|
||||
* [data-md-navigation-rail-brand]
|
||||
* [data-md-navigation-rail-fab-row] one <x-fab>, morphed (see the footer note)
|
||||
* [data-md-navigation-rail-destinations] scrolls when the window is too short
|
||||
* [data-md-navigation-rail-section] navigation-rail-section.css
|
||||
* [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, 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");
|
||||
* below `expanded` (840px) an `adaptive` rail not open — there is no standard rail below it; at
|
||||
* `expanded` (840–1199) an `adaptive` rail not open, when the visitor chose collapsed *or* has not
|
||||
* chosen (`data-rail-auto`, which `data-rail` alone cannot say, since it also carries
|
||||
* `rail.default` for a visitor who never chose); from `large` (1200) an `adaptive` rail not open,
|
||||
* only when the visitor chose collapsed — M3 starts it expanded there by default. Every branch but
|
||||
* the first reads `:not([data-md-open])`: a rail open over a scrim is drawn expanded whatever made
|
||||
* it collapsed, which only `hide-when-collapsed` can bring about outside the modal modes.
|
||||
* `resources/js/navigation.js` reads the same numbers, so the menu button and the drawing agree at
|
||||
* every width.
|
||||
*
|
||||
* **Closing**: nothing here transitions `display`, which Firefox cannot do even with
|
||||
* `allow-discrete`, so a panel sliding off the window and a fading scrim were cut to nothing there.
|
||||
* `resources/js/navigation.js` sets `data-md-closing` on a rail that was open over the page until
|
||||
* its exit transitions have run — `sheet` when the panel leaves the window (a compact adaptive
|
||||
* rail, or one that hides when collapsed), `scrim` when the panel stands in the layout again — and
|
||||
* the rules near the end of the file keep what is leaving displayed meanwhile, in every engine.
|
||||
* The collapsed branches do not read it: a closing rail is already collapsed, which is what
|
||||
* `--md-navigation-rail-value` says the moment it starts to close.
|
||||
*
|
||||
* The first set of branches also publishes the answer: `--md-navigation-rail-value` is `expanded`
|
||||
* on every rail and `collapsed` wherever those branches hold — M3's two rail values, Compose's
|
||||
* WideNavigationRailValue — so what an application puts in a rail reads it with
|
||||
* `@container style(--md-navigation-rail-value: collapsed)` rather than copying the conditions. It
|
||||
* is an unregistered custom property, so it inherits into every descendant and is unset outside a
|
||||
* rail, where neither value matches.
|
||||
*
|
||||
* `data-md-width="narrow"` is M3's other collapsed width, `NarrowContainerWidth` 80px against the
|
||||
* default 96 (`CollapsedContainerWidth`) — a variable, so every rule below that has its own reason
|
||||
* to name a collapsed width does not have to know which one applies.
|
||||
*
|
||||
* `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. `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) 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.
|
||||
*
|
||||
* The destination item, its indicator and its state layer are navigation-rail-item.css and
|
||||
* navigation-item.css (shared with the bar's item); a section's heading is
|
||||
* navigation-rail-section.css.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
@import './fab.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-navigation-rail] {
|
||||
--navigation-rail-expanded-width: clamp(220px, var(--navigation-rail-width, 256px), 360px);
|
||||
--navigation-rail-collapsed-width: 96px;
|
||||
--md-navigation-rail-value: expanded;
|
||||
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: var(--navigation-rail-expanded-width);
|
||||
transition: width var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default);
|
||||
|
||||
&: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]) *
|
||||
) {
|
||||
width: var(--navigation-rail-collapsed-width);
|
||||
--md-navigation-rail-value: collapsed;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
width: var(--navigation-rail-collapsed-width);
|
||||
--md-navigation-rail-value: collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
width: var(--navigation-rail-collapsed-width);
|
||||
--md-navigation-rail-value: collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
@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: var(--navigation-rail-collapsed-width);
|
||||
--md-navigation-rail-value: collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
@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: var(--navigation-rail-collapsed-width);
|
||||
--md-navigation-rail-value: collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail][data-md-width='narrow'] {
|
||||
--navigation-rail-collapsed-width: 80px;
|
||||
}
|
||||
|
||||
[data-md-navigation-rail][data-md-hide-when-collapsed] {
|
||||
&: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]) *
|
||||
) {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
width: 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]) *
|
||||
) {
|
||||
width: 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]) *
|
||||
) {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* There is no room for a label under the 56px indicator at 80px, so a narrow rail is the icons
|
||||
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] {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
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
|
||||
ModalWideNavigationRail does; the adaptive rail does below `expanded`, and takes no room at
|
||||
all below `medium`. */
|
||||
[data-md-navigation-rail='modal'] {
|
||||
width: var(--navigation-rail-collapsed-width);
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
[data-md-navigation-rail='adaptive'] {
|
||||
width: var(--navigation-rail-collapsed-width);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
[data-md-navigation-rail='adaptive'] {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
/* "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. `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'] {
|
||||
width: var(--navigation-rail-collapsed-width);
|
||||
}
|
||||
}
|
||||
|
||||
/* `--material-banner` is the height of a bar pinned across the top of the window — what
|
||||
<x-scaffold>'s `banner` slot holds, if the application made it sticky. The rail sticks under
|
||||
it rather than behind it; 0 without one. */
|
||||
[data-md-navigation-rail-panel] {
|
||||
position: sticky;
|
||||
top: var(--material-banner, 0px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: calc(100dvh - var(--material-banner, 0px));
|
||||
overflow-x: clip;
|
||||
padding-bottom: var(--material-safe-bottom, env(safe-area-inset-bottom));
|
||||
background-color: var(--md-sys-color-surface);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
transition:
|
||||
width var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast),
|
||||
background-color var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-effects-default);
|
||||
}
|
||||
|
||||
/* 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". 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);
|
||||
}
|
||||
|
||||
[data-md-navigation-rail][data-md-fill='false']:not([data-md-open], [data-md-closing='sheet']) > [data-md-navigation-rail-panel] {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Open: expanded over a scrim, in surface-container with a large corner at its inner edge — and
|
||||
still, while a panel that is leaving the window slides off it (`data-md-closing="sheet"`). */
|
||||
[data-md-navigation-rail]:is([data-md-open], [data-md-closing='sheet']) > [data-md-navigation-rail-panel] {
|
||||
position: fixed;
|
||||
inset-block: 0;
|
||||
inset-inline-start: 0;
|
||||
z-index: 50;
|
||||
width: var(--navigation-rail-expanded-width);
|
||||
max-width: calc(100vw - 56px);
|
||||
height: 100dvh;
|
||||
max-height: none;
|
||||
border-start-end-radius: var(--md-sys-shape-corner-lg);
|
||||
border-end-end-radius: var(--md-sys-shape-corner-lg);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
box-shadow: var(--md-sys-elevation-2);
|
||||
}
|
||||
|
||||
/* On a compact window there is no collapsed rail to grow out of: the open rail slides in from
|
||||
the edge, on emphasized decelerate rather than a spring, which would overshoot and open a
|
||||
gap. */
|
||||
@media (width < 600px) {
|
||||
[data-md-navigation-rail='adaptive'] > [data-md-navigation-rail-panel] {
|
||||
position: fixed;
|
||||
inset-block: 0;
|
||||
inset-inline-start: 0;
|
||||
z-index: 50;
|
||||
display: none;
|
||||
width: var(--navigation-rail-expanded-width);
|
||||
max-width: calc(100vw - 56px);
|
||||
height: 100dvh;
|
||||
max-height: none;
|
||||
border-start-end-radius: var(--md-sys-shape-corner-lg);
|
||||
border-end-end-radius: var(--md-sys-shape-corner-lg);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
box-shadow: var(--md-sys-elevation-2);
|
||||
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='adaptive'][data-md-open] > [data-md-navigation-rail-panel] {
|
||||
display: flex;
|
||||
translate: 0 0;
|
||||
transition:
|
||||
translate var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-easing-emphasized-decelerate);
|
||||
|
||||
@starting-style {
|
||||
translate: -100% 0;
|
||||
}
|
||||
|
||||
&:is([dir='rtl'], [dir='rtl'] *) {
|
||||
translate: 0 0;
|
||||
|
||||
@starting-style {
|
||||
translate: 100% 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* A rail that hides when collapsed is in the same position: nothing is left in the layout for
|
||||
it to grow out of, so it takes the compact window's slide-in. The whole treatment is under
|
||||
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] {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
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] {
|
||||
display: flex;
|
||||
translate: 0 0;
|
||||
transition:
|
||||
translate var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-easing-emphasized-decelerate);
|
||||
|
||||
@starting-style {
|
||||
translate: -100% 0;
|
||||
}
|
||||
|
||||
&:is([dir='rtl'], [dir='rtl'] *) {
|
||||
translate: 0 0;
|
||||
|
||||
@starting-style {
|
||||
translate: 100% 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Closing: what leaves stays drawn while it animates out. The exits above transition `translate`
|
||||
and the scrim's `opacity` only — not `display`, which Firefox cannot transition even with
|
||||
`allow-discrete`, so the slide and the fade were cut to nothing there — and
|
||||
resources/js/navigation.js sets `data-md-closing` on the rail until those transitions have
|
||||
run: `sheet` when the panel leaves the window, `scrim` when only the scrim was over the page.
|
||||
One attribute more than every collapsed branch, so a hold outranks the `display: none` those
|
||||
branches draw. */
|
||||
[data-md-navigation-rail][data-md-hide-when-collapsed][data-md-closing='sheet'] > [data-md-navigation-rail-panel] {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
[data-md-navigation-rail='adaptive'][data-md-closing='sheet'] > [data-md-navigation-rail-panel] {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* The two bands the configuration above does not reach, because there it is the window and not
|
||||
the visitor that collapses the rail: below `medium` for a collapsible rail — already held at
|
||||
its collapsed width above — and `medium` itself for the adaptive one, which M3 gives a
|
||||
collapsed rail (below `medium` the adaptive rail is already away, over the page). This file's
|
||||
header and resources/js/navigation.js read the same two numbers. */
|
||||
@media (width < 600px) {
|
||||
[data-md-navigation-rail='collapsible'][data-md-hide-when-collapsed] {
|
||||
width: var(--navigation-rail-collapsed-width);
|
||||
}
|
||||
|
||||
[data-md-navigation-rail='collapsible'][data-md-hide-when-collapsed] > [data-md-navigation-rail-panel] {
|
||||
display: flex;
|
||||
translate: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (600px <= width < 840px) {
|
||||
[data-md-navigation-rail='adaptive'][data-md-hide-when-collapsed] {
|
||||
width: var(--navigation-rail-collapsed-width);
|
||||
}
|
||||
|
||||
[data-md-navigation-rail='adaptive'][data-md-hide-when-collapsed] > [data-md-navigation-rail-panel] {
|
||||
display: flex;
|
||||
translate: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-scrim] {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 40;
|
||||
display: none;
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent);
|
||||
opacity: 0;
|
||||
transition: opacity var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-effects-default);
|
||||
}
|
||||
|
||||
[data-md-navigation-rail][data-md-closing] > [data-md-navigation-rail-scrim] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
[data-md-navigation-rail][data-md-open] > [data-md-navigation-rail-scrim] {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
|
||||
@starting-style {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 44px above the header (TopSpace), 40px under it (HeaderSpaceMinimum) — 32 here and 8 as the
|
||||
destinations' own padding, which keeps the first item's focus ring inside the scroller. */
|
||||
[data-md-navigation-rail-header] {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding-top: calc(var(--material-safe-top, env(safe-area-inset-top)) + 44px);
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-panel] > [data-md-navigation-rail-destinations]:first-child {
|
||||
padding-top: calc(var(--material-safe-top, env(safe-area-inset-top)) + 44px);
|
||||
}
|
||||
|
||||
/* The menu button and the brand: 20px at the row's end always; 28px at its start with the
|
||||
button, so it centres a 40px button in the default 96px collapsed rail (28 either side); 20px
|
||||
without one. A narrow (80px) rail's button re-centres to 20px either side while collapsed —
|
||||
expanded, the row always keeps 28, since a rail's *expanded* width never depends on `narrow`. */
|
||||
[data-md-navigation-rail-menu-row] {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-inline-end: 20px;
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-header]:has([data-md-navigation-rail-menu]) [data-md-navigation-rail-menu-row] {
|
||||
padding-inline-start: 28px;
|
||||
}
|
||||
|
||||
[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] {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-menu] {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* The two glyphs swap by `display: contents`/`none`, so the button's own size never depends on
|
||||
which is shown, and the hidden one never announces to a screen reader (the button's own
|
||||
aria-label already says what the click will do). */
|
||||
[data-md-navigation-rail-menu-collapse-icon] {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-menu-expand-icon] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-menu-collapse-icon],
|
||||
[data-md-navigation-rail-menu-expand-icon] {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-menu-expand-icon] {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-brand] {
|
||||
min-width: 0;
|
||||
flex: 1 1 0%;
|
||||
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* The FAB row: 20px either side in the default 96px rail, 12px in a narrow 80 while collapsed
|
||||
(the FAB itself is centred by the row's own width, not by this padding). */
|
||||
[data-md-navigation-rail-fab-row] {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding-inline: 20px;
|
||||
}
|
||||
|
||||
[data-md-navigation-rail][data-md-width='narrow'] [data-md-navigation-rail-fab-row] {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
padding-inline: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-destinations] {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding-block: 8px;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-destinations],
|
||||
[data-md-navigation-rail-footer] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-footer] {
|
||||
flex-shrink: 0;
|
||||
padding-block: 8px 16px;
|
||||
}
|
||||
|
||||
/* "Alignment: top or center; on tablets prefer center alignment for reach. Menu icon and FAB
|
||||
are always top-aligned" (§ Navigation Rail/Behaviour). Only the destinations move — the
|
||||
header and the footer stay at their ends — and `safe` puts them back at the top the moment
|
||||
there are more of them than fit, where centring would otherwise push the first one out of
|
||||
reach above the scroller. */
|
||||
[data-md-navigation-rail][data-md-align='center'] [data-md-navigation-rail-destinations] {
|
||||
justify-content: safe center;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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"). 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.
|
||||
*/
|
||||
[data-md-navigation-rail-header] [data-md-fab][data-md-fab],
|
||||
[data-md-navigation-rail-header] [data-md-fab][data-md-fab]:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* "Rail transitions from collapsed to expanded should animate the FAB into and out of an
|
||||
* 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
|
||||
* left against the height its size already sets — 56, 80 or 96px, the three FAB sizes — so no
|
||||
* number is repeated here. The label stays in the accessibility tree at both widths, so the FAB
|
||||
* keeps its name. The cap is there because a transition needs a length to travel to; a rail
|
||||
* FAB's label is a word or two.
|
||||
*/
|
||||
[data-md-navigation-rail-header] [data-md-fab] > span {
|
||||
max-width: 256px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
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);
|
||||
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
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] {
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
min-inline-size: 0;
|
||||
aspect-ratio: 1;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,584 +0,0 @@
|
||||
/*
|
||||
* M3 Expressive navigation: the flexible navigation bar and the navigation rail — collapsed,
|
||||
* expanded and modal.
|
||||
*
|
||||
* Values from androidx Compose Material 3 (Apache-2.0) at androidx-main
|
||||
* 27cf9a7d5788aa0f5f2d8b6699ce279560daf326: tokens/NavigationBarTokens.kt,
|
||||
* NavigationBarVerticalItemTokens.kt, NavigationBarHorizontalItemTokens.kt,
|
||||
* NavigationRailCollapsedTokens.kt, NavigationRailExpandedTokens.kt,
|
||||
* NavigationRailBaselineItemTokens.kt, NavigationRailVerticalItemTokens.kt,
|
||||
* NavigationRailHorizontalItemTokens.kt, NavigationRailColorTokens.kt, and the layout in
|
||||
* ShortNavigationBar.kt, WideNavigationRail.kt and NavigationItem.kt.
|
||||
*
|
||||
* [data-navigation-bar] surface-container, 64px, the bottom safe area under it
|
||||
* [data-navigation-bar-items] equal widths; centred from a 600px-wide bar
|
||||
* [data-navigation-bar-item] data-active
|
||||
* [data-navigation-pill] icon and label; the indicator itself from 600px
|
||||
* [data-navigation-indicator] the 56×32 indicator around the icon below 600px
|
||||
* [data-navigation-label]
|
||||
*
|
||||
* [data-navigation-rail="collapsed|expanded|collapsible|modal|adaptive"] data-open
|
||||
* [data-navigation-rail-scrim] modal and adaptive rails
|
||||
* [data-navigation-rail-panel] the <nav>
|
||||
* [data-navigation-rail-header] menu button, brand, FAB — never scrolls
|
||||
* [data-navigation-rail-destinations] scrolls when the window is too short
|
||||
* [data-navigation-rail-section] a heading (expanded only) and its items
|
||||
* [data-navigation-rail-item] data-active; the full-width pill when expanded
|
||||
* [data-navigation-indicator] the 56×32 indicator when collapsed
|
||||
* [data-navigation-label]
|
||||
* [data-navigation-rail-footer] never scrolls
|
||||
*
|
||||
* `rail-collapsed:` matches a rail, and everything in it, while it is drawn collapsed — whatever
|
||||
* made it so: its mode, the visitor's choice on <html data-rail> (set before the first paint by
|
||||
* <x-theme-script>), or a window under `lg` for the adaptive rail. A rail item is written once
|
||||
* and takes both shapes from it; so can anything an application puts in a rail
|
||||
* (`<span class="rail-collapsed:hidden">`).
|
||||
*/
|
||||
|
||||
@custom-variant rail-collapsed {
|
||||
&:where([data-navigation-rail='collapsed'], [data-navigation-rail='collapsed'] *) {
|
||||
@slot;
|
||||
}
|
||||
|
||||
&:where([data-rail='collapsed'] [data-navigation-rail='collapsible'], [data-rail='collapsed'] [data-navigation-rail='collapsible'] *) {
|
||||
@slot;
|
||||
}
|
||||
|
||||
&:where([data-navigation-rail='modal']:not([data-open]), [data-navigation-rail='modal']:not([data-open]) *) {
|
||||
@slot;
|
||||
}
|
||||
|
||||
@media (width < 64rem) {
|
||||
&:where([data-navigation-rail='adaptive']:not([data-open]), [data-navigation-rail='adaptive']:not([data-open]) *) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 64rem) {
|
||||
&:where([data-rail='collapsed'] [data-navigation-rail='adaptive'], [data-rail='collapsed'] [data-navigation-rail='adaptive'] *) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* ---------------------------------------------------------------- the navigation bar */
|
||||
|
||||
[data-navigation-bar] {
|
||||
container-type: inline-size;
|
||||
padding-inline: env(safe-area-inset-left) env(safe-area-inset-right);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-navigation-bar-items] {
|
||||
display: flex;
|
||||
min-height: 4rem;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
[data-navigation-bar-item] {
|
||||
--navigation-layer: 0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-block: 0.375rem;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
[data-navigation-bar-item] [data-navigation-pill] {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
font: var(--md-sys-typescale-label-md);
|
||||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||||
}
|
||||
|
||||
[data-navigation-bar-item] [data-navigation-label] {
|
||||
max-width: 100%;
|
||||
padding-inline: 0.25rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-navigation-bar-item][data-active] {
|
||||
color: var(--md-sys-color-secondary);
|
||||
}
|
||||
|
||||
/* From 600dp (M3's medium window), icon and label side by side in a 40px indicator, and the
|
||||
items centred with the padding ShortNavigationBar's Centered arrangement computes. */
|
||||
@container (width >= 37.5rem) {
|
||||
[data-navigation-bar-items] {
|
||||
width: calc(10% * (var(--navigation-bar-count, 7) + 3));
|
||||
min-width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
[data-navigation-bar-items]:has(> :last-child:nth-child(1)) { --navigation-bar-count: 1; }
|
||||
[data-navigation-bar-items]:has(> :last-child:nth-child(2)) { --navigation-bar-count: 2; }
|
||||
[data-navigation-bar-items]:has(> :last-child:nth-child(3)) { --navigation-bar-count: 3; }
|
||||
[data-navigation-bar-items]:has(> :last-child:nth-child(4)) { --navigation-bar-count: 4; }
|
||||
[data-navigation-bar-items]:has(> :last-child:nth-child(5)) { --navigation-bar-count: 5; }
|
||||
[data-navigation-bar-items]:has(> :last-child:nth-child(6)) { --navigation-bar-count: 6; }
|
||||
|
||||
[data-navigation-bar-item] {
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
[data-navigation-bar-item] [data-navigation-pill] {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
flex-direction: row;
|
||||
height: 2.5rem;
|
||||
padding-inline: 1rem;
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
}
|
||||
|
||||
[data-navigation-bar-item] [data-navigation-label] {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
[data-navigation-bar-item][data-active] {
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
[data-navigation-bar-item][data-active] [data-navigation-pill] {
|
||||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- the navigation rail */
|
||||
|
||||
[data-navigation-rail] {
|
||||
--navigation-rail-expanded-width: clamp(13.75rem, var(--navigation-rail-width, 16rem), 22.5rem);
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: var(--navigation-rail-expanded-width);
|
||||
transition: width var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default);
|
||||
|
||||
@variant rail-collapsed {
|
||||
width: 6rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* A modal rail keeps its collapsed width in the layout while it is open over it, as Compose's
|
||||
ModalWideNavigationRail does; the adaptive rail does below lg, and takes no room below sm. */
|
||||
[data-navigation-rail='modal'] {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
@media (width < 64rem) {
|
||||
[data-navigation-rail='adaptive'] {
|
||||
width: 6rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 40rem) {
|
||||
[data-navigation-rail='adaptive'] {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail-panel] {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100dvh;
|
||||
/* Clip, not hide: no scroll container, so the destinations below can still scroll and
|
||||
nothing sticky breaks. What only an expanded rail draws — a label, the brand — is drawn
|
||||
at once when the rail expands, while the width is still growing; the clip keeps it
|
||||
from spilling over the page for those frames. */
|
||||
overflow-x: clip;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
background-color: var(--md-sys-color-surface);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
transition:
|
||||
width var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast),
|
||||
background-color var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-effects-default);
|
||||
}
|
||||
|
||||
/* Open: expanded over a scrim, in surface-container with a large corner at its inner edge. */
|
||||
[data-navigation-rail][data-open] > [data-navigation-rail-panel] {
|
||||
position: fixed;
|
||||
inset-block: 0;
|
||||
inset-inline-start: 0;
|
||||
z-index: 50;
|
||||
width: var(--navigation-rail-expanded-width);
|
||||
max-width: calc(100vw - 3.5rem);
|
||||
height: 100dvh;
|
||||
max-height: none;
|
||||
border-start-end-radius: var(--md-sys-shape-corner-lg);
|
||||
border-end-end-radius: var(--md-sys-shape-corner-lg);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
box-shadow: var(--md-sys-elevation-2);
|
||||
}
|
||||
|
||||
/* Below sm there is no collapsed rail to grow out of: the open rail slides in from the edge,
|
||||
on emphasized decelerate rather than a spring, which would overshoot and open a gap. */
|
||||
@media (width < 40rem) {
|
||||
[data-navigation-rail='adaptive'] > [data-navigation-rail-panel] {
|
||||
position: fixed;
|
||||
inset-block: 0;
|
||||
inset-inline-start: 0;
|
||||
z-index: 50;
|
||||
display: none;
|
||||
width: var(--navigation-rail-expanded-width);
|
||||
max-width: calc(100vw - 3.5rem);
|
||||
height: 100dvh;
|
||||
max-height: none;
|
||||
border-start-end-radius: var(--md-sys-shape-corner-lg);
|
||||
border-end-end-radius: var(--md-sys-shape-corner-lg);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
box-shadow: var(--md-sys-elevation-2);
|
||||
translate: -100% 0;
|
||||
transition:
|
||||
translate var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-easing-emphasized-accelerate),
|
||||
display var(--md-sys-motion-effects-default-duration) allow-discrete;
|
||||
|
||||
&:dir(rtl) {
|
||||
translate: 100% 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail='adaptive'][data-open] > [data-navigation-rail-panel] {
|
||||
display: flex;
|
||||
translate: 0 0;
|
||||
transition:
|
||||
translate var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-easing-emphasized-decelerate),
|
||||
display var(--md-sys-motion-spatial-default-duration) allow-discrete;
|
||||
|
||||
@starting-style {
|
||||
translate: -100% 0;
|
||||
}
|
||||
|
||||
&:dir(rtl) {
|
||||
translate: 0 0;
|
||||
|
||||
@starting-style {
|
||||
translate: 100% 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail-scrim] {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 40;
|
||||
display: none;
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent);
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-effects-default),
|
||||
display var(--md-sys-motion-effects-default-duration) allow-discrete;
|
||||
}
|
||||
|
||||
[data-navigation-rail][data-open] > [data-navigation-rail-scrim] {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
|
||||
@starting-style {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 44px above the header (TopSpace), 40px under it (HeaderSpaceMinimum) — 32 here and 8 as
|
||||
the destinations' own padding, which keeps the first item's focus ring inside the scroller. */
|
||||
[data-navigation-rail-header] {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding-top: calc(env(safe-area-inset-top) + 2.75rem);
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
[data-navigation-rail-panel] > [data-navigation-rail-destinations]:first-child {
|
||||
padding-top: calc(env(safe-area-inset-top) + 2.75rem);
|
||||
}
|
||||
|
||||
[data-navigation-rail-destinations] {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding-block: 0.5rem;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
[data-navigation-rail-destinations],
|
||||
[data-navigation-rail-section],
|
||||
[data-navigation-rail-footer] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@variant rail-collapsed {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail-footer] {
|
||||
flex-shrink: 0;
|
||||
padding-block: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
[data-navigation-rail-heading] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 3rem;
|
||||
padding-inline: 2.25rem 1.25rem;
|
||||
overflow: hidden;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-title-sm);
|
||||
letter-spacing: var(--md-sys-typescale-title-sm-tracking);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@variant rail-collapsed {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail-section]:not(:first-child) {
|
||||
@variant rail-collapsed {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Expanded: a 56px full-width pill, icon and label 8px apart, label-large. */
|
||||
[data-navigation-rail-item] {
|
||||
--navigation-layer: 0;
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
height: 3.5rem;
|
||||
margin-inline: 1.25rem;
|
||||
padding-inline: 1rem;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
text-align: start;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
/* Collapsed: the icon in its 56×32 indicator over a label-medium label, 64px tall. */
|
||||
@variant rail-collapsed {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
height: auto;
|
||||
min-height: 4rem;
|
||||
margin-inline: 0;
|
||||
padding: 0.375rem 0.25rem;
|
||||
border-radius: 0;
|
||||
font: var(--md-sys-typescale-label-md);
|
||||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail-item] [data-navigation-label] {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@variant rail-collapsed {
|
||||
flex: none;
|
||||
display: -webkit-box;
|
||||
max-width: 100%;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
text-align: center;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail-item][data-active] {
|
||||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
|
||||
@variant rail-collapsed {
|
||||
background-image: none;
|
||||
color: var(--md-sys-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail-item] [data-navigation-indicator] {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
|
||||
@variant rail-collapsed {
|
||||
width: 3.5rem;
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- both: indicator, states */
|
||||
|
||||
[data-navigation-bar-item] [data-navigation-indicator] {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3.5rem;
|
||||
height: 2rem;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
}
|
||||
|
||||
[data-navigation-bar-item][data-active] [data-navigation-indicator] {
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
@container (width < 37.5rem) {
|
||||
[data-navigation-bar-item][data-active] [data-navigation-indicator] {
|
||||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail-item][data-active] [data-navigation-indicator] {
|
||||
@variant rail-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);
|
||||
}
|
||||
}
|
||||
|
||||
/* The active indicator is a secondary-container fill painted as a background image, so it can
|
||||
grow out of its centre without stretching the icon: when a page arrives through
|
||||
wire:navigate, resources/js/navigation.js starts it at zero width for a moment, and it
|
||||
springs open on the default spatial spring, as Compose's indicator does when the selection
|
||||
changes (NavigationItem.kt). A full page load draws it at once; reduced motion zeroes it. */
|
||||
[data-navigation-bar-item] :is([data-navigation-indicator], [data-navigation-pill]),
|
||||
[data-navigation-rail-item],
|
||||
[data-navigation-rail-item] [data-navigation-indicator] {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
transition: background-size var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default);
|
||||
}
|
||||
|
||||
/* The state layer covers only the indicator: the pill when the indicator holds the label
|
||||
too, the 56×32 shape when it holds the icon alone. Hover only where a pointer can hover. */
|
||||
@media (hover: hover) {
|
||||
:is([data-navigation-bar-item], [data-navigation-rail-item]):hover {
|
||||
--navigation-layer: 0.08;
|
||||
}
|
||||
}
|
||||
|
||||
:is([data-navigation-bar-item], [data-navigation-rail-item]):is(:focus-visible, :active) {
|
||||
--navigation-layer: 0.1;
|
||||
}
|
||||
|
||||
[data-navigation-bar-item] [data-navigation-indicator]::before,
|
||||
[data-navigation-bar-item] [data-navigation-pill]::before,
|
||||
[data-navigation-rail-item] [data-navigation-indicator]::before,
|
||||
[data-navigation-rail-item]::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
border-radius: inherit;
|
||||
background-color: var(--md-sys-color-on-surface);
|
||||
opacity: var(--navigation-layer);
|
||||
pointer-events: none;
|
||||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
@container (width < 37.5rem) {
|
||||
[data-navigation-bar-item] [data-navigation-pill]::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@container (width >= 37.5rem) {
|
||||
[data-navigation-bar-item] [data-navigation-indicator]::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* The rail item's layer is on the item while expanded and on its indicator while collapsed. A
|
||||
variant cannot follow a pseudo-element in a selector, so the item hands the layer on through
|
||||
variables; each rule below outweighs its line in the list above. */
|
||||
[data-navigation-rail-item] {
|
||||
--navigation-item-layer: var(--navigation-layer);
|
||||
--navigation-indicator-layer: 0;
|
||||
|
||||
@variant rail-collapsed {
|
||||
--navigation-item-layer: 0;
|
||||
--navigation-indicator-layer: var(--navigation-layer);
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail-item][data-navigation-rail-item]::before {
|
||||
background-color: var(--md-sys-color-on-secondary-container);
|
||||
opacity: var(--navigation-item-layer);
|
||||
}
|
||||
|
||||
[data-navigation-rail-item][data-navigation-rail-item] [data-navigation-indicator]::before {
|
||||
background-color: var(--md-sys-color-on-secondary-container);
|
||||
opacity: var(--navigation-indicator-layer);
|
||||
}
|
||||
|
||||
/* M3's focus indicator, 3px of secondary 2px out, around the same shape. */
|
||||
@container (width < 37.5rem) {
|
||||
[data-navigation-bar-item]:focus-visible [data-navigation-indicator] {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@container (width >= 37.5rem) {
|
||||
[data-navigation-bar-item]:focus-visible [data-navigation-pill] {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail-item]:focus-visible {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
|
||||
@variant rail-collapsed {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail-item]:focus-visible [data-navigation-indicator] {
|
||||
@variant rail-collapsed {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* The paginators, Laravel's and Livewire's, drawn in M3 (resources/views/pagination/**): named
|
||||
* `material`/`simple-material`, put in front of the frameworks' own namespaces and set as
|
||||
* Laravel's default view (`Paginator::defaultView()`/`defaultSimpleView()`) and, where Livewire
|
||||
* still reads its own default, as its `pagination_theme`.
|
||||
*
|
||||
* The numbered paginator is a row: on its start, what the page holds in body-small on-surface-variant
|
||||
* — "21–30 of 95", or "Page 3 of 10" below `medium` (600px), where the page numbers give way and only
|
||||
* previous and next stay — and on its end the steps, 4px apart. A step is a 40px circle in label-large
|
||||
* with tabular figures, M3's icon-button geometry: a live one (a link or a button) in on-surface,
|
||||
* rendering the foundation's `md-state-layer`, `md-focus-ring` and `md-touch-target`
|
||||
* (foundation/interaction.css) to reach the 48px target; the current page is the selected state,
|
||||
* secondary-container, never the action colour — it is where you are, not what to do next; an
|
||||
* unreachable one (`aria-disabled`) is on-surface at 38%. The chevrons mirror in a right-to-left page.
|
||||
* The simple paginator is the same row holding two outlined buttons.
|
||||
*
|
||||
* [data-md-pagination] the <nav>
|
||||
* [data-md-pagination-summary] [data-md-pagination-compact], [data-md-pagination-range]
|
||||
* [data-md-pagination-steps]
|
||||
* [data-md-pagination-step] aria-current="page", aria-disabled="true";
|
||||
* [data-md-pagination-page] is a number, hidden below 600px
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './button.css';
|
||||
@import './icon.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-pagination] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--md-sys-measurement-space200);
|
||||
}
|
||||
|
||||
[data-md-pagination-summary] {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-sm);
|
||||
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-pagination-steps] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--md-sys-measurement-space50);
|
||||
}
|
||||
|
||||
[data-md-pagination-step] {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: var(--md-sys-measurement-space500);
|
||||
height: var(--md-sys-measurement-space500);
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
:is(a, button)[data-md-pagination-step] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
[data-md-pagination-step][aria-current='page'] {
|
||||
background-color: var(--md-sys-color-secondary-container);
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
[data-md-pagination-step][aria-disabled='true'] {
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
[data-md-pagination-page],
|
||||
[data-md-pagination-range] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 600px) {
|
||||
[data-md-pagination-compact] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* <x-password>: a text field with M3's trailing eye (resources/views/components/password.blade.php).
|
||||
*
|
||||
* Nothing of its own to draw: the chrome and the eye, a trailing icon button, are the field's
|
||||
* (components/field.css), and the eye's two glyphs are <x-icon>s. The field's root carries
|
||||
* `data-md-password`.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './field.css';
|
||||
@import './icon.css';
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* <x-progress>: M3 Expressive's progress indicator, drawn on the server for its first frame and
|
||||
* frame by frame in the browser after.
|
||||
*
|
||||
* ProgressIndicatorTokens, LinearProgressIndicatorTokens, CircularProgressIndicatorTokens (androidx
|
||||
* Compose Material 3, Apache-2.0): 4px stroke (`data-md-thick` 8px), linear 4px tall (8px thick,
|
||||
* 10px `data-md-wavy`, 14px wavy thick — the container grows by the extra stroke, keeping the
|
||||
* wave's own height put), circular 40px (44px thick, 48px wavy, 52px wavy thick). `data-md-color`
|
||||
* is the active indicator's `color` and the track's `stroke`; the track is secondary-container for
|
||||
* primary and secondary, and the colour's own container otherwise.
|
||||
*
|
||||
* Linear mirrors in a right-to-left document unconditionally — M3's own rule for this indicator,
|
||||
* not the `mirror-rtl` a caller opts an icon into. The sizing here is a default only: an
|
||||
* application's own unlayered class always outranks `material.components`, so an `inline-size` or
|
||||
* `block-size` of its own narrows or grows the indicator.
|
||||
*
|
||||
* resources/js/progress.js reads and writes `data-md-value`, `data-md-max`, `data-md-circular`,
|
||||
* `data-md-wavy` and `data-md-thick` — ProgressIndicator.kt, WavyProgressIndicator.kt and its
|
||||
* Linear/CircularWavyProgressModifiers, ported at commit 27cf9a7d5788aa0f5f2d8b6699ce279560daf326
|
||||
* — and draws every frame straight onto the SVG's own attributes; nothing here reads a class or
|
||||
* runs a transition.
|
||||
*/
|
||||
|
||||
@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-color, var(--md-sys-color-primary));
|
||||
stroke: var(--md-container, var(--md-sys-color-secondary-container));
|
||||
|
||||
& > svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-progress]:not([data-md-circular]) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
[data-md-progress]:not([data-md-circular]):is([dir='rtl'], [dir='rtl'] *) {
|
||||
rotate: 180deg;
|
||||
}
|
||||
|
||||
[data-md-progress]:not([data-md-circular])[data-md-thick] {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
[data-md-progress]:not([data-md-circular])[data-md-wavy] {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
[data-md-progress]:not([data-md-circular])[data-md-wavy][data-md-thick] {
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
[data-md-progress][data-md-circular] {
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
vertical-align: middle;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
[data-md-progress][data-md-circular][data-md-thick] {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
[data-md-progress][data-md-circular][data-md-wavy] {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
[data-md-progress][data-md-circular][data-md-wavy][data-md-thick] {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* <x-radio>: M3's radio buttons (resources/views/components/radio.blade.php), on the row the
|
||||
* selection controls share (components/selection.css).
|
||||
*
|
||||
* RadioButtonTokens (androidx Compose Material 3, Apache-2.0): a 20px ring with a 2px
|
||||
* on-surface-variant outline, primary once chosen, and a 10px primary dot that grows in on the fast
|
||||
* spatial spring; the state layer a 40px circle around it. With the server's error the ring and
|
||||
* the dot are error. Disabled, both are on-surface at 38%. Beside a hint the ring drops 2px, to sit
|
||||
* on the label's first line.
|
||||
*
|
||||
* The group is a <fieldset> whose legend (label-large, on-surface-variant, 12px above the options)
|
||||
* is the question. The options stack 16px apart, as M3 lays radio buttons out; `data-md-inline`
|
||||
* lays them in a wrapping row 24px apart from `medium` (600px), and compact always stacks. The
|
||||
* hint, or the errors in its place, sits 8px under the options.
|
||||
*
|
||||
* [data-md-radio] the <fieldset>; `class` and `style` land here; data-md-inline
|
||||
* [data-md-radio-legend]
|
||||
* [data-md-radio-options]
|
||||
* [data-md-selection-row]
|
||||
* [data-md-radio-button] the ring: input, [data-md-radio-dot]
|
||||
* [data-md-selection-support] the hint, or the errors
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './selection.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-radio] {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-md-radio-legend] {
|
||||
margin-bottom: 12px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-radio-options] {
|
||||
display: grid;
|
||||
gap: var(--md-sys-measurement-space200);
|
||||
}
|
||||
|
||||
/* M3 stacks radio buttons and cautions against a row at any width; `inline` is for two or three
|
||||
short labels, and only from medium. */
|
||||
@media (width >= 600px) {
|
||||
[data-md-radio][data-md-inline] [data-md-radio-options] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
column-gap: var(--md-sys-measurement-space300);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-radio] [data-md-selection-support] {
|
||||
margin-top: var(--md-sys-measurement-space100);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-sm);
|
||||
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-radio] [data-md-selection-support][role='alert'] {
|
||||
color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
[data-md-radio-button] {
|
||||
position: relative;
|
||||
display: grid;
|
||||
flex: none;
|
||||
place-items: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
[data-md-radio-button] > * {
|
||||
grid-area: 1 / 1;
|
||||
}
|
||||
|
||||
[data-md-selection-row]:has([data-md-selection-hint]) [data-md-radio-button] {
|
||||
margin-top: var(--md-sys-measurement-space25);
|
||||
}
|
||||
|
||||
[data-md-radio-button]::before {
|
||||
inset: -10px;
|
||||
}
|
||||
|
||||
[data-md-radio-button]:has(input:focus-visible)::before {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
}
|
||||
|
||||
[data-md-radio-button] input {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--md-sys-color-on-surface-variant);
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
}
|
||||
|
||||
[data-md-radio-button] [data-md-radio-dot] {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: var(--md-sys-color-primary);
|
||||
scale: 0;
|
||||
pointer-events: none;
|
||||
transition: scale var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast);
|
||||
}
|
||||
|
||||
[data-md-radio-button] input:checked {
|
||||
border-color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
[data-md-radio-button] input:checked ~ [data-md-radio-dot] {
|
||||
scale: 1;
|
||||
}
|
||||
|
||||
[data-md-selection-row][data-md-invalid] [data-md-radio-button] input {
|
||||
border-color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
[data-md-selection-row][data-md-invalid] [data-md-radio-button] [data-md-radio-dot] {
|
||||
background-color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
[data-md-radio-button] input:disabled {
|
||||
border-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
}
|
||||
|
||||
[data-md-radio-button] input:disabled ~ [data-md-radio-dot] {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* <x-rich-tooltip>: an M3 rich tooltip, a few lines of context for a control, with an optional
|
||||
* subhead and actions.
|
||||
*
|
||||
* RichTooltipTokens (androidx Compose Material 3, Apache-2.0): surface-container, the medium
|
||||
* corner, elevation 2, 312px wide at most, 16px either side, 12px above the subhead (or the text
|
||||
* when there is none) and 8px below it. Title-small subhead and body-medium text in
|
||||
* on-surface-variant, label-large actions in primary — the actions row pulls itself back 12px to
|
||||
* the text's edge, as the alert's actions row does (alert.css).
|
||||
*
|
||||
* Placed by CSS anchor positioning on `data-md-side`, corner-to-corner so the 312px bubble has
|
||||
* room to spread past a narrow trigger; `position-try-fallbacks` flips it when the window has no
|
||||
* room, same shape as tooltip.css's plain tooltip. It fades in and out on the fast effects spring:
|
||||
* `@starting-style` gives the fade in a start, and the fade out is its exit copy's
|
||||
* (`data-md-popover-exit`, resources/js/popover-exit.js), which `data-md-popover-closing` turns
|
||||
* 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` — none of
|
||||
* which this stylesheet draws.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@layer material.components {
|
||||
[data-md-rich-tooltip] {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
[data-md-rich-tooltip-bubble] {
|
||||
inset: auto;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
width: max-content;
|
||||
max-width: 312px;
|
||||
border-width: 0;
|
||||
border-radius: var(--md-sys-shape-corner-md);
|
||||
padding-inline: var(--md-sys-measurement-space200);
|
||||
padding-block: 12px var(--md-sys-measurement-space100);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
box-shadow: var(--md-sys-elevation-2);
|
||||
text-align: start;
|
||||
white-space: normal;
|
||||
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-rich-tooltip-bubble]:is([data-md-side='top'], [data-md-side='bottom']) {
|
||||
margin-block: var(--md-sys-measurement-space50);
|
||||
position-try-fallbacks: flip-block, flip-inline;
|
||||
}
|
||||
|
||||
[data-md-rich-tooltip-bubble]:is([data-md-side='left'], [data-md-side='right']) {
|
||||
margin-inline: var(--md-sys-measurement-space50);
|
||||
position-try-fallbacks: flip-inline, flip-block;
|
||||
}
|
||||
|
||||
[data-md-rich-tooltip-bubble][data-md-side='top'] {
|
||||
position-area: top span-right;
|
||||
}
|
||||
|
||||
[data-md-rich-tooltip-bubble][data-md-side='bottom'] {
|
||||
position-area: bottom span-right;
|
||||
}
|
||||
|
||||
[data-md-rich-tooltip-bubble][data-md-side='left'] {
|
||||
position-area: left span-bottom;
|
||||
}
|
||||
|
||||
[data-md-rich-tooltip-bubble][data-md-side='right'] {
|
||||
position-area: right span-bottom;
|
||||
}
|
||||
|
||||
[data-md-rich-tooltip-title] {
|
||||
display: block;
|
||||
margin-block-end: var(--md-sys-measurement-space50);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-title-sm);
|
||||
letter-spacing: var(--md-sys-typescale-title-sm-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-rich-tooltip-text] {
|
||||
display: block;
|
||||
padding-block-end: var(--md-sys-measurement-space100);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-md);
|
||||
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-rich-tooltip-actions] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
margin-inline-start: -12px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* <x-scheme-picker>: a choice of colour profile as a grid of swatch cards, native radios under
|
||||
* them so `wire:model`/`x-model` bind and the arrow keys move the choice.
|
||||
*
|
||||
* [data-md-scheme-picker] fieldset
|
||||
* [data-md-scheme-picker-legend] optional; the fixed label-large/on-surface-variant text
|
||||
* classes carry its type and ink
|
||||
* [data-md-scheme-picker-options] 2 columns below medium, 4 from it
|
||||
* [data-md-scheme-picker-option] the card; md-state-layer for hover and press, a hidden
|
||||
* radio inside read with :has() for focus and choice
|
||||
* [data-md-scheme-picker-swatches] the overlapping dots
|
||||
* [data-md-scheme-picker-swatch] one role's colour, light by default, the dark theme's
|
||||
* custom property from [data-theme='dark']
|
||||
* [data-md-scheme-picker-label] the profile's name, truncated
|
||||
* [data-md-scheme-picker-check] the chosen mark, shown with the radio
|
||||
* [data-md-scheme-picker-hint] the hint or, in error, the validation message; ink and
|
||||
* type are the fixed text classes on the view
|
||||
*
|
||||
* The chosen card takes secondary-container over a transparent edge, on the fast effects spring;
|
||||
* its focus ring reads off the radio inside with `:has()`, the one thing `md-focus-ring` cannot draw
|
||||
* since the real control is the input, not the label — the same refinement group.css keeps for its
|
||||
* own segments. `dark:` is `[data-theme='dark']` per an ancestor or the element itself
|
||||
* (tokens/theme.css's own `dark` custom variant, `&:where([data-theme='dark'], [data-theme='dark']
|
||||
* *)`, reduces to a plain descendant selector here since the swatch itself never carries
|
||||
* `data-theme`).
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-scheme-picker] {
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
[data-md-scheme-picker-legend] {
|
||||
margin-block-end: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-md-scheme-picker-options] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
|
||||
@media (width >= 600px) {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-scheme-picker-option] {
|
||||
display: flex;
|
||||
min-inline-size: 0;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
border-radius: var(--md-sys-shape-corner-lg);
|
||||
padding: 12px;
|
||||
background-color: var(--md-sys-color-surface-container-low);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
transition-property: background-color, border-color;
|
||||
transition-duration: var(--md-sys-motion-effects-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-effects-fast);
|
||||
|
||||
&:has(:checked) {
|
||||
border-color: transparent;
|
||||
background-color: var(--md-sys-color-secondary-container);
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
/* Focus lands on the input inside, not the label, so it is read with :has() — the one
|
||||
difference md-state-layer's own :focus-visible rule cannot draw (group.css's segments
|
||||
keep the same refinement, for the same reason). */
|
||||
&:has(:focus-visible) {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
|
||||
&::before {
|
||||
opacity: var(--md-sys-state-focus-state-layer-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
/* 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] {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-md-scheme-picker-swatches] > * + * {
|
||||
margin-inline-start: -6px;
|
||||
}
|
||||
|
||||
[data-md-scheme-picker-swatch] {
|
||||
inline-size: 20px;
|
||||
block-size: 20px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: var(--swatch-light);
|
||||
box-shadow: 0 0 0 2px var(--md-sys-color-surface-container-low);
|
||||
}
|
||||
|
||||
[data-theme='dark'] [data-md-scheme-picker-swatch] {
|
||||
background-color: var(--swatch-dark);
|
||||
}
|
||||
|
||||
[data-md-scheme-picker-option]:has(:checked) [data-md-scheme-picker-swatch] {
|
||||
box-shadow: 0 0 0 2px var(--md-sys-color-secondary-container);
|
||||
}
|
||||
|
||||
[data-md-scheme-picker-label] {
|
||||
inline-size: 100%;
|
||||
padding-inline-end: var(--md-sys-measurement-space300);
|
||||
}
|
||||
|
||||
[data-md-scheme-picker-check] {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
inset-inline-end: var(--md-sys-measurement-space100);
|
||||
top: var(--md-sys-measurement-space100);
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-md-scheme-picker-option]:has(:checked) [data-md-scheme-picker-check] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
[data-md-scheme-picker-hint] {
|
||||
margin-block-start: var(--md-sys-measurement-space50);
|
||||
}
|
||||
}
|
||||
@@ -4,93 +4,161 @@
|
||||
*
|
||||
* The bar is a 56px pill in surface-container-high: a leading search icon, the input in
|
||||
* body-large, a clear button once something is typed, and whatever the caller trails it with (an
|
||||
* avatar, an icon button). Focus opens the view. On a medium or wider window the view is docked:
|
||||
* the bar grows down into an extra-large-cornered container at elevation 3 that holds the
|
||||
* results. On a compact window it takes the whole screen, and the search icon turns into a back
|
||||
* arrow. `docked` keeps it docked at every width.
|
||||
* avatar, an icon button). Focus opens the view. On a medium or wider window the view is docked
|
||||
* over a scrim: the bar grows down into an extra-large-cornered container at elevation 3 that
|
||||
* holds the results. On a compact window it takes the whole screen, and the search icon turns into
|
||||
* a back arrow. `docked` keeps it docked at every width.
|
||||
*
|
||||
* [data-search] the root; data-open, data-full-screen
|
||||
* [data-search-bar] the pill, above the view
|
||||
* [data-search-leading], [data-search-input], [data-search-clear], [data-search-trailing]
|
||||
* [data-search-view] the container behind the bar
|
||||
* [data-search-results]
|
||||
* Widths are M3's: never wider than 720px, and the bar grows to that width while it is focused —
|
||||
* `--search-width` is its resting width (the room it is given, by default) and
|
||||
* `--search-open-width` the focused one, so a caller that wants M3's 360px resting bar sets
|
||||
* `--search-width: 360px` on a wrapper. The leading and trailing padding is 24px unfocused and
|
||||
* 16px focused, as the search specs table gives it.
|
||||
*
|
||||
* Hovered and focused buttons in the bar take M3's state-layer opacities (tokens/state.css), and the
|
||||
* docked scrim is M3's scrim role at 32%, which no system token names. The breakpoint that makes the
|
||||
* view full screen is decided in resources/js/search.js (`upTo('medium')`, 600px).
|
||||
*
|
||||
* [data-md-search] the root; data-md-open, data-md-full-screen, data-md-trigger
|
||||
* [data-md-search-scrim] over the page while the view is docked
|
||||
* [data-md-search-trigger] data-md-trigger="icon" only: the icon button that expands the view
|
||||
* [data-md-search-bar] the pill, above the view
|
||||
* [data-md-search-leading], [data-md-search-field] (the combobox around
|
||||
* [data-md-search-input]), [data-md-search-clear], [data-md-search-trailing]
|
||||
* [data-md-search-view] the container behind the bar
|
||||
* [data-md-search-suggestions] before the first keystroke
|
||||
* [data-md-search-results] once something is typed; [data-md-search-empty] when nothing is
|
||||
* [data-md-search-status] the polite live region that counts whichever list is on screen
|
||||
*/
|
||||
|
||||
@layer components {
|
||||
[data-search] {
|
||||
--search-height: 3.5rem;
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-search] {
|
||||
--search-height: 56px;
|
||||
/* M3: min 360px, max 720px, and wider while focused. */
|
||||
--search-width: 100%;
|
||||
--search-open-width: 720px;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-search][data-open] {
|
||||
/* Above the page while open, and while the view leaves: full screen through `data-md-full-screen`,
|
||||
docked through the `md-transition` class Alpine's `x-transition` carries for the length of the
|
||||
view's exit, so later positioned content on the page never covers the fading view. */
|
||||
[data-md-search]:is([data-md-open], [data-md-full-screen], :has(> [data-md-search-view].md-transition)) {
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
[data-search-bar] {
|
||||
/* Docked, the view opens over the page: M3 puts a scrim under it. Behind the bar and the view
|
||||
inside the root's own stacking context, above everything the page draws outside it. */
|
||||
[data-md-search-scrim] {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent);
|
||||
transition-property: opacity;
|
||||
transition-duration: var(--md-sys-motion-effects-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
[data-md-search-scrim] {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Closing (or turning full screen), the scrim fades back out while the view's `x-transition`
|
||||
holds its `display`: not `allow-discrete`, which Firefox does not honour for `display`. */
|
||||
[data-md-search]:is(:not([data-md-open]), [data-md-full-screen]) > [data-md-search-scrim] {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[data-md-search-bar] {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
gap: var(--md-sys-measurement-space50);
|
||||
width: min(100%, var(--search-width));
|
||||
height: var(--search-height);
|
||||
padding-inline: 0.25rem;
|
||||
padding-inline: 12px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: var(--md-sys-color-surface-container-high);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
cursor: text;
|
||||
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
transition-property: background-color, width, padding-inline;
|
||||
transition-duration: var(--md-sys-motion-effects-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
/* Focused: wider, and the padding drops from 24px to the contained style's 16px. */
|
||||
[data-md-search][data-md-open] [data-md-search-bar] {
|
||||
width: min(100%, var(--search-open-width));
|
||||
padding-inline: var(--md-sys-measurement-space50);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-search]:not([data-open]) [data-search-bar]:hover {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, var(--md-sys-color-surface-container-high));
|
||||
[data-md-search]:not([data-md-open]) [data-md-search-bar]:hover {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), var(--md-sys-color-surface-container-high));
|
||||
}
|
||||
}
|
||||
|
||||
[data-search][data-open] [data-search-bar] {
|
||||
[data-md-search][data-md-open] [data-md-search-bar] {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
[data-search-leading],
|
||||
[data-search-clear] {
|
||||
[data-md-search-leading],
|
||||
[data-md-search-clear] {
|
||||
display: grid;
|
||||
flex: none;
|
||||
place-items: center;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
width: var(--md-sys-measurement-space600);
|
||||
height: var(--md-sys-measurement-space600);
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
}
|
||||
|
||||
[data-search-leading] {
|
||||
[data-md-search-leading] {
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
[data-search-clear] {
|
||||
[data-md-search-clear] {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
[data-search-bar] button {
|
||||
[data-md-search-bar] button {
|
||||
outline: none;
|
||||
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-search-bar] button:hover {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) 8%, transparent);
|
||||
[data-md-search-bar] button:hover {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), transparent);
|
||||
}
|
||||
}
|
||||
|
||||
[data-search-bar] button:focus-visible {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) 10%, transparent);
|
||||
[data-md-search-bar] button:focus-visible {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-focus-state-layer-opacity) * 100%), transparent);
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
[data-search-input] {
|
||||
/* The combobox wrapper is only a role holder; it has to lay out as the input used to. */
|
||||
[data-md-search-field] {
|
||||
display: flex;
|
||||
flex: 1 1 0%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
padding-inline: 0.25rem;
|
||||
}
|
||||
|
||||
[data-md-search-input] {
|
||||
flex: 1 1 0%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
padding-inline: var(--md-sys-measurement-space50);
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
@@ -100,83 +168,160 @@
|
||||
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
||||
}
|
||||
|
||||
[data-search-input]::placeholder {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-search-input]::-webkit-search-cancel-button,
|
||||
[data-search-input]::-webkit-search-decoration {
|
||||
[data-md-search-input]::-webkit-search-cancel-button,
|
||||
[data-md-search-input]::-webkit-search-decoration {
|
||||
appearance: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-search-bar]:has([data-search-input]:placeholder-shown) [data-search-clear] {
|
||||
[data-md-search-bar]:has([data-md-search-input]:placeholder-shown) [data-md-search-clear] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-search-trailing] {
|
||||
[data-md-search-trailing] {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding-inline-end: 0.25rem;
|
||||
gap: var(--md-sys-measurement-space50);
|
||||
padding-inline-end: var(--md-sys-measurement-space50);
|
||||
}
|
||||
|
||||
[data-search][data-open] [data-search-trailing] {
|
||||
[data-md-search][data-md-open] [data-md-search-trailing] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-search-view] {
|
||||
[data-md-search-view] {
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: min(40rem, 70dvh);
|
||||
max-width: var(--search-open-width);
|
||||
max-height: min(640px, 70dvh);
|
||||
padding-top: var(--search-height);
|
||||
overflow: hidden;
|
||||
border-radius: var(--md-sys-shape-corner-xl);
|
||||
background-color: var(--md-sys-color-surface-container-high);
|
||||
box-shadow: var(--md-sys-elevation-3);
|
||||
transform-origin: top;
|
||||
transition-property: opacity, scale, display;
|
||||
transition-property: opacity, scale;
|
||||
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
||||
transition-behavior: allow-discrete;
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
[data-search-view] {
|
||||
[data-md-search-view] {
|
||||
opacity: 0;
|
||||
scale: 1 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
[data-search-results] {
|
||||
/* Closing, the view goes back the way it came, into the bar, while its `x-transition` holds
|
||||
`display` for the same duration. */
|
||||
[data-md-search]:not([data-md-open]) > [data-md-search-view] {
|
||||
opacity: 0;
|
||||
scale: 1 0.9;
|
||||
}
|
||||
|
||||
/* M3: the docked container is at least 240px tall — once there is something in it to be tall
|
||||
about; a search with nothing to show stays the height of its bar. */
|
||||
[data-md-search-view]:has([data-md-search-results], [data-md-search-suggestions]) {
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
/* No divider: that belongs to the divided style, which Expressive deprecates in favour of the
|
||||
contained one this file draws. The suggestions stand in the same place, before the first
|
||||
keystroke; only one of the two is ever on screen. */
|
||||
[data-md-search-results],
|
||||
[data-md-search-suggestions] {
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
border-top: 1px solid var(--md-sys-color-outline);
|
||||
padding-block: 0.5rem;
|
||||
padding-block: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
/* M3's second entry point: search as a secondary action, one icon button that expands into the
|
||||
full-screen view. The root keeps the button's 48px whether the view is open or not, so a
|
||||
toolbar does not shift under it, and the bar is the view's header rather than a resting bar. */
|
||||
[data-md-search][data-md-trigger='icon'] {
|
||||
flex: none;
|
||||
width: var(--md-sys-measurement-space600);
|
||||
height: var(--md-sys-measurement-space600);
|
||||
}
|
||||
|
||||
[data-md-search-trigger] {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: var(--md-sys-measurement-space600);
|
||||
height: var(--md-sys-measurement-space600);
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-md-search-trigger]:hover {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), transparent);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-search-trigger]:focus-visible {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-focus-state-layer-opacity) * 100%), transparent);
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
[data-md-search-trigger]:active {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-pressed-state-layer-opacity) * 100%), transparent);
|
||||
}
|
||||
|
||||
[data-md-search][data-md-trigger='icon']:not([data-md-open], [data-md-full-screen]) [data-md-search-bar] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Full screen, on a compact window. */
|
||||
[data-search][data-full-screen] [data-search-bar] {
|
||||
[data-md-search][data-md-full-screen] [data-md-search-bar] {
|
||||
position: fixed;
|
||||
inset: 0 0 auto;
|
||||
height: calc(4.5rem + env(safe-area-inset-top));
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-inline: 0.25rem;
|
||||
width: auto;
|
||||
height: calc(var(--md-sys-measurement-space900) + var(--material-safe-top, env(safe-area-inset-top)));
|
||||
padding-top: var(--material-safe-top, env(safe-area-inset-top));
|
||||
padding-inline: var(--md-sys-measurement-space50);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
[data-search][data-full-screen] [data-search-view] {
|
||||
/* Leaving full screen (`data-md-full-screen` outlives `data-md-open` by the view's exit,
|
||||
resources/js/search.js): the header bar fades with the view, on the view's own spring, rather
|
||||
than going back to its resting pill — or to nothing, behind the icon — on the first frame. */
|
||||
[data-md-search][data-md-full-screen]:not([data-md-open]) [data-md-search-bar] {
|
||||
opacity: 0;
|
||||
background-color: transparent;
|
||||
transition-property: opacity;
|
||||
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
||||
}
|
||||
|
||||
[data-md-search][data-md-full-screen] [data-md-search-view] {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
padding-top: calc(4.5rem + env(safe-area-inset-top));
|
||||
padding-top: calc(var(--md-sys-measurement-space900) + var(--material-safe-top, env(safe-area-inset-top)));
|
||||
/* The full-screen layout has its own container role, one step from the docked one. */
|
||||
background-color: var(--md-sys-color-surface-container-low);
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* What the caller says when nothing matches: body-medium in on-surface-variant, padded like a
|
||||
list row (16px across, 12px down). */
|
||||
[data-md-search-empty] {
|
||||
padding: 12px var(--md-sys-measurement-space200);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-md);
|
||||
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* <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; 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
|
||||
* [data-md-section-nav-trigger] 48px, an outlined field-like button
|
||||
* [data-md-section-nav-trigger-label] the current section's title, truncated
|
||||
* nav hidden below medium
|
||||
* [data-md-tabs-bar] tabs.css, reused whole
|
||||
*
|
||||
* 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"`, 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")
|
||||
* instead of widening its item — which `<x-tabs>`'s buttons, direct children of the bar, never
|
||||
* need. Its tabs render `<x-tabs>`'s label anatomy (`data-md-tab-label` around
|
||||
* `data-md-tab-text`), so that rule draws both. The picker's trigger is not one of M3's controls
|
||||
* — an outlined field-like button naming the current section, 48px tall to meet the target M3 asks
|
||||
* for everywhere, in body-large type (a text class on the view, `md-type-body-lg
|
||||
* md-ink md-text-start`, since it is prose on a plain button rather than a component's own type).
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './tabs.css';
|
||||
@import './icon.css';
|
||||
@import './menu.css';
|
||||
@import './menu-item.css';
|
||||
@import './badge.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-section-nav] {
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
[data-md-section-nav-picker] {
|
||||
@media (width >= 600px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-section-nav] > nav {
|
||||
@media (width < 600px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-section-nav-trigger] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
block-size: var(--md-sys-measurement-space600);
|
||||
inline-size: min(320px, calc(100vw - 32px));
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--md-sys-color-outline);
|
||||
border-radius: var(--md-sys-shape-corner-xs);
|
||||
padding-inline: var(--md-sys-measurement-space200);
|
||||
}
|
||||
|
||||
[data-md-section-nav-trigger-label] {
|
||||
min-inline-size: 0;
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
[data-md-section-nav] [data-md-tabs-bar] > li {
|
||||
display: flex;
|
||||
min-inline-size: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* <x-select>: the native <select> in the text field's chrome, drawn as M3's exposed dropdown menu
|
||||
* (resources/views/components/select.blade.php).
|
||||
*
|
||||
* The chrome is the field's (components/field.css); this file is what a <select> changes in it. The
|
||||
* select covers its whole field, so a press anywhere opens it and the list the browser anchors to
|
||||
* it is the field's width. Where the browser has the customizable select (`appearance:
|
||||
* base-select`), the closed select loses the button chrome that brings, the field reads as focused
|
||||
* while the list is open, and the arrow turns over. The open list itself is M3's menu, drawn by
|
||||
* components/menu.css.
|
||||
*
|
||||
* A value too long for the field ends before the arrow with an ellipsis. The native select cuts its
|
||||
* own text at its end padding; the customizable one draws the value in a box of its shadow tree
|
||||
* that no author style reaches, so the view gives it a `<button>` holding `<selectedcontent>`, the
|
||||
* browser's copy of the chosen option, and the ellipsis is drawn there.
|
||||
*
|
||||
* The field's root carries `data-md-select`.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './field.css';
|
||||
@import './icon.css';
|
||||
@import './menu.css';
|
||||
|
||||
@layer material.components {
|
||||
/* A select covers its whole field — out over the padding and the leading icon at
|
||||
the start, under the arrow to the end — and its own padding puts the value back
|
||||
where it was. So a press anywhere on the field opens it, and the list, which
|
||||
the browser anchors to the select, is the field's width (components/menu.css).
|
||||
A value longer than the field is cut at that padding, with an ellipsis, rather
|
||||
than running on under the arrow. */
|
||||
select[data-md-field-control] {
|
||||
--field-end: calc(var(--field-icon) + var(--field-gap) + var(--field-pad));
|
||||
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
margin-inline: calc(-1 * var(--field-start)) calc(-1 * var(--field-end));
|
||||
padding-inline: var(--field-start) var(--field-end);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
select[data-md-field-control]:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Where the browser has a customizable select, the closed select is its own
|
||||
button: without the border, padding and arrow that button brings, it is the
|
||||
same field as before. While its list is open the field reads as focused —
|
||||
said from `:open`, because focus inside the list is in the top layer — in
|
||||
the error colour when the field is in error, as a focused one is — and the
|
||||
arrow turns over, as the searchable choices' does. Both outweigh field.css's
|
||||
hover, so the pointer left on the field does not take the focus edge away. */
|
||||
@supports (appearance: base-select) {
|
||||
select[data-md-field-control] {
|
||||
appearance: base-select;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 0;
|
||||
padding-block: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* The customizable select's value is `<selectedcontent>`, the only part of the
|
||||
closed select an author style reaches: the box the browser draws otherwise
|
||||
takes neither `text-overflow` nor a width from the select, and ran on under
|
||||
the arrow. As the select's flex item it may shrink to the room left. */
|
||||
select[data-md-field-control] selectedcontent {
|
||||
flex: 1 1 0%;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
select[data-md-field-control]::picker-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-md-field-box]:has(select[data-md-field-control]:open) {
|
||||
--field-edge: var(--md-sys-color-primary);
|
||||
--field-ink: var(--md-sys-color-primary);
|
||||
|
||||
[data-md-field-outline] {
|
||||
border-width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-field][data-md-invalid] [data-md-field-box]:has(select[data-md-field-control]:open) {
|
||||
--field-edge: var(--md-sys-color-error);
|
||||
--field-ink: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
[data-md-field]:has(select[data-md-field-control]:open) [data-md-field-arrow] {
|
||||
rotate: 180deg;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,66 +1,90 @@
|
||||
/*
|
||||
* M3's selection controls: the checkbox, the radio button and the switch (CheckboxTokens,
|
||||
* RadioButtonTokens, SwitchTokens, androidx Compose Material 3, Apache-2.0).
|
||||
* What M3's three selection controls share: the checkbox (components/checkbox.css), the radio button
|
||||
* (components/radio.css) and the switch (components/toggle.css) — CheckboxTokens, RadioButtonTokens,
|
||||
* SwitchTokens, androidx Compose Material 3, Apache-2.0. Each control's stylesheet imports this one.
|
||||
*
|
||||
* Each is a real `<input>` with its appearance removed and drawn over, so focus, the keyboard
|
||||
* (Space; the arrow keys between radios), the form value, `wire:model` and what a screen reader
|
||||
* announces stay the browser's. The row around it is a `<label>`, so its text is pressable too.
|
||||
* Hooks are data attributes, not class names: `checkbox`, `radio` and `toggle` are daisyUI's, which
|
||||
* the design guard rejects.
|
||||
* announces stay the browser's. The row around it is a `<label>`, so its text is pressable too:
|
||||
*
|
||||
* [data-selection] the <label> row; `data-invalid` when the server has an error for it
|
||||
* [data-checkbox] the 18px box, its state layer a 40px circle around it
|
||||
* input, [data-check], [data-mixed]
|
||||
* [data-radio] the 20px ring and its 10px dot
|
||||
* input, [data-dot]
|
||||
* [data-switch] the 52×32 track; `data-icons` = "both" or "selected"
|
||||
* input, [data-handle] (with [data-on] and [data-off] icons)
|
||||
* [data-md-selection-row] the <label>: the control and its text, 16px apart; with a hint
|
||||
* the control aligns to the first line; `data-md-right` puts the
|
||||
* control at the end; `data-md-invalid` when the server has an error
|
||||
* [data-md-checkbox-box] | [data-md-radio-button] | [data-md-switch] the control
|
||||
* [data-md-selection-text] the label (body-large, on-surface) and the hint under it
|
||||
* [data-md-selection-label], [data-md-selection-hint]
|
||||
*
|
||||
* Every state layer is on-surface when unselected and primary when selected, 8% on hover (only
|
||||
* where a pointer can hover) and 10% on focus and press; the focus indicator is the package's
|
||||
* secondary ring. A disabled control is on-surface at 38% and has no state layer.
|
||||
* Every state layer is on-surface when unselected and primary when selected, at M3's hover opacity
|
||||
* (only where a pointer can hover) and its focus and press opacities (tokens/state.css); the focus
|
||||
* indicator is the package's secondary ring. A disabled control is on-surface at 38% and has no
|
||||
* state layer.
|
||||
*
|
||||
* The state layer is a ::before with no pointer events, so it is not a target: a labelled control
|
||||
* is pressed anywhere along its row, and a control with no label wears `md-touch-target`
|
||||
* (foundation/interaction.css) so its box catches presses over M3's 48px minimum.
|
||||
*/
|
||||
|
||||
@layer components {
|
||||
[data-selection] {
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@layer material.components {
|
||||
[data-md-selection-row] {
|
||||
/* The state layer's tint. */
|
||||
--selection-ink: var(--md-sys-color-on-surface);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--md-sys-measurement-space200);
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
[data-selection]:has(input:disabled) {
|
||||
[data-md-selection-row]:has([data-md-selection-hint]) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
[data-md-selection-row][data-md-right] {
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
[data-md-selection-row]:has(input:disabled) {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
[data-selection]:has(input:disabled) [data-selection-label] {
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||||
[data-md-selection-text] {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-checkbox],
|
||||
[data-radio] {
|
||||
position: relative;
|
||||
display: grid;
|
||||
flex: none;
|
||||
place-items: center;
|
||||
[data-md-selection-label] {
|
||||
display: block;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
font: var(--md-sys-typescale-body-lg);
|
||||
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-checkbox] > *,
|
||||
[data-radio] > * {
|
||||
grid-area: 1 / 1;
|
||||
[data-md-selection-hint] {
|
||||
display: block;
|
||||
margin-top: var(--md-sys-measurement-space25);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-sm);
|
||||
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-checkbox]::before,
|
||||
[data-radio]::before,
|
||||
[data-handle]::before {
|
||||
content: "";
|
||||
[data-md-selection-row]:has(input:disabled) [data-md-selection-label] {
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
}
|
||||
|
||||
:is([data-md-checkbox-box], [data-md-radio-button], [data-md-switch-handle])::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
pointer-events: none;
|
||||
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
[data-checkbox] input,
|
||||
[data-radio] input,
|
||||
[data-switch] input {
|
||||
:is([data-md-checkbox-box], [data-md-radio-button], [data-md-switch]) input {
|
||||
margin: 0;
|
||||
appearance: none;
|
||||
cursor: inherit;
|
||||
@@ -70,275 +94,26 @@
|
||||
transition-timing-function: var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
/* The state layer. `--selection-ink` is what it is tinted with. */
|
||||
[data-selection] {
|
||||
--selection-ink: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
[data-selection]:has(input:checked),
|
||||
[data-selection]:has(input:indeterminate) {
|
||||
[data-md-selection-row]:has(input:checked),
|
||||
[data-md-selection-row]:has(input:indeterminate) {
|
||||
--selection-ink: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
[data-selection][data-invalid] {
|
||||
[data-md-selection-row][data-md-invalid] {
|
||||
--selection-ink: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-selection]:hover:not(:has(input:disabled)) :is([data-checkbox], [data-radio], [data-handle])::before {
|
||||
background-color: color-mix(in srgb, var(--selection-ink) 8%, transparent);
|
||||
[data-md-selection-row]:hover:not(:has(input:disabled)) :is([data-md-checkbox-box], [data-md-radio-button], [data-md-switch-handle])::before {
|
||||
background-color: color-mix(in srgb, var(--selection-ink) calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), transparent);
|
||||
}
|
||||
}
|
||||
|
||||
[data-selection]:has(input:focus-visible) :is([data-checkbox], [data-radio], [data-handle])::before,
|
||||
[data-selection]:not(:has(input:disabled)):active :is([data-checkbox], [data-radio], [data-handle])::before {
|
||||
background-color: color-mix(in srgb, var(--selection-ink) 10%, transparent);
|
||||
[data-md-selection-row]:has(input:focus-visible) :is([data-md-checkbox-box], [data-md-radio-button], [data-md-switch-handle])::before {
|
||||
background-color: color-mix(in srgb, var(--selection-ink) calc(var(--md-sys-state-focus-state-layer-opacity) * 100%), transparent);
|
||||
}
|
||||
|
||||
[data-checkbox]:has(input:focus-visible)::before,
|
||||
[data-radio]:has(input:focus-visible)::before {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
}
|
||||
|
||||
/* ---- Checkbox ---------------------------------------------------------------------------- */
|
||||
|
||||
[data-checkbox] {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
}
|
||||
|
||||
[data-checkbox]::before {
|
||||
inset: -0.6875rem;
|
||||
}
|
||||
|
||||
[data-checkbox] input {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
border: 2px solid var(--md-sys-color-on-surface-variant);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
[data-checkbox] input:is(:checked, :indeterminate) {
|
||||
border-color: var(--md-sys-color-primary);
|
||||
background-color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
[data-checkbox] :is([data-check], [data-mixed]) {
|
||||
color: var(--md-sys-color-on-primary);
|
||||
opacity: 0;
|
||||
scale: 0.5;
|
||||
pointer-events: none;
|
||||
transition-property: opacity, scale;
|
||||
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
||||
}
|
||||
|
||||
[data-checkbox] input:checked:not(:indeterminate) ~ [data-check],
|
||||
[data-checkbox] input:indeterminate ~ [data-mixed] {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
|
||||
[data-selection][data-invalid] [data-checkbox] input {
|
||||
border-color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
[data-selection][data-invalid] [data-checkbox] input:is(:checked, :indeterminate) {
|
||||
background-color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
[data-selection][data-invalid] [data-checkbox] :is([data-check], [data-mixed]) {
|
||||
color: var(--md-sys-color-on-error);
|
||||
}
|
||||
|
||||
[data-checkbox] input:disabled {
|
||||
border-color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||||
}
|
||||
|
||||
[data-checkbox] input:disabled:is(:checked, :indeterminate) {
|
||||
border-color: transparent;
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||||
}
|
||||
|
||||
[data-checkbox] input:disabled ~ :is([data-check], [data-mixed]) {
|
||||
color: var(--md-sys-color-surface);
|
||||
}
|
||||
|
||||
/* ---- Radio button ------------------------------------------------------------------------ */
|
||||
|
||||
[data-radio] {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
[data-radio]::before {
|
||||
inset: -0.625rem;
|
||||
}
|
||||
|
||||
[data-radio] input {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border: 2px solid var(--md-sys-color-on-surface-variant);
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
}
|
||||
|
||||
[data-radio] [data-dot] {
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: var(--md-sys-color-primary);
|
||||
scale: 0;
|
||||
pointer-events: none;
|
||||
transition: scale var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast);
|
||||
}
|
||||
|
||||
[data-radio] input:checked {
|
||||
border-color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
[data-radio] input:checked ~ [data-dot] {
|
||||
scale: 1;
|
||||
}
|
||||
|
||||
[data-selection][data-invalid] [data-radio] input {
|
||||
border-color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
[data-selection][data-invalid] [data-radio] [data-dot] {
|
||||
background-color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
[data-radio] input:disabled {
|
||||
border-color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||||
}
|
||||
|
||||
[data-radio] input:disabled ~ [data-dot] {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||||
}
|
||||
|
||||
/* ---- Switch -------------------------------------------------------------------------------
|
||||
*
|
||||
* The handle's centre is 16px from the track's start unselected and 36px selected; it is 16px
|
||||
* across unselected, 24px with an icon or selected, and 28px while pressed. The positions below
|
||||
* are those centres less half the size, measured inside the track's 2px outline. */
|
||||
|
||||
[data-switch] {
|
||||
position: relative;
|
||||
display: grid;
|
||||
flex: none;
|
||||
width: 3.25rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
[data-switch] input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 2px solid var(--md-sys-color-outline);
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: var(--md-sys-color-surface-container-highest);
|
||||
}
|
||||
|
||||
[data-switch] input:focus-visible {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
[data-handle] {
|
||||
--handle: 1rem;
|
||||
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
inset-inline-start: calc(1rem - var(--handle) / 2);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: var(--handle);
|
||||
height: var(--handle);
|
||||
translate: 0 -50%;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: var(--md-sys-color-outline);
|
||||
color: var(--md-sys-color-surface-container-highest);
|
||||
pointer-events: none;
|
||||
transition-property: inset-inline-start, width, height, background-color;
|
||||
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
||||
}
|
||||
|
||||
[data-handle]::before {
|
||||
inset: 50% auto auto 50%;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
translate: -50% -50%;
|
||||
}
|
||||
|
||||
[data-handle] > svg {
|
||||
grid-area: 1 / 1;
|
||||
opacity: 0;
|
||||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
[data-switch][data-icons="both"] [data-handle] {
|
||||
--handle: 1.5rem;
|
||||
}
|
||||
|
||||
[data-switch][data-icons="both"] input:not(:checked) ~ [data-handle] [data-off],
|
||||
[data-switch][data-icons] input:checked ~ [data-handle] [data-on] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-switch] input:checked {
|
||||
border-color: var(--md-sys-color-primary);
|
||||
background-color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
[data-switch] input:checked ~ [data-handle] {
|
||||
--handle: 1.5rem;
|
||||
|
||||
inset-inline-start: calc(2.25rem - var(--handle) / 2);
|
||||
background-color: var(--md-sys-color-on-primary);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
[data-selection]:active [data-switch] input:not(:disabled) ~ [data-handle] {
|
||||
--handle: 1.75rem;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-selection]:hover [data-switch] input:not(:disabled) ~ [data-handle] {
|
||||
background-color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-selection]:hover [data-switch] input:checked:not(:disabled) ~ [data-handle] {
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
}
|
||||
}
|
||||
|
||||
[data-switch] input:focus-visible ~ [data-handle],
|
||||
[data-selection]:active [data-switch] input:not(:disabled) ~ [data-handle] {
|
||||
background-color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-switch] input:checked:focus-visible ~ [data-handle],
|
||||
[data-selection]:active [data-switch] input:checked:not(:disabled) ~ [data-handle] {
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
}
|
||||
|
||||
[data-switch] input:disabled {
|
||||
border-color: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent);
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-surface-container-highest) 12%, transparent);
|
||||
}
|
||||
|
||||
[data-switch] input:disabled ~ [data-handle] {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||||
color: color-mix(in srgb, var(--md-sys-color-surface-container-highest) 38%, transparent);
|
||||
}
|
||||
|
||||
[data-switch] input:disabled:checked {
|
||||
border-color: transparent;
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent);
|
||||
}
|
||||
|
||||
[data-switch] input:disabled:checked ~ [data-handle] {
|
||||
background-color: var(--md-sys-color-surface);
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||||
[data-md-selection-row]:not(:has(input:disabled)):active :is([data-md-checkbox-box], [data-md-radio-button], [data-md-switch-handle])::before {
|
||||
background-color: color-mix(in srgb, var(--selection-ink) calc(var(--md-sys-state-pressed-state-layer-opacity) * 100%), transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* <x-shape>: one of M3 Expressive's shapes, at `size` when one is given (`--md-shape-size`);
|
||||
* otherwise the caller's own CSS sizes it.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@layer material.components {
|
||||
[data-md-shape][style*='--md-shape-size'] {
|
||||
inline-size: var(--md-shape-size);
|
||||
block-size: var(--md-shape-size);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
/*
|
||||
* <x-slider>: M3 Expressive's slider (resources/views/components/slider.blade.php), drawn over native
|
||||
* range inputs by resources/js/slider.js, which places every part with inline `left`, `width` and
|
||||
* `border-radius`; this file gives the parts their size, colour and states.
|
||||
*
|
||||
* From androidx Compose Material 3 at commit 27cf9a7 (Slider.kt, SliderTokens.kt, Apache-2.0) and
|
||||
* M3's sliders/specs table: a 4px handle, 2px while pressed or focused and 6px shorter while its
|
||||
* focus ring shows (Material Components' m3_slider_focus_ring_thumb_height_decrease); 4px stop
|
||||
* indicators and ticks; the active track and the handle in the colour, the inactive track in its
|
||||
* container, and a stop, a tick or the inset icon in the "on" colour of the part of the track it
|
||||
* sits on. Disabled, the active parts are on-surface at 38% and the inactive track, and the ticks
|
||||
* over the active track, at 12% (tokens/state.css). The value label is M3's label container: 44px
|
||||
* tall and at least 48px wide in inverse-surface with label-large, 4px above the handle, growing in
|
||||
* on the fast effects spring while the handle is pressed or focused (`data-md-value-label="drag"`).
|
||||
*
|
||||
* Expressive's sizes, by `data-md-size`, as custom properties on the root:
|
||||
*
|
||||
* size track handle (and the slider across) inset icon
|
||||
* xs 16px 44px (the slider 48px) —
|
||||
* sm 24px 44px (48px) —
|
||||
* md 40px 52px 24px
|
||||
* lg 56px 68px 24px
|
||||
* xl 96px 108px 32px
|
||||
*
|
||||
* `data-md-orientation="vertical"` turns the same drawing a quarter inside a size container: the
|
||||
* slider is as wide as a horizontal one is tall and 192px long unless the root's height says
|
||||
* otherwise; the value label turns back so it reads across, beside the handle. A right-to-left page
|
||||
* mirrors a horizontal drawing and turns its label and icon back. Without script the inputs show as
|
||||
* native ranges in the colour.
|
||||
*
|
||||
* [data-md-slider] the root; data-md-size, data-md-color, data-md-value-label,
|
||||
* data-md-orientation, data-md-centered
|
||||
* [data-md-slider-label]
|
||||
* [data-md-slider-control] the element the pointer presses; holds the inputs
|
||||
* [data-md-slider-drawing]
|
||||
* [data-md-slider-track]
|
||||
* [data-md-slider-segment="start|active|end"], [data-md-slider-tick] (data-md-active),
|
||||
* [data-md-slider-stop="start|end"], [data-md-slider-icon] (data-md-active)
|
||||
* [data-md-slider-handle="start|end"] (data-md-pressed, data-md-focused)
|
||||
* [data-md-slider-thumb], [data-md-slider-value-anchor] > [data-md-slider-value]
|
||||
* [data-md-slider-input="start|end"]
|
||||
* [data-md-slider-hint] | [data-md-slider-errors]
|
||||
*/
|
||||
|
||||
@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-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;
|
||||
--slider-track: 16px;
|
||||
--slider-handle: 44px;
|
||||
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-md-slider][data-md-size='sm'] {
|
||||
--slider-track: 24px;
|
||||
}
|
||||
|
||||
[data-md-slider][data-md-size='md'] {
|
||||
--slider-across: 52px;
|
||||
--slider-track: 40px;
|
||||
--slider-handle: 52px;
|
||||
}
|
||||
|
||||
[data-md-slider][data-md-size='lg'] {
|
||||
--slider-across: 68px;
|
||||
--slider-track: 56px;
|
||||
--slider-handle: 68px;
|
||||
}
|
||||
|
||||
[data-md-slider][data-md-size='xl'] {
|
||||
--slider-across: 108px;
|
||||
--slider-track: 96px;
|
||||
--slider-handle: 108px;
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
[data-md-slider]:has([data-md-slider-input]:disabled) {
|
||||
--slider-active: var(--slider-disabled-active);
|
||||
--slider-inactive: var(--slider-disabled-inactive);
|
||||
--slider-on-active: var(--slider-disabled-inactive);
|
||||
--slider-on-inactive: var(--slider-disabled-active);
|
||||
}
|
||||
|
||||
[data-md-slider][data-md-orientation='vertical'] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
[data-md-slider-label] {
|
||||
display: block;
|
||||
margin-bottom: var(--md-sys-measurement-space100);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-slider-control] {
|
||||
position: relative;
|
||||
height: var(--slider-across);
|
||||
cursor: pointer;
|
||||
touch-action: pan-y;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
[data-md-slider-control]:has(:disabled) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Standing up: as wide as a lying slider is tall, 192px long unless the root says otherwise, and
|
||||
a size container, which lets the drawing be as long as this is tall. */
|
||||
[data-md-slider][data-md-orientation='vertical'] [data-md-slider-control] {
|
||||
flex: auto;
|
||||
width: var(--slider-across);
|
||||
height: 192px;
|
||||
min-height: 0;
|
||||
container-type: size;
|
||||
touch-action: pan-x;
|
||||
}
|
||||
|
||||
/* Room for a label that never hides: half the handle, 4px and the 44px pill — above the track
|
||||
lying down, beside it standing up. */
|
||||
[data-md-slider][data-md-value-label='always'] [data-md-slider-control] {
|
||||
margin-top: var(--md-sys-measurement-space600);
|
||||
}
|
||||
|
||||
[data-md-slider][data-md-value-label='always'][data-md-orientation='vertical'] [data-md-slider-control] {
|
||||
margin-top: 0;
|
||||
margin-inline-start: var(--md-sys-measurement-space600);
|
||||
}
|
||||
|
||||
[data-md-slider-drawing] {
|
||||
position: absolute;
|
||||
inset-block: 0;
|
||||
inset-inline: var(--md-sys-measurement-space25);
|
||||
pointer-events: none;
|
||||
|
||||
&:is([dir='rtl'], [dir='rtl'] *) {
|
||||
scale: -1 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* The same drawing, laid out as long as the slider is tall and turned a quarter anticlockwise so
|
||||
the value grows upwards. Everything inside it is placed as if the slider were lying down. */
|
||||
[data-md-slider][data-md-orientation='vertical'] [data-md-slider-drawing] {
|
||||
inset: auto;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: calc(100cqh - var(--md-sys-measurement-space50));
|
||||
height: 100cqw;
|
||||
translate: -50% -50%;
|
||||
rotate: -90deg;
|
||||
scale: none;
|
||||
}
|
||||
|
||||
[data-md-slider-track] {
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
top: 50%;
|
||||
height: var(--slider-track);
|
||||
translate: 0 -50%;
|
||||
}
|
||||
|
||||
[data-md-slider-segment] {
|
||||
position: absolute;
|
||||
inset-block: 0;
|
||||
background-color: var(--slider-inactive);
|
||||
}
|
||||
|
||||
[data-md-slider-segment='active'] {
|
||||
background-color: var(--slider-active);
|
||||
}
|
||||
|
||||
:is([data-md-slider-tick], [data-md-slider-stop]) {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
translate: -50% -50%;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: var(--slider-on-inactive);
|
||||
}
|
||||
|
||||
[data-md-slider-tick][data-md-active] {
|
||||
background-color: var(--slider-on-active);
|
||||
}
|
||||
|
||||
/* A disabled icon is on-surface at 38% wherever it sits. */
|
||||
[data-md-slider]:has([data-md-slider-input]:disabled) [data-md-slider-icon] {
|
||||
color: var(--slider-disabled-active);
|
||||
}
|
||||
|
||||
[data-md-slider-icon] {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
display: flex;
|
||||
translate: 0 -50%;
|
||||
color: var(--slider-on-inactive);
|
||||
|
||||
&[data-md-active] {
|
||||
color: var(--slider-on-active);
|
||||
}
|
||||
|
||||
&:is([dir='rtl'], [dir='rtl'] *) {
|
||||
scale: -1 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Turned back the quarter the drawing turns, so the glyph stands up. */
|
||||
[data-md-slider][data-md-orientation='vertical'] [data-md-slider-icon] {
|
||||
rotate: 90deg;
|
||||
scale: none;
|
||||
}
|
||||
|
||||
[data-md-slider-handle] {
|
||||
position: absolute;
|
||||
inset-block: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
[data-md-slider-thumb] {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: var(--slider-handle);
|
||||
translate: -50% -50%;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: var(--slider-active);
|
||||
outline-offset: 2px;
|
||||
transition-property: width, height;
|
||||
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
||||
}
|
||||
|
||||
:is([data-md-pressed], [data-md-focused]) > [data-md-slider-thumb] {
|
||||
width: 2px;
|
||||
}
|
||||
|
||||
[data-md-focused] > [data-md-slider-thumb] {
|
||||
height: calc(var(--slider-handle) - 6px);
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
}
|
||||
|
||||
[data-md-slider-value-anchor] {
|
||||
position: absolute;
|
||||
bottom: calc(50% + var(--slider-handle) / 2 + var(--md-sys-measurement-space50));
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
translate: -50% 0;
|
||||
|
||||
&:is([dir='rtl'], [dir='rtl'] *) {
|
||||
scale: -1 1;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-slider][data-md-orientation='vertical'] [data-md-slider-value-anchor] {
|
||||
scale: none;
|
||||
}
|
||||
|
||||
[data-md-slider-value] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: var(--md-sys-measurement-space600);
|
||||
height: 44px;
|
||||
padding-inline: var(--md-sys-measurement-space125);
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: var(--md-sys-color-inverse-surface);
|
||||
color: var(--md-sys-color-inverse-on-surface);
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
white-space: nowrap;
|
||||
transform-origin: bottom;
|
||||
transition-property: opacity, scale;
|
||||
transition-duration: var(--md-sys-motion-effects-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
/* Turned back the quarter the drawing turns: the pill lands beside the handle and reads across. */
|
||||
[data-md-slider][data-md-orientation='vertical'] [data-md-slider-value] {
|
||||
transform-origin: center;
|
||||
rotate: 90deg;
|
||||
}
|
||||
|
||||
[data-md-slider][data-md-value-label='drag'] [data-md-slider-value] {
|
||||
opacity: 0;
|
||||
scale: 0.75;
|
||||
}
|
||||
|
||||
[data-md-slider][data-md-value-label='drag'] :is([data-md-pressed], [data-md-focused]) [data-md-slider-value] {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
|
||||
[data-md-slider-input] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
accent-color: var(--slider-active);
|
||||
}
|
||||
|
||||
[data-md-slider-hint],
|
||||
[data-md-slider-errors] > p {
|
||||
margin-top: var(--md-sys-measurement-space50);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-sm);
|
||||
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-slider-errors] > p {
|
||||
color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
/* Without script there is no drawing: the inputs show as native ranges, one under the other. */
|
||||
@media (scripting: none) {
|
||||
:is([data-md-slider], [data-md-slider][data-md-orientation]) [data-md-slider-control] {
|
||||
height: auto;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
[data-md-slider-control] > :not(:last-child) {
|
||||
margin-block-end: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-md-slider-drawing] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-md-slider-input] {
|
||||
position: static;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
appearance: auto;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* <x-sort-header>: a column header that sorts the table (resources/views/components/sort-header.blade.php),
|
||||
* inside <x-table> (components/table.css), which draws the cell itself.
|
||||
*
|
||||
* The button is the header's own text, in the header cell's type and colour, with a 16px arrow 4px
|
||||
* after it; the corner is extra-small, for the focus ring the button wears (`md-focus-ring`), and
|
||||
* `md-touch-target` gives the one-line button M3's 48px target (foundation/interaction.css). The
|
||||
* sorted column (`data-md-active`) reads in on-surface with its arrow showing the direction; another
|
||||
* column turns on-surface only while hovered, and shows a faint upward arrow while hovered or
|
||||
* focused, at 60% of the ink — a hint of what pressing would do, not a state M3 names, fading on the
|
||||
* fast effects spring.
|
||||
*
|
||||
* [data-md-sort-header] the <th>; data-md-active, aria-sort
|
||||
* [data-md-sort-header-button]
|
||||
* [data-md-sort-header-arrow]
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-sort-header-button] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--md-sys-measurement-space50);
|
||||
border-radius: var(--md-sys-shape-corner-xs);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
[data-md-sort-header][data-md-active] [data-md-sort-header-button] {
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-md-sort-header-button]:hover {
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-sort-header-arrow] {
|
||||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
[data-md-sort-header]:not([data-md-active]) [data-md-sort-header-arrow] {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[data-md-sort-header]:not([data-md-active]) [data-md-sort-header-button]:focus-visible [data-md-sort-header-arrow] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-md-sort-header]:not([data-md-active]) [data-md-sort-header-button]:hover [data-md-sort-header-arrow] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* <x-split-button>: an action and a menu of its alternatives, as two `<x-button>`s 2px apart.
|
||||
*
|
||||
* The halves share one shape (SplitButtonXSmall…XLargeTokens, androidx Compose Material 3,
|
||||
* Apache-2.0): outer corners full — half the height, 16/20/28/48/68px, never 9999px, which would
|
||||
* scale the small inner corners away — and inner corners of 4/4/4/8/12px that *grow* under the
|
||||
* finger to 8/12/12/20/20px, the opposite of a connected button group. The trailing half turns
|
||||
* fully round while its menu is open (`aria-expanded`). `<x-button>`'s own corner rules carry no
|
||||
* specificity, so these win.
|
||||
*
|
||||
* At the two smallest sizes the leading half keeps less room on its inner side (10px at `xs`,
|
||||
* 12px at `sm`) and the trailing half is 48px wide. The trailing chevron has its own size,
|
||||
* 22/22/26/38/50px, which the icon-button table does not know about, and sits
|
||||
* `--md-split-button-nudge` (1/1/2/3/6px) towards the leading half so the pair reads as one
|
||||
* control: equal and opposite inline margins move it by exactly that much inside a centred flex
|
||||
* row, in whichever direction the writing mode runs. It turns over while the menu is open, on M3's
|
||||
* standard scheme rather than the expressive springs (the site: "the trailing button uses the
|
||||
* standard motion scheme, not expressive, when rotating").
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './button.css';
|
||||
@import './menu.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-split-button] {
|
||||
--md-split-button-inner: var(--md-sys-shape-corner-xs);
|
||||
--md-split-button-inner-pressed: var(--md-sys-shape-corner-md);
|
||||
--md-split-button-full: 20px;
|
||||
--md-split-button-icon: 22px;
|
||||
--md-split-button-nudge: 1px;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--md-sys-measurement-space25);
|
||||
}
|
||||
|
||||
[data-md-split-button][data-md-size='xs'] {
|
||||
--md-split-button-inner-pressed: var(--md-sys-shape-corner-sm);
|
||||
--md-split-button-full: 16px;
|
||||
}
|
||||
|
||||
[data-md-split-button][data-md-size='md'] {
|
||||
--md-split-button-full: 28px;
|
||||
--md-split-button-icon: 26px;
|
||||
--md-split-button-nudge: 2px;
|
||||
}
|
||||
|
||||
[data-md-split-button][data-md-size='lg'] {
|
||||
--md-split-button-inner: var(--md-sys-shape-corner-sm);
|
||||
--md-split-button-inner-pressed: var(--md-sys-shape-corner-lg-increased);
|
||||
--md-split-button-full: 48px;
|
||||
--md-split-button-icon: 38px;
|
||||
--md-split-button-nudge: 3px;
|
||||
}
|
||||
|
||||
[data-md-split-button][data-md-size='xl'] {
|
||||
--md-split-button-inner: var(--md-sys-shape-corner-md);
|
||||
--md-split-button-inner-pressed: var(--md-sys-shape-corner-lg-increased);
|
||||
--md-split-button-full: 68px;
|
||||
--md-split-button-icon: 50px;
|
||||
--md-split-button-nudge: 6px;
|
||||
}
|
||||
|
||||
[data-md-split-button] [data-md-split] {
|
||||
--md-split-button-corner: var(--md-split-button-inner);
|
||||
|
||||
border-radius: var(--md-split-button-corner);
|
||||
}
|
||||
|
||||
[data-md-split-button] [data-md-split]:active {
|
||||
--md-split-button-corner: var(--md-split-button-inner-pressed);
|
||||
}
|
||||
|
||||
[data-md-split-button] [data-md-split='trailing'][aria-expanded='true'] {
|
||||
--md-split-button-corner: var(--md-split-button-full);
|
||||
}
|
||||
|
||||
[data-md-split-button] [data-md-split='leading'] {
|
||||
border-start-start-radius: var(--md-split-button-full);
|
||||
border-end-start-radius: var(--md-split-button-full);
|
||||
}
|
||||
|
||||
[data-md-split-button] [data-md-split='trailing'] {
|
||||
border-start-end-radius: var(--md-split-button-full);
|
||||
border-end-end-radius: var(--md-split-button-full);
|
||||
}
|
||||
|
||||
[data-md-split-button][data-md-size='xs'] [data-md-split='leading'] {
|
||||
padding-inline-end: var(--md-sys-measurement-space125);
|
||||
}
|
||||
|
||||
[data-md-split-button][data-md-size='sm'] [data-md-split='leading'] {
|
||||
padding-inline-end: 12px;
|
||||
}
|
||||
|
||||
[data-md-split-button]:is([data-md-size='xs'], [data-md-size='sm']) [data-md-split='trailing'] {
|
||||
inline-size: var(--md-sys-measurement-space600);
|
||||
}
|
||||
|
||||
[data-md-split-button] [data-md-split='trailing'] [data-md-icon] {
|
||||
inline-size: var(--md-split-button-icon);
|
||||
block-size: var(--md-split-button-icon);
|
||||
margin-inline-start: calc(-1 * var(--md-split-button-nudge));
|
||||
margin-inline-end: var(--md-split-button-nudge);
|
||||
transition: rotate var(--md-sys-motion-duration-short) var(--md-sys-motion-easing-standard);
|
||||
}
|
||||
|
||||
[data-md-split-button] [data-md-split='trailing'][aria-expanded='true'] [data-md-icon] {
|
||||
rotate: 180deg;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* <x-stat>: a figure and what it counts — not an M3 component; drawn in M3's terms, a panel that
|
||||
* separates from the page in surface-container, a label-large title beside its icon, the value as
|
||||
* an emphasized-headline-md hero with tabular figures (so the panel never reflows as it counts),
|
||||
* and a body-small description. resources/js/figure.js counts the value up on first appearance
|
||||
* and on every change after, and rests under reduced motion.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-stat] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--md-sys-measurement-space50);
|
||||
border-radius: var(--md-sys-shape-corner-lg);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
padding: var(--md-sys-measurement-space200);
|
||||
}
|
||||
|
||||
[data-md-stat-header] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-stat-value] {
|
||||
font: var(--md-sys-typescale-emphasized-headline-md);
|
||||
letter-spacing: var(--md-sys-typescale-emphasized-headline-md-tracking);
|
||||
font-variation-settings: "ROND" 100;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
[data-md-stat-description] {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-sm);
|
||||
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-stat-extra] {
|
||||
padding-top: var(--md-sys-measurement-space100);
|
||||
}
|
||||
}
|
||||
@@ -2,21 +2,37 @@
|
||||
* Data tables: `<x-table>` (resources/views/components/table.blade.php), whose callers write plain
|
||||
* `<thead>`, `<tr>`, `<th>` and `<td>` inside it.
|
||||
*
|
||||
* So the styling is descendant selectors on the one attribute the component sets, all inside
|
||||
* `:where()` and `@layer components`: a caller's `text-end` or `whitespace-nowrap` on a cell always
|
||||
* wins. Header cells in title-small on-surface-variant over an outline-variant rule, body cells in
|
||||
* body-medium between faint rules. One density step tighter on a fine pointer — keyed on the
|
||||
* pointer, not the width, so a touch tablet in landscape keeps rows a finger can hit. A row that
|
||||
* opens something is `data-list-row` and answers a pointer as a list row does (components/list.css).
|
||||
* So the styling is descendant selectors on the one attribute the component sets, `data-md-table`,
|
||||
* with every cell and row selector inside `:where()`: a caller's alignment or wrapping on a cell
|
||||
* always wins, whatever layer it comes from. Header cells in title-small on-surface-variant over an
|
||||
* outline-variant rule, body cells in body-medium between outline-variant rules — the role itself,
|
||||
* never a fraction of it, since M3 reserves opacity for state layers and disabled. A selected row
|
||||
* (`data-md-selected`, or `aria-selected="true"` where the table is a grid, the only place ARIA
|
||||
* allows it on a row) is secondary-container. A row that opens something is `data-md-list-row`
|
||||
* and answers a pointer as a list row does (components/list-item.css, shared with `<x-card>` and
|
||||
* `<x-list-item>`); a selected one names its fill `--md-list-row-fill`, so that state layer is laid
|
||||
* over the secondary-container rather than in place of it.
|
||||
*
|
||||
* A row is 52px: 16px above and below a body-medium line, 12px beside each cell. Density is never
|
||||
* applied by itself — "don't apply density by default; offer an explicit density opt-in instead,
|
||||
* keeping opt-out targets at >= 48x48 CSS px" (docs/reference/m3/foundations-supplement.md
|
||||
* § Accessibility) — so `dense` (`data-md-dense`) is the caller's decision, and it is the caller's
|
||||
* to justify: a dense row is 36px (8px above and below). `data-md-size="xs"`, for a table inside a
|
||||
* panel inside a panel, is body-small with label-medium headers, 8px around each cell (32px rows),
|
||||
* and 4px above and below when dense (24px).
|
||||
*/
|
||||
|
||||
@layer components {
|
||||
/* `relative` makes the table the containing block for anything absolute inside it — an `sr-only`
|
||||
header label, a tooltip. Without it they escape the scroll box and widen a phone's layout
|
||||
viewport. */
|
||||
[data-table] {
|
||||
--cell-x: 0.75rem;
|
||||
--cell-y: 0.75rem;
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './list-item.css';
|
||||
|
||||
@layer material.components {
|
||||
/* `position: relative` makes the table the containing block for anything absolute inside it —
|
||||
a visually hidden header label, a tooltip. Without it they escape the scroll box and widen a
|
||||
phone's layout viewport. */
|
||||
[data-md-table] {
|
||||
--cell-x: 12px;
|
||||
--cell-y: var(--md-sys-measurement-space200);
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@@ -26,31 +42,29 @@
|
||||
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
||||
}
|
||||
|
||||
[data-table][data-size="xs"] {
|
||||
--cell-x: 0.5rem;
|
||||
--cell-y: 0.5rem;
|
||||
[data-md-table][data-md-size='xs'] {
|
||||
--cell-x: var(--md-sys-measurement-space100);
|
||||
--cell-y: var(--md-sys-measurement-space100);
|
||||
|
||||
font: var(--md-sys-typescale-body-sm);
|
||||
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
||||
}
|
||||
|
||||
@media (pointer: fine) {
|
||||
[data-table] {
|
||||
--cell-y: 0.5rem;
|
||||
}
|
||||
|
||||
[data-table][data-size="xs"] {
|
||||
--cell-y: 0.25rem;
|
||||
}
|
||||
[data-md-table][data-md-dense] {
|
||||
--cell-y: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-table] :where(th, td) {
|
||||
[data-md-table][data-md-size='xs'][data-md-dense] {
|
||||
--cell-y: var(--md-sys-measurement-space50);
|
||||
}
|
||||
|
||||
[data-md-table] :where(th, td) {
|
||||
padding: var(--cell-y) var(--cell-x);
|
||||
text-align: start;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
[data-table] :where(thead th) {
|
||||
[data-md-table] :where(thead th) {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-title-sm);
|
||||
letter-spacing: var(--md-sys-typescale-title-sm-tracking);
|
||||
@@ -58,21 +72,23 @@
|
||||
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
[data-table][data-size="xs"] :where(thead th) {
|
||||
[data-md-table][data-md-size='xs'] :where(thead th) {
|
||||
font: var(--md-sys-typescale-label-md);
|
||||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||||
}
|
||||
|
||||
[data-table] :where(tbody tr) {
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--md-sys-color-outline-variant) 60%, transparent);
|
||||
[data-md-table] :where(tbody tr) {
|
||||
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
[data-table] :where(tbody tr:last-child) {
|
||||
[data-md-table] :where(tbody tr:last-child) {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
[data-table] :where(tbody tr[aria-selected="true"]) {
|
||||
background-color: var(--md-sys-color-secondary-container);
|
||||
[data-md-table] :where(tbody tr:is([aria-selected='true'], [data-md-selected])) {
|
||||
--md-list-row-fill: var(--md-sys-color-secondary-container);
|
||||
|
||||
background-color: var(--md-list-row-fill);
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,125 +1,207 @@
|
||||
/*
|
||||
* M3's tabs (PrimaryNavigationTabTokens, SecondaryNavigationTabTokens, androidx Compose Material 3,
|
||||
* Apache-2.0): a 48px tablist on the surface over an outline-variant divider (64px when a primary
|
||||
* tab stacks its icon over its label), title-small labels in on-surface-variant, the chosen tab's
|
||||
* in primary (primary tabs) or on-surface (secondary tabs).
|
||||
* `<x-tabs>`, `<x-tab>`: M3's tabs (PrimaryNavigationTabTokens.kt, SecondaryNavigationTabTokens.kt,
|
||||
* androidx Compose Material 3, Apache-2.0; docs/reference/m3/components-navigation-selection-inputs.md
|
||||
* § Tabs) — a tablist of tabs at least 48px tall on the surface over an outline-variant divider
|
||||
* (64px when a primary tab stacks its icon over its label), title-small labels in on-surface-variant, the chosen tab's in
|
||||
* primary (primary tabs) or on-surface (secondary tabs).
|
||||
*
|
||||
* [data-md-tabs] min-width: 0
|
||||
* [data-md-tabs-bar] role="tablist"; data-md-variant, data-md-stacked,
|
||||
* data-md-scrollable
|
||||
* [data-md-tab] role="tab"
|
||||
* [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
|
||||
*
|
||||
* "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
|
||||
* tab shares the row equally, so its label keeps one row while it fits its share, then wraps to a
|
||||
* second line and truncates that one with an ellipsis. 48px (64px stacked) is the tab's minimum,
|
||||
* not its height: the tab grows to hold the second line, and the bar's flex row stretches every
|
||||
* tab to the tallest, so the tabs keep one height, each content box fills its tab, and every
|
||||
* indicator stays on the divider under its tab's content. A scrollable tab is as wide as its label
|
||||
* and keeps it on one row, the room M3 gives longer titles instead.
|
||||
*
|
||||
* 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), 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.
|
||||
* (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.
|
||||
*
|
||||
* The bar scrolls sideways, so it clips whatever leaves it — including the focus ring, which the
|
||||
* 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. 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.
|
||||
*/
|
||||
|
||||
@layer components {
|
||||
[data-tabs-bar] {
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
@import './badge.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-tabs] {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-md-tabs-bar] {
|
||||
--tabs-ring: 5px;
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
overscroll-behavior-x: contain;
|
||||
padding: var(--tabs-ring);
|
||||
scroll-padding-inline: var(--tabs-ring);
|
||||
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
/* "Offset the first scrollable tab 52dp from the leading edge so it's clear that more content
|
||||
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;
|
||||
}
|
||||
|
||||
/* A section nav's links sit in list items that share the width. */
|
||||
[data-tabs-bar] > li {
|
||||
[data-md-tabs-bar] > li {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
[data-tabs-bar] > li > [data-tab] {
|
||||
[data-md-tabs-bar] > li > [data-md-tab] {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
[data-tab] {
|
||||
[data-md-tab] {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1 1 0%;
|
||||
min-width: 5.625rem;
|
||||
height: 3rem;
|
||||
min-width: 90px;
|
||||
min-height: 48px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-inline: 1rem;
|
||||
padding-inline: var(--md-sys-measurement-space200);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
font: var(--md-sys-typescale-title-sm);
|
||||
letter-spacing: var(--md-sys-typescale-title-sm-tracking);
|
||||
white-space: nowrap;
|
||||
transition: color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
transition-property: color;
|
||||
transition-duration: var(--md-sys-motion-effects-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
[data-tabs-bar][data-scrollable] [data-tab] {
|
||||
[data-md-tabs-bar][data-md-scrollable] > li,
|
||||
[data-md-tabs-bar][data-md-scrollable] [data-md-tab] {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
[data-tabs-bar][data-stacked] [data-tab] {
|
||||
height: 4rem;
|
||||
[data-md-tabs-bar][data-md-stacked] [data-md-tab] {
|
||||
min-height: 64px;
|
||||
}
|
||||
|
||||
[data-tab]::before {
|
||||
content: "";
|
||||
/* The state layer covers the tab and the ring's room around it, so the wash meets the divider. */
|
||||
[data-md-tab]::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
inset-block: calc(-1 * var(--tabs-ring));
|
||||
inset-inline: 0;
|
||||
pointer-events: none;
|
||||
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
transition-property: background-color;
|
||||
transition-duration: var(--md-sys-motion-effects-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-tab]:hover {
|
||||
[data-md-tab]:hover {
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
[data-tab]:hover::before {
|
||||
[data-md-tab]:hover::before {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
[data-tab]:focus-visible::before,
|
||||
[data-tab]:active::before {
|
||||
[data-md-tab]:focus-visible::before,
|
||||
[data-md-tab]:active::before {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
|
||||
}
|
||||
|
||||
[data-tab]:focus-visible {
|
||||
[data-md-tab]:focus-visible {
|
||||
color: var(--md-sys-color-on-surface);
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: -3px;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
[data-tab][aria-selected="true"] {
|
||||
/* 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. */
|
||||
[data-md-tab]:is([aria-selected='true'], [aria-current='page']) {
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
[data-tabs-bar][data-variant="secondary"] [data-tab]:is([aria-selected="true"], [aria-current="page"]) {
|
||||
[data-md-tabs-bar][data-md-variant='secondary'] [data-md-tab]:is([aria-selected='true'], [aria-current='page']) {
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
[data-tab]:disabled {
|
||||
[data-md-tab]:disabled {
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
[data-tab]:disabled::before {
|
||||
[data-md-tab]:disabled::before {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
[data-tab-content] {
|
||||
/* Stretched rather than 100% tall, so it fills a tab that grew past its minimum too, and the
|
||||
primary indicator at its foot lands on the divider in every tab of the bar. */
|
||||
[data-md-tab-content] {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
height: 100%;
|
||||
min-width: 1.5rem;
|
||||
align-self: stretch;
|
||||
min-width: 24px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-tabs-bar][data-stacked] [data-tab-content] {
|
||||
[data-md-tabs-bar][data-md-stacked] [data-md-tab-content] {
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
gap: var(--md-sys-measurement-space25);
|
||||
}
|
||||
|
||||
[data-tab-indicator] {
|
||||
[data-md-tab-label] {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: var(--md-sys-measurement-space75);
|
||||
}
|
||||
|
||||
/* A fixed tab's label: one row while it fits, then a second, truncated (the M3 line above).
|
||||
The tab's own nowrap still holds the badge and a scrollable tab's label to one row. */
|
||||
[data-md-tabs-bar]:not([data-md-scrollable]) [data-md-tab-text] {
|
||||
display: -webkit-box;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
inset-inline: 0;
|
||||
bottom: 0;
|
||||
inset-inline: 2px;
|
||||
bottom: calc(-1 * var(--tabs-ring));
|
||||
height: 3px;
|
||||
border-radius: 3px 3px 0 0;
|
||||
background-color: var(--md-sys-color-primary);
|
||||
@@ -129,22 +211,29 @@
|
||||
}
|
||||
|
||||
/* A secondary tab's indicator spans the tab, not its content. */
|
||||
[data-tabs-bar][data-variant="secondary"] [data-tab-content] {
|
||||
[data-md-tabs-bar][data-md-variant='secondary'] [data-md-tab-content] {
|
||||
position: static;
|
||||
}
|
||||
|
||||
[data-tabs-bar][data-variant="secondary"] [data-tab-indicator] {
|
||||
[data-md-tabs-bar][data-md-variant='secondary'] [data-md-tab-indicator] {
|
||||
inset-inline: 0;
|
||||
height: 2px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
[data-tab]:is([aria-selected="true"], [aria-current="page"]) [data-tab-indicator] {
|
||||
[data-md-tab]:is([aria-selected='true'], [aria-current='page']) [data-md-tab-indicator] {
|
||||
opacity: 1;
|
||||
view-transition-name: var(--tabs-indicator);
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-group(*.material-tab-indicator) {
|
||||
animation-duration: var(--md-sys-motion-spatial-default-duration);
|
||||
animation-timing-function: var(--md-sys-motion-spatial-default);
|
||||
/* pt-6, before the rewrite: the panel's content stands clear of the bar above it. */
|
||||
[data-md-tab-panel] {
|
||||
padding-top: var(--md-sys-measurement-space300);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
::view-transition-group(*.material-tab-indicator) {
|
||||
animation-duration: var(--md-sys-motion-spatial-default-duration);
|
||||
animation-timing-function: var(--md-sys-motion-spatial-default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* <x-textarea>: the text field grown to several lines (resources/views/components/textarea.blade.php).
|
||||
*
|
||||
* The chrome is the field's (components/field.css); this file is what a <textarea> changes in it.
|
||||
* The browser's resize grip belongs in the field's corner, so the textarea reaches to just inside
|
||||
* the outline's end and bottom and its padding puts the text back. Its label rests on the first
|
||||
* line rather than in the middle of the box. With `data-md-autogrow` it grows with its text from
|
||||
* `--field-rows` lines to `--field-max-rows` (both set inline by the view) through
|
||||
* `field-sizing: content`, and resources/js/field.js sets the height where the browser cannot.
|
||||
* A line is body-large's 1.5rem, so the row arithmetic stays in rem with the text.
|
||||
*
|
||||
* The field's root carries `data-md-textarea`.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './field.css';
|
||||
|
||||
@layer material.components {
|
||||
/* The browser draws the resize grip in the textarea's own corner, so the
|
||||
textarea reaches to just inside the outline's end and bottom — the grip sits
|
||||
in the field's corner, not on the bottom outline a padding in from it — and
|
||||
its padding puts the text back where it was. */
|
||||
textarea[data-md-field-control] {
|
||||
--field-grip: var(--md-sys-measurement-space50);
|
||||
|
||||
/* The first half-line of the top offset is a margin, not padding: text scrolled up in a full
|
||||
textarea disappears under the edge instead of running through the label. */
|
||||
--textarea-clear: var(--md-sys-measurement-space100);
|
||||
--textarea-pad-top: calc((var(--field-height) - 1.5rem) / 2 - var(--textarea-clear));
|
||||
--textarea-pad-bottom: calc((var(--field-height) - 1.5rem) / 2 - var(--field-grip));
|
||||
|
||||
margin-block: var(--textarea-clear) var(--field-grip);
|
||||
margin-inline-end: calc(var(--field-grip) - var(--field-pad));
|
||||
padding-block: var(--textarea-pad-top) var(--textarea-pad-bottom);
|
||||
padding-inline-end: calc(var(--field-pad) - var(--field-grip));
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* `<x-textarea>` grows with what is typed, from `rows` lines up to `max-rows`. Where the browser
|
||||
cannot size a field to its content, resources/js/field.js sets the height instead. */
|
||||
textarea[data-md-field-control][data-md-autogrow] {
|
||||
field-sizing: content;
|
||||
min-block-size: calc(var(--field-rows, 3) * 1.5rem + var(--textarea-pad-top) + var(--textarea-pad-bottom));
|
||||
max-block-size: calc(var(--field-max-rows, 1000) * 1.5rem + var(--textarea-pad-top) + var(--textarea-pad-bottom));
|
||||
resize: none;
|
||||
}
|
||||
|
||||
/* A textarea's label rests on its first line, not in the middle of the box. */
|
||||
[data-md-field]:not([data-md-floated]):has([data-md-field-label]):has(textarea[data-md-field-control]:placeholder-shown):not(:focus-within) [data-md-field-label] {
|
||||
top: calc(var(--field-height) / 2);
|
||||
}
|
||||
|
||||
/* Filled, the label floats inside the box: the text starts under it, 24px down, and the
|
||||
bottom keeps the field's 8px. */
|
||||
[data-md-field][data-md-variant='filled']:has([data-md-field-label]) textarea[data-md-field-control] {
|
||||
--textarea-clear: var(--md-sys-measurement-space300);
|
||||
--textarea-pad-top: 0px;
|
||||
--textarea-pad-bottom: calc(var(--md-sys-measurement-space100) - var(--field-grip));
|
||||
|
||||
padding-block: var(--textarea-pad-top) var(--textarea-pad-bottom);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* <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 — 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, 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
|
||||
* 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
|
||||
* (button.css, `:active`).
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './group.css';
|
||||
@import './icon.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-theme-toggle='toggle'],
|
||||
[data-md-theme-toggle='cycle'] {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
inline-size: var(--md-sys-measurement-space500);
|
||||
block-size: var(--md-sys-measurement-space500);
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
cursor: pointer;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
transition: border-radius var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast);
|
||||
|
||||
&:active {
|
||||
border-radius: var(--md-sys-shape-corner-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,71 +5,111 @@
|
||||
* Sizes and roles are androidx Compose Material 3's (TimePickerTokens, TimeInputTokens, TimePicker.kt
|
||||
* and TimePickerDialog.kt at commit 27cf9a7d5788aa0f5f2d8b6699ce279560daf326, Apache-2.0):
|
||||
*
|
||||
* [data-timepicker-surface] surface-container-high, extra-large corner, elevation 3; 24px in
|
||||
* [data-timepicker-title] label-medium, on-surface-variant, 20px above the content
|
||||
* [data-timepicker-picker] the dial variant
|
||||
* [data-timepicker-display] the time selector: hour and minute boxes 96×80 (display-large,
|
||||
* [data-md-timepicker-surface] surface-container-high, extra-large corner, elevation 3; 24px in
|
||||
* [data-md-timepicker-title] label-medium, on-surface-variant, 20px above the content
|
||||
* [data-md-timepicker-picker] the dial variant
|
||||
* [data-md-timepicker-display] the time selector: hour and minute boxes 96×80 (display-large,
|
||||
* small corner; primary-container when selected, otherwise
|
||||
* surface-container-highest), a 24px separator, and the period
|
||||
* selector 52×80 beside them, 4px away
|
||||
* [data-timepicker-dial] 256px, surface-container-highest; numbers in body-large at radius
|
||||
* selector 52×80 beside them, 4px away. On a 24-hour clock
|
||||
* (data-md-cycle="24") there is no period selector and the boxes are
|
||||
* 114px, TimeSelector24HVerticalContainerWidth.
|
||||
* [data-md-timepicker-dial] 256px, surface-container-highest; numbers in body-large at radius
|
||||
* 101 (the 24-hour inner ring at 69), a primary selector — a 2px line,
|
||||
* an 8px centre and a 48px handle — with the number under the handle
|
||||
* in on-primary; 36px below the display, 24px above the actions
|
||||
* [data-timepicker-inputs] the input variant: fields 96×72 in display-medium, "Hour" and
|
||||
* [data-md-timepicker-inputs] the input variant: fields 96×72 in display-medium, "Hour" and
|
||||
* "Minute" (or the error) below in body-small, the period 52×72
|
||||
* [data-timepicker-actions] a 48px row: the mode toggle, then Cancel and OK
|
||||
* [data-md-timepicker-actions] a 48px row: the mode toggle, then Cancel and OK
|
||||
*
|
||||
* The period selector is Compose's current one (ComposeMaterial3Flags.isUpdatedTimepickerToggleEnabled,
|
||||
* on by default): two toggle buttons 4px apart, round and surface-container-lowest when off, a 12px
|
||||
* corner and primary-container with a bold label when on, not the outlined pair of the tokens.
|
||||
* The period selector keeps Compose's current shape (ComposeMaterial3Flags.isUpdatedTimepickerToggleEnabled,
|
||||
* on by default): two buttons 4px apart, round and surface-container-lowest when off, a 12px corner
|
||||
* when on, not the outlined pair of the tokens. The selected colour is M3's own tertiary-container
|
||||
* with a bold label (PeriodSelectorSelectedContainerColor), which keeps AM/PM apart from the
|
||||
* primary-container hour and minute boxes. They are radios, so the state is `aria-checked`.
|
||||
*
|
||||
* In a landscape window the dial variant lies on its side, as Compose's HorizontalTimePicker does
|
||||
* whenever the screen is wider than it is tall: the display with the period selector (216×38, 16px
|
||||
* under it) on the start side, the dial 36px after it, the actions under both; and the dial shrinks
|
||||
* to 238 or 200px when the window is short (ClockFaceSizeModifier). The input variant always stands.
|
||||
* In a landscape window with no room for the upright dial the dial variant lies on its side, as
|
||||
* Compose's HorizontalTimePicker does: the display with the period selector (216×38, 16px under it)
|
||||
* on the start side, the dial 36px after it, the actions under both; and the dial shrinks to 238 or
|
||||
* 200px when the window is shorter still (ClockFaceSizeModifier). The input variant always stands.
|
||||
*
|
||||
* The switch is orientation and viewport *height*, never a width breakpoint: M3 says the picker
|
||||
* "swaps orientation/variant based on device orientation and viewport height"
|
||||
* (docs/reference/m3/components-navigation-selection-inputs.md § Time pickers/Behaviour), and no
|
||||
* window size class applies — so the media queries below name a viewport height and never a width.
|
||||
* 560px is what the upright dial needs — 528px of surface (48 padding, 36 title, 116 display, 280
|
||||
* dial, 48 actions) inside the dialog's `100dvh - 32px` — so a landscape phone lies the dial down
|
||||
* and a landscape tablet or desktop, which has the height, leaves it standing. The queries and the
|
||||
* sizes they make room for are both px (M3 gives the sizes in dp), so a larger text size moves
|
||||
* neither.
|
||||
*
|
||||
* The hour and minute boxes and the period buttons render the foundation's `md-state-layer` and
|
||||
* `md-focus-ring` (foundation/interaction.css) — each is its own interactive box with nothing drawn
|
||||
* smaller inside it, so the classes need no refinement. The dial renders `md-focus-ring` alone (it
|
||||
* draws no state layer of its own); nothing here copies its outline rule. Cancel, OK and the mode
|
||||
* toggle are <x-button>, which draws from the classes already. `md-touch-target` is not rendered:
|
||||
* the boxes, the dial and the inputs are well over 48px, and the period buttons are the two halves
|
||||
* of M3's 52×80 (72 in the input variant, 216×38 lying down) selector, where a 48px target on each
|
||||
* would reach over its neighbour.
|
||||
*
|
||||
* Disabled is on-surface at M3's 38% content and 12% container opacities (tokens/state.css).
|
||||
*
|
||||
* The selector's angle is a registered property, so a single transition turns the line, carries the
|
||||
* handle round and moves the clip that inks the number under it, on the default spatial spring. The
|
||||
* numbers cross-fade between hours and minutes on the default effects spring.
|
||||
*/
|
||||
|
||||
@property --timepicker-angle {
|
||||
syntax: '<angle>';
|
||||
inherits: true;
|
||||
initial-value: 0deg;
|
||||
}
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@layer components {
|
||||
[data-timepicker-field] .field-control {
|
||||
@import './field.css';
|
||||
@import './icon.css';
|
||||
@import './button.css';
|
||||
@import './modal.css';
|
||||
|
||||
@layer material.components {
|
||||
@property --timepicker-angle {
|
||||
syntax: '<angle>';
|
||||
inherits: true;
|
||||
initial-value: 0deg;
|
||||
}
|
||||
|
||||
[data-md-timepicker-field] [data-md-field-control] {
|
||||
cursor: pointer;
|
||||
caret-color: transparent;
|
||||
}
|
||||
|
||||
[data-timepicker-dialog] {
|
||||
max-width: calc(100vw - 2rem);
|
||||
max-height: calc(100dvh - 2rem);
|
||||
/* 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));
|
||||
margin: auto;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
[data-timepicker-surface] {
|
||||
[data-md-timepicker-surface] {
|
||||
display: grid;
|
||||
grid-template-columns: max-content;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
padding: var(--md-sys-measurement-space300);
|
||||
border-radius: var(--md-sys-shape-corner-xl);
|
||||
background-color: var(--md-sys-color-surface-container-high);
|
||||
box-shadow: var(--md-sys-elevation-3);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
[data-timepicker-title] {
|
||||
padding-bottom: 1.25rem;
|
||||
[data-md-timepicker-title] {
|
||||
padding-bottom: 20px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-label-md);
|
||||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||||
}
|
||||
|
||||
[data-timepicker-picker] {
|
||||
[data-md-timepicker-picker] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -77,34 +117,34 @@
|
||||
|
||||
/* The mode is an attribute rather than x-show, which shows only on the next frame: focus moves
|
||||
into the variant that was just switched to within the same tick. */
|
||||
[data-timepicker-surface]:not([data-mode="input"]) :is([data-timepicker-typing], [data-timepicker-when="input"]),
|
||||
[data-timepicker-surface][data-mode="input"] :is([data-timepicker-picker], [data-timepicker-when="dial"]) {
|
||||
[data-md-timepicker-surface]:not([data-md-mode='input']) :is([data-md-timepicker-typing], [data-md-timepicker-when='input']),
|
||||
[data-md-timepicker-surface][data-md-mode='input'] :is([data-md-timepicker-picker], [data-md-timepicker-when='dial']) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-timepicker-when] {
|
||||
[data-md-timepicker-when] {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
/* ---- The time selector and the period selector ------------------------------------------ */
|
||||
|
||||
[data-timepicker-display] {
|
||||
[data-md-timepicker-display] {
|
||||
display: flex;
|
||||
margin-bottom: 2.25rem;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
[data-timepicker-numbers],
|
||||
[data-timepicker-inputs] {
|
||||
[data-md-timepicker-numbers],
|
||||
[data-md-timepicker-inputs] {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
[data-timepicker-box] {
|
||||
[data-md-timepicker-box] {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 6rem;
|
||||
height: 5rem;
|
||||
width: 96px;
|
||||
height: 80px;
|
||||
border-radius: var(--md-sys-shape-corner-sm);
|
||||
background-color: var(--md-sys-color-surface-container-highest);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
@@ -117,32 +157,39 @@
|
||||
transition-timing-function: var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
[data-timepicker-box][aria-pressed="true"] {
|
||||
[data-md-timepicker-box][aria-pressed='true'] {
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
[data-timepicker-separator] {
|
||||
/* A 24-hour clock has no period selector beside the boxes, and M3 widens them into the room
|
||||
(TimePickerTokens.TimeSelector24HVerticalContainerWidth = 114dp). Landscape keeps 96px: the
|
||||
display is a 216px column there. */
|
||||
[data-md-timepicker-display][data-md-cycle='24'] [data-md-timepicker-box] {
|
||||
width: 114px;
|
||||
}
|
||||
|
||||
[data-md-timepicker-separator] {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 1.5rem;
|
||||
height: 5rem;
|
||||
width: 24px;
|
||||
height: 80px;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
font: var(--md-sys-typescale-display-lg);
|
||||
translate: 0 -0.25rem;
|
||||
translate: 0 -4px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
[data-timepicker-period] {
|
||||
[data-md-timepicker-period] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
width: 3.25rem;
|
||||
height: 5rem;
|
||||
margin-inline-start: 0.25rem;
|
||||
gap: var(--md-sys-measurement-space50);
|
||||
width: 52px;
|
||||
height: 80px;
|
||||
margin-inline-start: var(--md-sys-measurement-space50);
|
||||
}
|
||||
|
||||
[data-timepicker-period] > button {
|
||||
[data-md-timepicker-period] > button {
|
||||
flex: 1 1 0%;
|
||||
min-width: 0;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
@@ -156,27 +203,29 @@
|
||||
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
||||
}
|
||||
|
||||
[data-timepicker-period] > button:active,
|
||||
[data-timepicker-period] > button[aria-pressed="true"] {
|
||||
border-radius: 0.75rem;
|
||||
[data-md-timepicker-period] > button:active,
|
||||
[data-md-timepicker-period] > button[aria-checked='true'] {
|
||||
border-radius: var(--md-sys-shape-corner-md);
|
||||
}
|
||||
|
||||
[data-timepicker-period] > button[aria-pressed="true"] {
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
/* Tertiary, not the primary the hour and minute boxes take: M3 gives the two selectors different
|
||||
emphases (PeriodSelectorSelectedContainerColor = TertiaryContainer). */
|
||||
[data-md-timepicker-period] > button[aria-checked='true'] {
|
||||
background-color: var(--md-sys-color-tertiary-container);
|
||||
color: var(--md-sys-color-on-tertiary-container);
|
||||
font-weight: var(--md-ref-typeface-weight-bold);
|
||||
}
|
||||
|
||||
[data-timepicker-period] > button:disabled {
|
||||
[data-md-timepicker-period] > button:disabled {
|
||||
cursor: default;
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-container-opacity) * 100%), transparent);
|
||||
}
|
||||
|
||||
/* ---- The dial ----------------------------------------------------------------------------- */
|
||||
|
||||
[data-timepicker-dial] {
|
||||
--dial: 16rem;
|
||||
[data-md-timepicker-dial] {
|
||||
--dial: 256px;
|
||||
--unit: calc(var(--dial) / 256);
|
||||
--outer: calc(101 * var(--unit));
|
||||
--inner: calc(69 * var(--unit));
|
||||
@@ -187,7 +236,7 @@
|
||||
flex: none;
|
||||
width: var(--dial);
|
||||
height: var(--dial);
|
||||
margin-bottom: 1.5rem;
|
||||
margin-bottom: var(--md-sys-measurement-space300);
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: var(--md-sys-color-surface-container-highest);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
@@ -198,50 +247,44 @@
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
outline: none;
|
||||
direction: ltr;
|
||||
transition: --timepicker-angle var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default);
|
||||
}
|
||||
|
||||
[data-timepicker-dial]:focus-visible {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* A drag keeps the handle under the pointer; the spring only settles it on release. */
|
||||
[data-timepicker-dial][data-dragging] {
|
||||
[data-md-timepicker-dial][data-md-dragging] {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
[data-timepicker-dial][data-inner] {
|
||||
[data-md-timepicker-dial][data-md-inner] {
|
||||
--reach: var(--inner);
|
||||
}
|
||||
|
||||
[data-timepicker-labels],
|
||||
[data-timepicker-ink],
|
||||
[data-timepicker-set] {
|
||||
[data-md-timepicker-labels],
|
||||
[data-md-timepicker-ink],
|
||||
[data-md-timepicker-set] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
[data-timepicker-set] {
|
||||
[data-md-timepicker-set] {
|
||||
transition-property: opacity, visibility;
|
||||
transition-duration: var(--md-sys-motion-effects-default-duration);
|
||||
transition-timing-function: var(--md-sys-motion-effects-default);
|
||||
}
|
||||
|
||||
[data-timepicker-dial]:not([data-cycle="24"]) [data-timepicker-set="hour24"],
|
||||
[data-timepicker-dial][data-cycle="24"] [data-timepicker-set="hour12"] {
|
||||
[data-md-timepicker-dial]:not([data-md-cycle='24']) [data-md-timepicker-set='hour24'],
|
||||
[data-md-timepicker-dial][data-md-cycle='24'] [data-md-timepicker-set='hour12'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-timepicker-dial][data-view="minute"] :is([data-timepicker-set="hour12"], [data-timepicker-set="hour24"]),
|
||||
[data-timepicker-dial]:not([data-view="minute"]) [data-timepicker-set="minute"] {
|
||||
[data-md-timepicker-dial][data-md-view='minute'] :is([data-md-timepicker-set='hour12'], [data-md-timepicker-set='hour24']),
|
||||
[data-md-timepicker-dial]:not([data-md-view='minute']) [data-md-timepicker-set='minute'] {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
[data-timepicker-set] > span {
|
||||
[data-md-timepicker-set] > span {
|
||||
--ring: var(--outer);
|
||||
|
||||
position: absolute;
|
||||
@@ -249,29 +292,29 @@
|
||||
top: calc(50% + var(--y) * var(--ring));
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
translate: -50% -50%;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
[data-timepicker-set] > span[data-inner] {
|
||||
[data-md-timepicker-set] > span[data-md-inner] {
|
||||
--ring: var(--inner);
|
||||
}
|
||||
|
||||
[data-timepicker-set] > span[data-disabled] {
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||||
[data-md-timepicker-set] > span[data-md-disabled] {
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
}
|
||||
|
||||
/* The selector: the line from the centre to the handle's edge, the centre dot, the handle. */
|
||||
[data-timepicker-selector] {
|
||||
[data-md-timepicker-selector] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-timepicker-track] {
|
||||
[data-md-timepicker-track] {
|
||||
position: absolute;
|
||||
left: calc(50% - 1px);
|
||||
bottom: 50%;
|
||||
@@ -282,22 +325,22 @@
|
||||
rotate: var(--timepicker-angle);
|
||||
}
|
||||
|
||||
[data-timepicker-centre],
|
||||
[data-timepicker-handle] {
|
||||
[data-md-timepicker-centre],
|
||||
[data-md-timepicker-handle] {
|
||||
position: absolute;
|
||||
translate: -50% -50%;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
[data-timepicker-centre] {
|
||||
[data-md-timepicker-centre] {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
[data-timepicker-handle] {
|
||||
[data-md-timepicker-handle] {
|
||||
left: calc(50% + sin(var(--timepicker-angle)) * var(--reach));
|
||||
top: calc(50% - cos(var(--timepicker-angle)) * var(--reach));
|
||||
width: var(--handle);
|
||||
@@ -305,34 +348,34 @@
|
||||
}
|
||||
|
||||
/* The numbers again, in on-primary, cut to the handle: what Compose draws with BlendMode.DstOver. */
|
||||
[data-timepicker-ink] {
|
||||
[data-md-timepicker-ink] {
|
||||
color: var(--md-sys-color-on-primary);
|
||||
pointer-events: none;
|
||||
clip-path: circle(calc(var(--handle) / 2) at calc(50% + sin(var(--timepicker-angle)) * var(--reach)) calc(50% - cos(var(--timepicker-angle)) * var(--reach)));
|
||||
}
|
||||
|
||||
[data-timepicker-ink] [data-timepicker-set] > span[data-disabled] {
|
||||
[data-md-timepicker-ink] [data-md-timepicker-set] > span[data-md-disabled] {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* ---- The input variant -------------------------------------------------------------------- */
|
||||
|
||||
[data-timepicker-inputs] [data-timepicker-separator] {
|
||||
height: 4.5rem;
|
||||
[data-md-timepicker-inputs] [data-md-timepicker-separator] {
|
||||
height: 72px;
|
||||
}
|
||||
|
||||
[data-timepicker-inputs] [data-timepicker-period] {
|
||||
height: 4.5rem;
|
||||
[data-md-timepicker-inputs] [data-md-timepicker-period] {
|
||||
height: 72px;
|
||||
}
|
||||
|
||||
[data-timepicker-column] {
|
||||
width: 6rem;
|
||||
[data-md-timepicker-column] {
|
||||
width: 96px;
|
||||
}
|
||||
|
||||
[data-timepicker-input] {
|
||||
[data-md-timepicker-input] {
|
||||
display: block;
|
||||
width: 6rem;
|
||||
height: 4.5rem;
|
||||
width: 96px;
|
||||
height: 72px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: var(--md-sys-shape-corner-sm);
|
||||
@@ -349,112 +392,117 @@
|
||||
transition-timing-function: var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
[data-timepicker-input]:focus {
|
||||
[data-md-timepicker-input]:focus {
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
box-shadow: inset 0 0 0 2px var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
[data-timepicker-input][aria-invalid="true"] {
|
||||
[data-md-timepicker-input][aria-invalid='true'] {
|
||||
background-color: var(--md-sys-color-error-container);
|
||||
color: var(--md-sys-color-error);
|
||||
caret-color: var(--md-sys-color-error);
|
||||
box-shadow: inset 0 0 0 1px var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
[data-timepicker-input][aria-invalid="true"]:focus {
|
||||
[data-md-timepicker-input][aria-invalid='true']:focus {
|
||||
box-shadow: inset 0 0 0 2px var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
/* SupportingText: two lines' room, 7px under the field; the error in its place. */
|
||||
[data-timepicker-support] {
|
||||
/* SupportingText: two lines' room, 7px under the field; the error in its place. The room is two
|
||||
body-small lines (1rem each), so it stays in rem and grows with the text. */
|
||||
[data-md-timepicker-support] {
|
||||
min-height: 2rem;
|
||||
padding-top: 0.4375rem;
|
||||
padding-top: 7px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-sm);
|
||||
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
||||
}
|
||||
|
||||
[data-timepicker-support][data-error],
|
||||
[data-timepicker-range-error] {
|
||||
[data-md-timepicker-support][data-md-error],
|
||||
[data-md-timepicker-range-error] {
|
||||
color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
[data-timepicker-range-error] {
|
||||
max-width: 17rem;
|
||||
padding-bottom: 0.5rem;
|
||||
[data-md-timepicker-range-error] {
|
||||
max-width: 272px;
|
||||
padding-bottom: var(--md-sys-measurement-space100);
|
||||
font: var(--md-sys-typescale-body-sm);
|
||||
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
||||
}
|
||||
|
||||
[data-timepicker-actions] {
|
||||
[data-md-timepicker-actions] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-height: 3rem;
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
[data-timepicker-actions] > [data-timepicker-spacer] {
|
||||
[data-md-timepicker-actions] > [data-md-timepicker-spacer] {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
/* ---- Landscape ---------------------------------------------------------------------------- */
|
||||
|
||||
@media (orientation: landscape) and (min-width: 37rem) {
|
||||
[data-timepicker-surface][data-mode="dial"] {
|
||||
grid-template-columns: 13.5rem auto;
|
||||
@media (orientation: landscape) and (height < 560px) {
|
||||
[data-md-timepicker-surface][data-md-mode='dial'] {
|
||||
grid-template-columns: 216px auto;
|
||||
grid-template-areas:
|
||||
"display dial"
|
||||
"actions actions";
|
||||
column-gap: 2.25rem;
|
||||
padding: 1rem 1.5rem 0.5rem;
|
||||
'display dial'
|
||||
'actions actions';
|
||||
column-gap: 36px;
|
||||
padding: var(--md-sys-measurement-space200) var(--md-sys-measurement-space300) var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-timepicker-surface][data-mode="dial"] [data-timepicker-title] {
|
||||
[data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-title] {
|
||||
grid-area: display;
|
||||
align-self: start;
|
||||
margin-top: 0.5rem;
|
||||
margin-top: var(--md-sys-measurement-space100);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
[data-timepicker-surface][data-mode="dial"] [data-timepicker-picker] {
|
||||
[data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-picker] {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
[data-timepicker-surface][data-mode="dial"] [data-timepicker-display] {
|
||||
[data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-display] {
|
||||
grid-area: display;
|
||||
flex-direction: column;
|
||||
align-self: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[data-timepicker-surface][data-mode="dial"] [data-timepicker-display] [data-timepicker-period] {
|
||||
flex-direction: row;
|
||||
width: 13.5rem;
|
||||
height: 2.375rem;
|
||||
margin: 1rem 0 0;
|
||||
[data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-display][data-md-cycle='24'] [data-md-timepicker-box] {
|
||||
width: 96px;
|
||||
}
|
||||
|
||||
[data-timepicker-surface][data-mode="dial"] [data-timepicker-dial] {
|
||||
[data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-display] [data-md-timepicker-period] {
|
||||
flex-direction: row;
|
||||
width: 216px;
|
||||
height: 38px;
|
||||
margin: var(--md-sys-measurement-space200) 0 0;
|
||||
}
|
||||
|
||||
[data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-dial] {
|
||||
grid-area: dial;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[data-timepicker-surface][data-mode="dial"] [data-timepicker-actions] {
|
||||
[data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-actions] {
|
||||
grid-area: actions;
|
||||
margin-top: 0.25rem;
|
||||
margin-top: var(--md-sys-measurement-space50);
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation: landscape) and (min-width: 37rem) and (max-height: 22.75rem) {
|
||||
[data-timepicker-surface][data-mode="dial"] [data-timepicker-dial] {
|
||||
--dial: 14.875rem;
|
||||
@media (orientation: landscape) and (height <= 364px) {
|
||||
[data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-dial] {
|
||||
--dial: 238px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation: landscape) and (min-width: 37rem) and (max-height: 21.625rem) {
|
||||
[data-timepicker-surface][data-mode="dial"] [data-timepicker-dial] {
|
||||
--dial: 12.5rem;
|
||||
@media (orientation: landscape) and (height <= 346px) {
|
||||
[data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-dial] {
|
||||
--dial: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* <x-toast>: the snackbar host — shows every toast queued behind it, one at a time.
|
||||
*
|
||||
* SnackbarTokens (androidx Compose Material 3, Apache-2.0): inverse-surface container,
|
||||
* inverse-on-surface text, an inverse-primary action, extra-small corners, elevation 3, 48px for
|
||||
* one line and 68px for two (SnackbarTokens.TwoLinesContainerHeight — the more precise of M3's
|
||||
* two published figures; the site's prose says 64dp). `[data-md-two-line]`, bound from whether
|
||||
* the current toast carries a description, pins the container to 68px instead of leaving it to
|
||||
* grow into it; below `medium` (600px), a two-line snackbar with an action wraps the action under
|
||||
* the text through `[data-md-toast-wrap]`, bound the same way — M3's "two lines with longer
|
||||
* action" configuration.
|
||||
*
|
||||
* It lifts above a bottom bar through `--material-bottom-bar`, and above a toolbar placed at the
|
||||
* bottom through the `--material-bottom-toolbar` toolbar.css publishes — whichever reaches higher,
|
||||
* 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.
|
||||
* 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; 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. 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.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-toast] {
|
||||
position: fixed;
|
||||
z-index: 50;
|
||||
inset-inline: var(--md-sys-measurement-space200);
|
||||
bottom: calc(max(var(--material-bottom-bar, 0px), var(--material-bottom-toolbar, 0px)) + var(--md-sys-measurement-space200));
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-md-toast][data-md-position='bottom-start'] {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
@media (width >= 600px) {
|
||||
[data-md-toast][data-md-position='bottom-start'] {
|
||||
inset-inline-start: var(--md-sys-measurement-space300);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-toast-snackbar] {
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
max-width: min(100%, 576px);
|
||||
min-height: var(--md-sys-measurement-space600);
|
||||
border-radius: var(--md-sys-shape-corner-xs);
|
||||
padding-block: 6px;
|
||||
padding-inline: var(--md-sys-measurement-space200) var(--md-sys-measurement-space100);
|
||||
background-color: var(--md-sys-color-inverse-surface);
|
||||
color: var(--md-sys-color-inverse-on-surface);
|
||||
box-shadow: var(--md-sys-elevation-3);
|
||||
translate: 0;
|
||||
opacity: 1;
|
||||
transition-property: translate, opacity;
|
||||
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
||||
|
||||
@starting-style {
|
||||
translate: 0 16px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-toast-snackbar][data-md-two-line] {
|
||||
min-height: 68px;
|
||||
}
|
||||
|
||||
@media (width >= 600px) {
|
||||
[data-md-toast-snackbar] {
|
||||
width: auto;
|
||||
min-width: 344px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-toast-content] {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
padding-block: 6px;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
[data-md-toast-content][data-md-toast-wrap] {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-toast-title],
|
||||
[data-md-toast-description] {
|
||||
font: var(--md-sys-typescale-body-md);
|
||||
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
:is([data-md-toast-action], [data-md-toast-dismiss]) {
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
[data-md-toast-action] {
|
||||
margin-inline-start: auto;
|
||||
height: var(--md-sys-measurement-space500);
|
||||
padding-inline: 12px;
|
||||
color: var(--md-sys-color-inverse-primary);
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-toast-dismiss] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--md-sys-measurement-space500);
|
||||
height: var(--md-sys-measurement-space500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* <x-toggle>: M3's switch (resources/views/components/toggle.blade.php), on the row the selection
|
||||
* controls share (components/selection.css). The hooks say `switch` because `toggle` is daisyUI's
|
||||
* class, which the design guard rejects.
|
||||
*
|
||||
* SwitchTokens (androidx Compose Material 3, Apache-2.0): a 52×32 track with a 2px outline, in
|
||||
* surface-container-highest when off and primary when on. The handle's centre is 16px from the
|
||||
* track's start off and 36px on; it is 16px across off, 24px with an icon or on, and 28px while
|
||||
* pressed, moving and growing on the fast spatial spring. Off it is outline, on-surface-variant
|
||||
* while hovered, focused or pressed; on it is on-primary, primary-container in those states. The
|
||||
* state layer is a 40px circle around the handle. Its icons are 16px, on-primary-container when on.
|
||||
* Disabled, the track is on-surface at 12% and the handle on-surface at 38% (surface when on).
|
||||
*
|
||||
* [data-md-toggle] the root; `class` and `style` land here
|
||||
* [data-md-selection-row] data-md-right puts the switch at the end of the row
|
||||
* [data-md-switch] the track; `data-md-icons` = "both" or "selected"
|
||||
* input, [data-md-switch-handle] (with [data-md-switch-on] and [data-md-switch-off] icons)
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './selection.css';
|
||||
@import './icon.css';
|
||||
|
||||
@layer material.components {
|
||||
/* In a flex row, a setting's text beside its switch, the root keeps its automatic minimum:
|
||||
the track, and beside a label the track, the gap and the label's longest word. So the text
|
||||
beside a bare switch, or the switch's own label, wraps before the track gives way; a root
|
||||
free to shrink below 52px left the track running out of it, and off a card's edge on a
|
||||
phone. */
|
||||
[data-md-toggle] {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
/* The positions below are the handle's centres less half its size, measured inside the track's
|
||||
2px outline. */
|
||||
[data-md-switch] {
|
||||
position: relative;
|
||||
display: grid;
|
||||
flex: none;
|
||||
width: 52px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
[data-md-switch] input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 2px solid var(--md-sys-color-outline);
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: var(--md-sys-color-surface-container-highest);
|
||||
}
|
||||
|
||||
[data-md-switch] input:focus-visible {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
[data-md-switch-handle] {
|
||||
--handle: 16px;
|
||||
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
inset-inline-start: calc(16px - var(--handle) / 2);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: var(--handle);
|
||||
height: var(--handle);
|
||||
translate: 0 -50%;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: var(--md-sys-color-outline);
|
||||
color: var(--md-sys-color-surface-container-highest);
|
||||
pointer-events: none;
|
||||
transition-property: inset-inline-start, width, height, background-color;
|
||||
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
||||
}
|
||||
|
||||
[data-md-switch-handle]::before {
|
||||
inset: 50% auto auto 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
translate: -50% -50%;
|
||||
}
|
||||
|
||||
[data-md-switch-handle] > svg {
|
||||
grid-area: 1 / 1;
|
||||
opacity: 0;
|
||||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
[data-md-switch][data-md-icons='both'] [data-md-switch-handle] {
|
||||
--handle: 24px;
|
||||
}
|
||||
|
||||
[data-md-switch][data-md-icons='both'] input:not(:checked) ~ [data-md-switch-handle] [data-md-switch-off],
|
||||
[data-md-switch][data-md-icons] input:checked ~ [data-md-switch-handle] [data-md-switch-on] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-md-switch] input:checked {
|
||||
border-color: var(--md-sys-color-primary);
|
||||
background-color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
[data-md-switch] input:checked ~ [data-md-switch-handle] {
|
||||
--handle: 24px;
|
||||
|
||||
inset-inline-start: calc(36px - var(--handle) / 2);
|
||||
background-color: var(--md-sys-color-on-primary);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
[data-md-selection-row]:active [data-md-switch] input:not(:disabled) ~ [data-md-switch-handle] {
|
||||
--handle: 28px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-md-selection-row]:hover [data-md-switch] input:not(:disabled) ~ [data-md-switch-handle] {
|
||||
background-color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-md-selection-row]:hover [data-md-switch] input:checked:not(:disabled) ~ [data-md-switch-handle] {
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-switch] input:focus-visible ~ [data-md-switch-handle],
|
||||
[data-md-selection-row]:active [data-md-switch] input:not(:disabled) ~ [data-md-switch-handle] {
|
||||
background-color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-md-switch] input:checked:focus-visible ~ [data-md-switch-handle],
|
||||
[data-md-selection-row]:active [data-md-switch] input:checked:not(:disabled) ~ [data-md-switch-handle] {
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
}
|
||||
|
||||
[data-md-switch] input:disabled {
|
||||
border-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-container-opacity) * 100%), transparent);
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-surface-container-highest) calc(var(--md-sys-state-disabled-container-opacity) * 100%), transparent);
|
||||
}
|
||||
|
||||
[data-md-switch] input:disabled ~ [data-md-switch-handle] {
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
color: color-mix(in srgb, var(--md-sys-color-surface-container-highest) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
}
|
||||
|
||||
[data-md-switch] input:disabled:checked {
|
||||
border-color: transparent;
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-container-opacity) * 100%), transparent);
|
||||
}
|
||||
|
||||
[data-md-switch] input:disabled:checked ~ [data-md-switch-handle] {
|
||||
background-color: var(--md-sys-color-surface);
|
||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
}
|
||||
}
|
||||
@@ -1,96 +1,237 @@
|
||||
/*
|
||||
* M3 Expressive's toolbars (DockedToolbarTokens, FloatingToolbarTokens, androidx Compose Material 3,
|
||||
* Apache-2.0).
|
||||
* `<x-toolbar>`: M3 Expressive's toolbars (DockedToolbarTokens.kt, FloatingToolbarTokens.kt,
|
||||
* androidx Compose Material 3, Apache-2.0; docs/reference/m3/components-navigation-selection-inputs.md
|
||||
* § Toolbars).
|
||||
*
|
||||
* [data-md-toolbar] role="toolbar"; data-md-variant, data-md-vertical, data-md-vibrant,
|
||||
* data-md-rounded, data-md-toolbar-place
|
||||
* [data-md-toolbar-fab] a FAB docked at the end, flat on the bar (elevation 0)
|
||||
* [data-md-toolbar-group] a floating toolbar and its own FAB, side by side (or stacked)
|
||||
*
|
||||
* docked a 64px bar across the bottom of the screen in surface-container, square, its
|
||||
* controls spread out 4 to 32px apart between 16px ends
|
||||
* controls spread out 4 to 32px apart between 16px ends; with a FAB, the controls
|
||||
* at the start and the FAB flat at the end
|
||||
* floating a 64px pill in surface-container (or primary-container, `vibrant`) at elevation 3,
|
||||
* 8px at its ends and 4px between controls; `vertical` stands it on end
|
||||
*
|
||||
* Unlayered on purpose: a vibrant toolbar recolours the icon buttons inside it, which draw their
|
||||
* ink as utilities, and a rule in any layer loses to a utility.
|
||||
* 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.
|
||||
*
|
||||
* 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
|
||||
* slot the second at any width.
|
||||
*
|
||||
* `<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, 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.
|
||||
*/
|
||||
|
||||
[data-toolbar] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
[data-toolbar][data-variant="docked"] {
|
||||
width: 100%;
|
||||
min-height: calc(4rem + env(safe-area-inset-bottom));
|
||||
justify-content: center;
|
||||
column-gap: clamp(0.25rem, 4vw, 2rem);
|
||||
padding-inline: 1rem;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
@layer material.components {
|
||||
[data-md-toolbar] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--md-sys-measurement-space50);
|
||||
}
|
||||
|
||||
[data-toolbar][data-variant="floating"] {
|
||||
display: inline-flex;
|
||||
height: 4rem;
|
||||
padding-inline: 0.5rem;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
box-shadow: var(--md-sys-elevation-3);
|
||||
}
|
||||
[data-md-toolbar][data-md-variant='docked'] {
|
||||
width: 100%;
|
||||
min-height: calc(64px + var(--material-safe-bottom, env(safe-area-inset-bottom)));
|
||||
justify-content: center;
|
||||
column-gap: clamp(var(--md-sys-measurement-space50), 4vw, var(--md-sys-measurement-space400));
|
||||
padding-inline: var(--md-sys-measurement-space200);
|
||||
padding-bottom: var(--material-safe-bottom, env(safe-area-inset-bottom));
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-toolbar][data-variant="floating"][data-vertical] {
|
||||
width: 4rem;
|
||||
height: auto;
|
||||
flex-direction: column;
|
||||
padding-block: 0.5rem;
|
||||
padding-inline: 0;
|
||||
}
|
||||
[data-md-toolbar][data-md-variant='floating'] {
|
||||
display: inline-flex;
|
||||
height: 64px;
|
||||
padding-inline: var(--md-sys-measurement-space100);
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
box-shadow: var(--md-sys-elevation-3);
|
||||
}
|
||||
|
||||
[data-toolbar][data-vibrant] {
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
[data-md-toolbar][data-md-variant='floating'][data-md-vertical] {
|
||||
width: 64px;
|
||||
height: auto;
|
||||
flex-direction: column;
|
||||
padding-block: var(--md-sys-measurement-space100);
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
[data-toolbar][data-vibrant] [data-icon-button]:not([aria-pressed="true"]) {
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
[data-md-toolbar][data-md-vibrant] {
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
[data-toolbar][data-vibrant] [data-icon-button][aria-pressed="true"] {
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
[data-md-toolbar][data-md-vibrant] [data-md-icon-button]:not([data-md-variant='filled'], [data-md-variant='tonal'], [data-md-selected='true'], [aria-pressed='true'], :disabled, [aria-disabled='true']) {
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
/* A floating toolbar and its FAB, side by side (or stacked, vertical). */
|
||||
[data-toolbar-group] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
[data-md-toolbar][data-md-vibrant] [data-md-icon-button][aria-pressed='true']:not(:disabled, [aria-disabled='true']) {
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
[data-toolbar-group][data-vertical] {
|
||||
flex-direction: column;
|
||||
}
|
||||
/* 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. 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
|
||||
navigation toolbar came out a blank circle. A disabled button keeps button.css's disabled
|
||||
colours in either. */
|
||||
[data-md-toolbar]:not([data-md-vibrant]) [data-md-icon-button]:not([data-md-variant='filled'], [data-md-variant='tonal'], [data-md-selected='true'], [aria-pressed='true'], :disabled, [aria-disabled='true']) {
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
/* Placed over the page: centred above the bottom edge, or centred against the end edge. */
|
||||
[data-toolbar-place="bottom"] {
|
||||
position: fixed;
|
||||
bottom: calc(1rem + env(safe-area-inset-bottom));
|
||||
left: 50%;
|
||||
z-index: 30;
|
||||
translate: -50% 0;
|
||||
}
|
||||
/* A docked toolbar with a FAB: the controls at the start, the FAB at the end, resting on the
|
||||
bar at elevation 0 — M3 puts a FAB nested in another component there, and Compose's
|
||||
FloatingActionButtonDefaults.bottomAppBarFabElevation() is 0 in every state. */
|
||||
[data-md-toolbar][data-md-variant='docked']:has(> [data-md-toolbar-fab]) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
[data-toolbar-place="end"] {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
inset-inline-end: calc(1rem + env(safe-area-inset-right));
|
||||
z-index: 30;
|
||||
translate: 0 -50%;
|
||||
}
|
||||
[data-md-toolbar-fab] {
|
||||
display: flex;
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
[data-toolbar][data-variant="docked"][data-toolbar-place="bottom"] {
|
||||
inset-inline: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
translate: none;
|
||||
[data-md-toolbar-fab] [data-md-fab],
|
||||
[data-md-toolbar-fab] [data-md-fab]:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Groups of controls, a divider between each two (`<x-divider vertical />`, or `<x-divider />`
|
||||
in a vertical toolbar). The divider draws itself stretched across its row, which in a
|
||||
toolbar would run it into the container's edges; here it stands as long as the 40px icon
|
||||
buttons it separates, centred on them, as the large-screen docked toolbar in M3's
|
||||
guidelines draws it (toolbars/guidelines § Adaptive design → Resizing). */
|
||||
[data-md-toolbar]:not([data-md-vertical]) > [role='separator'][aria-orientation='vertical'] {
|
||||
align-self: center;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
[data-md-toolbar][data-md-vertical] > [role='separator'][aria-orientation='horizontal'] {
|
||||
align-self: center;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
/* A floating toolbar and its FAB, side by side (or stacked, vertical). */
|
||||
[data-md-toolbar-group] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-md-toolbar-group][data-md-vertical] {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Placed over the page: centred above the bottom edge, or centred against the end edge.
|
||||
*
|
||||
* 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. 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'] {
|
||||
position: fixed;
|
||||
bottom: calc(max(var(--material-bottom-bar, 0px), var(--material-safe-bottom, env(safe-area-inset-bottom))) + var(--md-sys-measurement-space200));
|
||||
left: 50%;
|
||||
z-index: 30;
|
||||
translate: -50% 0;
|
||||
}
|
||||
|
||||
[data-md-toolbar-place='end'] {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
inset-inline-end: calc(var(--md-sys-measurement-space200) + var(--material-safe-right, env(safe-area-inset-right)));
|
||||
z-index: 30;
|
||||
translate: 0 -50%;
|
||||
}
|
||||
|
||||
/* "Horizontal toolbars should have a minimum 16dp margin from the edge of the window. Vertical
|
||||
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)));
|
||||
}
|
||||
|
||||
[data-md-toolbar][data-md-variant='docked'][data-md-toolbar-place='bottom'] {
|
||||
inset-inline: 0;
|
||||
bottom: var(--material-bottom-bar, 0px);
|
||||
left: 0;
|
||||
translate: none;
|
||||
}
|
||||
|
||||
/* The large-screen docked toolbar: from `expanded` (840px) fully rounded, like the one M3 shows
|
||||
on the web, and spanning what it is written in. At the bottom of the window it lifts off the
|
||||
edges — 16dp from each side, M3's minimum outside padding for a toolbar, and 16px above the
|
||||
bottom bar or the safe area, as a floating toolbar sits — so the curve never meets the
|
||||
window's frame, and the bottom safe area, which it no longer touches, stops padding it.
|
||||
Below `expanded` it is the square, full-width docked toolbar M3 requires ("Avoid applying
|
||||
rounded corners to the container"). */
|
||||
@media (width >= 840px) {
|
||||
[data-md-toolbar][data-md-variant='docked'][data-md-rounded] {
|
||||
min-height: 64px;
|
||||
padding-bottom: 0;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
}
|
||||
|
||||
[data-md-toolbar][data-md-variant='docked'][data-md-rounded][data-md-toolbar-place='bottom'] {
|
||||
right: calc(var(--md-sys-measurement-space200) + var(--material-safe-right, env(safe-area-inset-right)));
|
||||
bottom: calc(max(var(--material-bottom-bar, 0px), var(--material-safe-bottom, env(safe-area-inset-bottom))) + var(--md-sys-measurement-space200));
|
||||
left: calc(var(--md-sys-measurement-space200) + var(--material-safe-left, env(safe-area-inset-left)));
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* What a toolbar at the bottom covers, published as --material-bottom-toolbar: the distance from
|
||||
* the window's bottom edge to the toolbar's top, each formula the placement above plus the
|
||||
* height. The snackbar clears it (toast.css) — M3 nudges a snackbar "upward to avoid overlapping
|
||||
* FABs/docked toolbars" and never puts one "in front of … navigation"
|
||||
* (docs/reference/m3/components-actions-communication-containment.md § Snackbar) — and a page
|
||||
* pads its end with it, so its last line never hides behind the toolbar.
|
||||
*
|
||||
* Declared on the root and again on <x-scaffold>'s root: a custom property resolves its var()s
|
||||
* where it is declared, and --material-bottom-bar is only set on the scaffold's root, so only
|
||||
* there does the formula see the bar. A floating toolbar is 64px, beside an 80px (`md`) or
|
||||
* 96px (`lg`) FAB as tall as the FAB; a docked one is 64px plus the bottom safe area it pads,
|
||||
* until `rounded` lifts it off the edge from `expanded`. A vertical toolbar, which belongs at
|
||||
* the end edge, publishes nothing. */
|
||||
:root:has([data-md-toolbar-place='bottom']:is([data-md-toolbar], [data-md-toolbar-group]):not([data-md-vertical])),
|
||||
[data-md-scaffold]:has([data-md-toolbar-place='bottom']:is([data-md-toolbar], [data-md-toolbar-group]):not([data-md-vertical])) {
|
||||
--material-bottom-toolbar: calc(max(var(--material-bottom-bar, 0px), var(--material-safe-bottom, env(safe-area-inset-bottom))) + var(--md-sys-measurement-space200) + 64px);
|
||||
}
|
||||
|
||||
:root:has([data-md-toolbar-group][data-md-toolbar-place='bottom']:not([data-md-vertical]) [data-md-fab][data-md-size='md']),
|
||||
[data-md-scaffold]:has([data-md-toolbar-group][data-md-toolbar-place='bottom']:not([data-md-vertical]) [data-md-fab][data-md-size='md']) {
|
||||
--material-bottom-toolbar: calc(max(var(--material-bottom-bar, 0px), var(--material-safe-bottom, env(safe-area-inset-bottom))) + var(--md-sys-measurement-space200) + 80px);
|
||||
}
|
||||
|
||||
:root:has([data-md-toolbar-group][data-md-toolbar-place='bottom']:not([data-md-vertical]) [data-md-fab][data-md-size='lg']),
|
||||
[data-md-scaffold]:has([data-md-toolbar-group][data-md-toolbar-place='bottom']:not([data-md-vertical]) [data-md-fab][data-md-size='lg']) {
|
||||
--material-bottom-toolbar: calc(max(var(--material-bottom-bar, 0px), var(--material-safe-bottom, env(safe-area-inset-bottom))) + var(--md-sys-measurement-space200) + 96px);
|
||||
}
|
||||
|
||||
:root:has([data-md-toolbar][data-md-variant='docked'][data-md-toolbar-place='bottom']),
|
||||
[data-md-scaffold]:has([data-md-toolbar][data-md-variant='docked'][data-md-toolbar-place='bottom']) {
|
||||
--material-bottom-toolbar: calc(var(--material-bottom-bar, 0px) + 64px + var(--material-safe-bottom, env(safe-area-inset-bottom)));
|
||||
}
|
||||
|
||||
@media (width >= 840px) {
|
||||
:root:has([data-md-toolbar][data-md-variant='docked'][data-md-rounded][data-md-toolbar-place='bottom']),
|
||||
[data-md-scaffold]:has([data-md-toolbar][data-md-variant='docked'][data-md-rounded][data-md-toolbar-place='bottom']) {
|
||||
--material-bottom-toolbar: calc(max(var(--material-bottom-bar, 0px), var(--material-safe-bottom, env(safe-area-inset-bottom))) + var(--md-sys-measurement-space200) + 64px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* <x-tooltip>: M3's plain tooltip, a short label for a control in the inverse surface.
|
||||
*
|
||||
* The bubble is a `popover="manual"` in the top layer, so no `overflow: hidden` parent clips it,
|
||||
* placed by CSS anchor positioning on its `data-md-side` and flipping to the other side where the
|
||||
* window has no room. PlainTooltipTokens and TooltipDefaults (androidx Compose Material 3,
|
||||
* Apache-2.0): `inverse-surface` container with the extra-small corner, `body-small` supporting
|
||||
* text in `inverse-on-surface`, 8px either side and 4px above and below, 200px wide at most, 4px
|
||||
* from its anchor. It never takes the pointer, so it can stand over the control it names.
|
||||
*
|
||||
* It fades in and out on the fast effects spring: `@starting-style` gives the fade in a start, and
|
||||
* the fade out is its exit copy's (`data-md-popover-exit`, resources/js/popover-exit.js), which
|
||||
* `data-md-popover-closing` turns back to the closed opacity. resources/js/tooltip.js shows and
|
||||
* hides it.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user