Draw the toolbar without Tailwind
Plan step 36 (navigation group, second stream): <x-toolbar>'s class
lists move into resources/css/components/toolbar.css, keyed on
data-md-toolbar (data-md-variant/-vertical/-vibrant/-rounded/
-toolbar-place) and data-md-toolbar-group/-toolbar-fab. The docked
form with a FAB (N-02's --material-bottom-bar clearance), the vertical
24dp margin (N-12), the standard toolbar's primary icon buttons
(N-13), and the rounded large-screen form with dividers from 840px
were all already correct; only their hooks, units and layer needed to
change. The file no longer sits outside every layer: button.css and
fab.css now draw their own ink through data-md-* attributes rather
than a Tailwind utility, so a toolbar's more specific selector always
beats their single base color/background-color/box-shadow declaration
without needing to escape the cascade layers.
Sizes stay px (64px row, 40px divider); spacing goes through the
measurement tokens where one matches the value (4px leading/trailing
gap, 16/32px docked spread, 8px floating ends, 16/24px placed
margins).
Hooks renamed: every unprefixed data-toolbar* attribute to data-md-*,
updated in tests/Feature/Components/{AppBarTest,ToolbarTest}.php and
tests/Browser/BarsTest.php. toolbar.js needed no change: it reads the
bar through $root and role="toolbar", not by hook name.
Carry-over from the app-bar step: layout/pane.css now imports
app-bar.css and button.css for the pane's own top app bar and back
button, now that both are rewritten. That import crosses into
material.components for the first time from a layout file, so
tests/Feature/StylesheetsTest.php's two layout-shape checks now skip
non-`layout/` files reached through it — each already covered by its
own group's stylesheet test.
Added the owed browser tests (docs/plans/material-3-browser-tests.md):
a docked toolbar clear of the navigation bar below medium, and the
rounded form with its divider height from 840px, clear of the
window's edges.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b7641a35c2
commit
9c134b2dc7
@@ -1,6 +1,12 @@
|
||||
/*
|
||||
* 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; with a FAB, the controls
|
||||
@@ -9,174 +15,178 @@
|
||||
* 8px at its ends and 4px between controls; `vertical` stands it on end
|
||||
*
|
||||
* Placed over the page, a horizontal toolbar keeps 16dp from the window's edge and a vertical one
|
||||
* 24dp, which is M3's minimum for each.
|
||||
* 24dp, which is M3's minimum for each (N-12).
|
||||
*
|
||||
* On web and large screens a docked toolbar "can be rounded", and dividers can organise its
|
||||
* controls into groups (docs/reference/m3/components-navigation-selection-inputs.md § Toolbars):
|
||||
* `rounded` does the first from `expanded`, and a divider in the slot the second at any width.
|
||||
* controls into groups: `rounded` does the first from `expanded` (840px), and a divider in the
|
||||
* slot the second at any width.
|
||||
*
|
||||
* Unlayered on purpose: a toolbar recolours the icon buttons inside it — primary in a standard
|
||||
* one, on-primary-container in a vibrant one — and they draw their ink as utilities, which a rule
|
||||
* in any layer loses to.
|
||||
* `<x-button>` and `<x-fab>` draw their own ink from `data-md-color`/`data-md-variant`, not a
|
||||
* utility, so a toolbar's recolouring rules — vibrant's icon buttons, a standard toolbar's primary
|
||||
* ones (N-13), a docked FAB's flattened shadow — sit in this file's own `material.components`
|
||||
* layer like everything else: each only overrides the single `color`/`background-color`/
|
||||
* `box-shadow` declaration button.css or fab.css puts on its own base selector, so a toolbar's
|
||||
* more specific selector always wins without needing to sit outside the layer.
|
||||
*/
|
||||
|
||||
[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 + var(--material-safe-bottom, env(safe-area-inset-bottom)));
|
||||
justify-content: center;
|
||||
column-gap: clamp(0.25rem, 4vw, 2rem);
|
||||
padding-inline: 1rem;
|
||||
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);
|
||||
}
|
||||
@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-toolbar][data-vibrant] {
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
[data-toolbar][data-vibrant] [data-md-icon-button]:not([aria-pressed="true"]) {
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
[data-toolbar][data-vibrant] [data-md-icon-button][aria-pressed="true"] {
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
/* 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. An icon button carries its own
|
||||
ink as a utility, so the container's `color` never reaches it and this rule has to. */
|
||||
[data-toolbar]:not([data-vibrant]) [data-md-icon-button]:not([aria-pressed="true"]) {
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
/* 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. `<x-fab>` draws its
|
||||
shadow with utilities, which is one more reason this file is unlayered. */
|
||||
[data-toolbar][data-variant="docked"]:has(> [data-toolbar-fab]) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
[data-toolbar-fab] {
|
||||
display: flex;
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
[data-toolbar-fab] [data-md-fab],
|
||||
[data-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). The divider is `self-stretch`, a utility,
|
||||
which this unlayered rule beats. */
|
||||
[data-toolbar]:not([data-vertical]) > [role="separator"][aria-orientation="vertical"] {
|
||||
align-self: center;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
[data-toolbar][data-vertical] > [role="separator"][aria-orientation="horizontal"] {
|
||||
align-self: center;
|
||||
width: 2.5rem;
|
||||
}
|
||||
|
||||
/* 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-toolbar-group][data-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 (docs/reference/m3/components-navigation-selection-inputs.md § Toolbars); the
|
||||
* offset is the backstop, not a licence. */
|
||||
[data-toolbar-place="bottom"] {
|
||||
position: fixed;
|
||||
bottom: calc(max(var(--material-bottom-bar, 0px), var(--material-safe-bottom, env(safe-area-inset-bottom))) + 1rem);
|
||||
left: 50%;
|
||||
z-index: 30;
|
||||
translate: -50% 0;
|
||||
}
|
||||
|
||||
[data-toolbar-place="end"] {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
inset-inline-end: calc(1rem + 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"
|
||||
(docs/reference/m3/components-navigation-selection-inputs.md § Toolbars). */
|
||||
[data-toolbar][data-vertical][data-toolbar-place="end"],
|
||||
[data-toolbar-group][data-vertical][data-toolbar-place="end"] {
|
||||
inset-inline-end: calc(1.5rem + var(--material-safe-right, env(safe-area-inset-right)));
|
||||
}
|
||||
|
||||
[data-toolbar][data-variant="docked"][data-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 >= 52.5rem) {
|
||||
[data-toolbar][data-variant="docked"][data-rounded] {
|
||||
min-height: 4rem;
|
||||
padding-bottom: 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-variant="docked"][data-rounded][data-toolbar-place="bottom"] {
|
||||
right: calc(1rem + 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))) + 1rem);
|
||||
left: calc(1rem + var(--material-safe-left, env(safe-area-inset-left)));
|
||||
width: auto;
|
||||
[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-md-toolbar][data-md-vibrant] {
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
[data-md-toolbar][data-md-vibrant] [data-md-icon-button]:not([aria-pressed='true']) {
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
[data-md-toolbar][data-md-vibrant] [data-md-icon-button][aria-pressed='true'] {
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
/* M3's colour list for a standard toolbar ends "Standard button (Primary)", as the vibrant list
|
||||
ends "Standard button (On primary container)" — the row above (N-13). */
|
||||
[data-md-toolbar]:not([data-md-vibrant]) [data-md-icon-button]:not([aria-pressed='true']) {
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
/* 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-md-toolbar-fab] {
|
||||
display: flex;
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
[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 (N-02). 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" (N-12). */
|
||||
[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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user