Draw the scaffold without Tailwind
Plan step 36 (navigation group, third batch): <x-scaffold>'s own styling moves into resources/css/layout/scaffold.css (the content region, the bar and rail row, the banner, the actions row and its rail-collapsed column layout, --material-bottom-bar and --material-margin publishing, the skip link) alongside step 35's FAB and content-margin rules already there. Every data-app-shell* hook becomes data-md-scaffold-* (data-app-shell-bar, -actions, -banner); the skip link is data-md-skip-link; data-app-shell itself is dropped, data-md-scaffold already named the root. The actions row's rail-collapsed:flex-col is written out branch for branch as the navigation rail's own rewrite did for its internal parts: the three width-independent conditions in one :where() group, the four width-gated ones each in their own @media block. With that gone, resources/css/tailwind.css's rail-collapsed custom-variant shim (its last use) is removed; tailwind.css now carries only tokens/theme.css and tokens/utilities.css, which the showcase still needs until step 38. navigation-bar.css's hide-on-scroll rule reading --material-bottom-bar stayed unlayered only because <x-scaffold> published that variable through a Tailwind utility, which no layered rule could outrank; now scaffold.css sets it itself in material.layout, a layer navigation-bar.css's own material.components always beats, so the rule moves into the layer and the file fits one @layer material.components block like every other navigation stylesheet. navigation-bar rejoins NavigationStylesheetsTest.php's dataset and NavigationBarTest.php's own duplicate shape test is retired in favour of it. Browser tests added (docs/plans/material-3-browser-tests.md): the scaffold's FAB dropping the bar's own height once hide-bar-on-scroll slides it away, at the trailing edge in a right-to-left document, and clearing a safe area an application sets on its inline-end and bottom 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
20b062846d
commit
ed93222d22
@@ -73,19 +73,21 @@
|
||||
[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. Unlayered on purpose, still: <x-scaffold> (its own rewrite is a
|
||||
* later batch) publishes --material-bottom-bar with a Tailwind utility
|
||||
* (`max-medium:[--material-bottom-bar:…]`), and a rule in any layer loses to an unlayered utility
|
||||
* regardless of specificity — unlike toolbar.css's and fab.css's overrides, which only have to beat
|
||||
* another `material.components` rule. Keyed on `data-md-scaffold`, the hook the scaffold already
|
||||
* renders, not `data-app-shell`, which belongs to that later rewrite.
|
||||
*/
|
||||
[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));
|
||||
/*
|
||||
* 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. Layered now, unlike before this
|
||||
* rewrite: <x-scaffold> used to publish --material-bottom-bar with a Tailwind utility, which
|
||||
* no rule in any layer could outrank regardless of specificity; now scaffold.css publishes it
|
||||
* 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,
|
||||
* not chase a Tailwind utility out of the cascade. Keyed on `data-md-scaffold`, the hook the
|
||||
* scaffold renders on its root, not `data-app-shell`, which that rewrite also retired.
|
||||
*/
|
||||
[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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,214 @@
|
||||
/*
|
||||
* <x-scaffold>: the FAB it places, and the margin its content region already draws.
|
||||
* <x-scaffold>: M3's scaffold — bars, then rails, then panes (docs/reference/m3/foundations.md
|
||||
* § Layout → Scaffold) — as a column: an optional `banner` full width, a row below it holding the
|
||||
* adaptive navigation rail and the content region, the bottom navigation bar on a compact window,
|
||||
* and the snackbar host. Plan step 36 (navigation group) moves in everything the view drew with
|
||||
* Tailwind; `data-md-scaffold-fab` and the content region's `--md-layout-margin` are step 35's,
|
||||
* unchanged below.
|
||||
*
|
||||
* The rest of the scaffold — the rail, the navigation bar, the content region — is still drawn by
|
||||
* its Tailwind classes until its rewrite (plan step 36); this file holds only what step 35 added.
|
||||
* `--material-margin` is M3's window margin, read by a pane and the canonical layouts wherever
|
||||
* `--md-layout-margin` has not already zeroed it: 16px below `medium`, 24px from it
|
||||
* (docs/reference/m3/foundations-supplement.md § Breakpoints), the same two spacing tokens
|
||||
* layout/pane.css reads for its own margin.
|
||||
*
|
||||
* `data-md-scaffold-fab` places the page's FAB as Compose's Scaffold places its floating action
|
||||
* button: fixed at the bottom-end corner (M3's adaptive placement for a FAB, "lower right (bottom
|
||||
* trailing) corner", docs/reference/m3/components-actions-communication-containment.md § FAB →
|
||||
* Behaviour), with M3's margin from the window's edges — 16px below medium and 24px from 600px
|
||||
* (docs/reference/m3/foundations-supplement.md § Breakpoints). `inset-inline-end` is the trailing
|
||||
* edge, so a right-to-left document puts it on the left, clear of the safe area on that side.
|
||||
* `[data-md-skip-link]` hides the way `md-visually-hidden` does at rest (text.css) — duplicated
|
||||
* here rather than shared, because a class cannot be un-applied on `:focus` — and reveals itself
|
||||
* fixed at the window's leading corner, clear of a safe area an application sets, in M3's pill
|
||||
* shape and label-large type, with the elevation-3 shadow every temporary surface above the page
|
||||
* takes.
|
||||
*
|
||||
* From the bottom it clears, in order: the navigation bar, or the bottom safe area where there is
|
||||
* none — `max()`, since `--material-bottom-bar` already includes the safe area (the navigation bar
|
||||
* publishes it on the scaffold below medium); then a snackbar while one shows. <x-toast> sits 16px
|
||||
* above the bar and publishes its height as `--material-snackbar-height` while it is on screen, so
|
||||
* the FAB adds that height and the snackbar's own 16px (`min()` of the height and 16px is 16px when
|
||||
* a snackbar shows and 0px when none does) — M3: "snackbars should appear above FABs — never in
|
||||
* front of or behind one" (§ Snackbar → Placement). It moves there on the spatial spring, which
|
||||
* reduced motion makes instant. z-index 30 is the navigation bar's own layer: over the page, under
|
||||
* a scrim and the snackbar.
|
||||
* `[data-md-scaffold-row]` is flex from `medium` only (the width the rail joins the layout at
|
||||
* instead of sliding over it as a modal) but always takes the remaining height, whichever it is;
|
||||
* `[data-md-scaffold-content]` — the rail's sibling — is always a flex column, so the page's own
|
||||
* bar, the FAB wrapper and `<main>` stack regardless of the row's own display.
|
||||
*
|
||||
* `--md-layout-margin: 0px` on the content region tells a pane or canonical layout inside that the
|
||||
* region already keeps the window margin (layout/pane.css), so neither draws it a second time.
|
||||
* `[data-md-scaffold-actions]`'s column layout is `rail-collapsed:flex-col`, the one Tailwind
|
||||
* variant scaffold.blade.php still used (`resources/css/tailwind.css`, removed with this rule):
|
||||
* written out branch for branch as the navigation rail's own rewrite did for its internal parts
|
||||
* (`resources/css/components/navigation-rail.css`) — the three width-independent conditions in one
|
||||
* `:where()` group, the four width-gated ones each in their own `@media` block, since CSS cannot
|
||||
* merge different queries. Only the "descendant of a collapsed rail" half of each pair applies:
|
||||
* the actions row can never be the rail element itself.
|
||||
*
|
||||
* `[data-md-scaffold-bar]` pins the navigation bar to the window's bottom edge below `medium`, at
|
||||
* the navigation bar's own stacking layer (z-index 30: over the page, under a scrim or the
|
||||
* snackbar — navigation-bar.css and toast.css agree on the number). It also carries
|
||||
* `--material-bottom-bar`, which everything the bar would otherwise cover reads: the FAB
|
||||
* (`[data-md-scaffold-fab]`, below), the snackbar host (toast.css) and the content region's own
|
||||
* bottom padding, all through `max()`/`var()` with a `0px` fallback, so nothing below `medium`, or
|
||||
* once the bar itself is absent, reads a stale height. It stayed a Tailwind utility
|
||||
* (`max-medium:[--material-bottom-bar:…]`, unlayered so it could win over `navigation-bar.css`'s
|
||||
* own override) until this rewrite; now both live in `material.components`
|
||||
* (`navigation-bar.css`'s own header explains why that layer, not this one, is enough) and this
|
||||
* declaration only has to beat `main`'s own `0px` fallback inside the very same layer, which normal
|
||||
* cascade order already does. 64px and 80px are `NavigationBarTokens.ContainerHeight` and
|
||||
* `TallContainerHeight`, the bar's own dimensions, not the spacing scale — the number
|
||||
* navigation-bar.css itself draws the bar at, kept literal here for the same reason.
|
||||
*
|
||||
* Imports the stylesheets of every component this view renders: the adaptive rail and its item and
|
||||
* section, the navigation bar and its item, and the snackbar host.
|
||||
*
|
||||
* In `material.layout`.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './visibility.css';
|
||||
@import '../components/navigation-rail.css';
|
||||
@import '../components/navigation-rail-item.css';
|
||||
@import '../components/navigation-rail-section.css';
|
||||
@import '../components/navigation-bar.css';
|
||||
@import '../components/navigation-bar-item.css';
|
||||
@import '../components/toast.css';
|
||||
|
||||
@layer material.layout {
|
||||
[data-md-scaffold] {
|
||||
--material-margin: var(--md-sys-measurement-space200);
|
||||
|
||||
display: flex;
|
||||
min-block-size: 100dvh;
|
||||
flex-direction: column;
|
||||
background-color: var(--md-sys-color-surface);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
|
||||
@media (width >= 600px) {
|
||||
--material-margin: var(--md-sys-measurement-space300);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-skip-link] {
|
||||
position: absolute;
|
||||
inline-size: 1px;
|
||||
block-size: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
|
||||
&:focus {
|
||||
position: fixed;
|
||||
z-index: 60;
|
||||
inset-inline-start: var(--md-sys-measurement-space200);
|
||||
top: calc(var(--material-safe-top, env(safe-area-inset-top)) + var(--md-sys-measurement-space200));
|
||||
inline-size: auto;
|
||||
block-size: auto;
|
||||
padding: var(--md-sys-measurement-space100) var(--md-sys-measurement-space200);
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
clip-path: none;
|
||||
white-space: normal;
|
||||
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;
|
||||
box-shadow: var(--md-sys-elevation-3);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-scaffold-banner] {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-md-scaffold-row] {
|
||||
flex: 1 1 0%;
|
||||
|
||||
@media (width >= 600px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-scaffold-content] {
|
||||
display: flex;
|
||||
min-inline-size: 0;
|
||||
flex: 1 1 0%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[data-md-scaffold-actions] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--md-sys-measurement-space50);
|
||||
padding-block-start: var(--md-sys-measurement-space100);
|
||||
padding-inline: 20px;
|
||||
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 840px) {
|
||||
@media (width < 1200px) {
|
||||
&:where(:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where([data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-scaffold] main {
|
||||
--md-layout-margin: 0px;
|
||||
|
||||
min-inline-size: 0;
|
||||
flex: 1 1 0%;
|
||||
padding-inline: var(--material-margin);
|
||||
outline: none;
|
||||
|
||||
@media (width < 840px) {
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
padding-block-end: var(--material-bottom-bar);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-scaffold-bar] {
|
||||
position: fixed;
|
||||
z-index: 30;
|
||||
inset-inline: 0;
|
||||
inset-block-end: 0;
|
||||
|
||||
@media (width >= 600px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-scaffold]:has(> [data-md-scaffold-bar] [data-md-navigation-bar]:not([data-md-tall])) {
|
||||
@media (width < 600px) {
|
||||
--material-bottom-bar: calc(64px + var(--material-safe-bottom, env(safe-area-inset-bottom)) + var(--material-bottom-extra, 0px));
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-scaffold]:has(> [data-md-scaffold-bar] [data-md-navigation-bar][data-md-tall]) {
|
||||
@media (width < 600px) {
|
||||
--material-bottom-bar: calc(80px + var(--material-safe-bottom, env(safe-area-inset-bottom)) + var(--material-bottom-extra, 0px));
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-scaffold-fab] {
|
||||
|
||||
@@ -16,50 +16,3 @@
|
||||
|
||||
@import './tokens/theme.css';
|
||||
@import './tokens/utilities.css';
|
||||
|
||||
/*
|
||||
* `rail-collapsed:` — Tailwind's, for the one class left that needs it:
|
||||
* scaffold.blade.php's `data-app-shell-actions` row (`rail-collapsed:flex-col`), stacking the
|
||||
* footer's icon buttons once the rail they sit in narrows. The definition used to live in
|
||||
* navigation.css; that file is gone (plan step 36, navigation group), so this is what is left of
|
||||
* it — updated to the `data-md-*` hooks navigation-rail.css now renders, otherwise identical to
|
||||
* every branch that file's own header still documents. Goes with the scaffold's own rewrite,
|
||||
* whenever `data-app-shell-actions` stops being a Tailwind class list.
|
||||
*/
|
||||
@custom-variant rail-collapsed {
|
||||
&:where([data-md-navigation-rail='collapsed'], [data-md-navigation-rail='collapsed'] *) {
|
||||
@slot;
|
||||
}
|
||||
|
||||
&:where([data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]), [data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
@slot;
|
||||
}
|
||||
|
||||
&:where([data-md-navigation-rail='modal']:not([data-md-open]), [data-md-navigation-rail='modal']:not([data-md-open]) *) {
|
||||
@slot;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
|
||||
@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]) *) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
|
||||
@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]) *) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user