diff --git a/resources/css/components.css b/resources/css/components.css index 0c3c7b29..3ea05df8 100644 --- a/resources/css/components.css +++ b/resources/css/components.css @@ -69,3 +69,4 @@ @import './components/error-page.css'; /* Navigation */ +@import './components/app-bar.css'; diff --git a/resources/css/components/app-bar.css b/resources/css/components/app-bar.css index cf92a5fd..f427eaf0 100644 --- a/resources/css/components/app-bar.css +++ b/resources/css/components/app-bar.css @@ -1,14 +1,25 @@ /* - * M3 Expressive's top app bars (AppBarTokens, AppBarSmallTokens, AppBarMediumFlexibleTokens, - * AppBarLargeFlexibleTokens, androidx Compose Material 3, Apache-2.0). + * ``: 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 — a three-column grid, so it shrinks against whatever is - * beside it rather than ellipsising underneath it + * beside it rather than ellipsising underneath it (N-11: a fixed inset fit exactly one + * of M3's two trailing buttons) * medium 112px (136px with a subtitle): the 64px row of icons over a headline-medium title * large 120px (152px with a subtitle): a display-small title - * search the row holds a search bar, full width to 312px and then half of what is left + * search the row holds a search bar, full width to 312px and then half of what is left (N-09) * * 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 @@ -17,13 +28,22 @@ * 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: 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. + * 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; @@ -32,40 +52,42 @@ 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; @@ -73,63 +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; } /* `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 - (docs/reference/m3/components-navigation-selection-inputs.md § Top app bar → Behaviour and - guidelines). The wrappers stay out of the row's layout until a width hides them. */ - [data-app-bar-action], - [data-app-bar-overflow] { + 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 < 37.5rem) { - [data-app-bar-action][data-overflows-compact], - [data-app-bar-overflow="medium"] { + @media (width < 600px) { + [data-md-app-bar-action][data-md-overflows-compact], + [data-md-app-bar-overflow='medium'] { display: none; } } - @media (width >= 37.5rem) { - [data-app-bar-overflow="compact"] { + @media (width >= 600px) { + [data-md-app-bar-overflow='compact'] { display: none; } } - [data-app-bar-headline] { + [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; @@ -137,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; @@ -148,91 +169,92 @@ /* 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-app-bar][data-variant="center"] [data-app-bar-row] { + buttons, which a fixed inset for one would have let a long title ellipsise underneath (N-11). */ + [data-md-app-bar][data-md-variant='center'] [data-md-app-bar-row] { display: grid; grid-template-columns: 1fr auto 1fr; } - [data-app-bar][data-variant="center"] [data-app-bar-leading] { + [data-md-app-bar][data-md-variant='center'] [data-md-app-bar-leading] { grid-column: 1; justify-self: start; } - [data-app-bar][data-variant="center"] [data-app-bar-headline] { + [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: 0.75rem; + padding-inline: 12px; text-align: center; } - [data-app-bar][data-variant="center"] [data-app-bar-trailing] { + [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 - (docs/reference/m3/components-navigation-selection-inputs.md § Top app bar). 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-app-bar-search] { + wide, and then grows into only half of what is left (N-09). 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(19.5rem + (100% - 19.5rem) / 2); + max-width: calc(312px + (100% - 312px) / 2); flex: 1 1 0%; margin-inline: auto; - padding-inline: 0.25rem; + 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); } diff --git a/resources/css/tailwind.css b/resources/css/tailwind.css index 97ad47d2..68a17ab8 100644 --- a/resources/css/tailwind.css +++ b/resources/css/tailwind.css @@ -19,6 +19,5 @@ /* Navigation — leaving in step 36 */ @import './components/tabs.css'; -@import './components/app-bar.css'; @import './components/navigation.css'; @import './components/toolbar.css'; diff --git a/resources/views/components/app-bar.blade.php b/resources/views/components/app-bar.blade.php index 403fd8a5..6de8608a 100644 --- a/resources/views/components/app-bar.blade.php +++ b/resources/views/components/app-bar.blade.php @@ -16,20 +16,29 @@ trailing icons" (said of the search app bar, the only count it gives for a wider window), and lets actions at the trailing edge "collapse into an overflow menu at smaller breakpoints" and "become visible again at larger sizes" (docs/reference/m3/components-navigation-selection-inputs.md - § Top app bar → Behaviour and guidelines; app-bars/guidelines § Trailing icon buttons, § Large - screens, § Resizing). So below `medium` (600px) the bar shows at most two icon buttons and from - there at most four, the overflow button — `more_vert`, "More options" — counted among them: - with more actions than that, the first one (or three) stay and the rest go into its menu. Each - width has a menu of its own holding only what overflows there, so the arrow keys never land on - a hidden row, and it is all CSS (resources/css/components/app-bar.css): nothing moves after the - page has drawn. Only the list overflows; markup in the slot is drawn as written. A slot and a - list are alternatives — a slot of the same name replaces the attribute. + § Top App Bar → Behaviour and guidelines). So below `medium` (600px) the bar shows at most two + icon buttons and from there at most four, the overflow button — `more_vert`, "More options" — + counted among them: with more actions than that, the first one (or three) stay and the rest go + into its menu. Each width has a menu of its own holding only what overflows there, so the + arrow keys never land on a hidden row, and it is all CSS (resources/css/components/app-bar.css): + nothing moves after the page has drawn. Only the list overflows; markup in the slot is drawn + as written. A slot and a list are alternatives — a slot of the same name replaces the + attribute. Sticky at the top by default (`:sticky="false"` for one that scrolls away), under the top safe area; the surface turns surface-container once content scrolls under it (resources/css/components/app-bar.css, resources/js/app-bar.js). A collapsing bar needs the window to be what scrolls: an ancestor with `overflow: hidden` or `auto` would stop it - sticking (`overflow-x: clip` is fine). --}} + sticking (`overflow-x: clip` is fine). + + The root renders `data-md-app-bar` with `data-md-variant`, `data-md-sticky`, + `data-md-subtitled`, and `data-md-scrolled`/`data-md-collapsed` once the page has scrolled + under it; its row is `data-md-app-bar-row` holding `data-md-app-bar-leading`, + `data-md-app-bar-headline` (`data-md-app-bar-title`, `data-md-app-bar-subtitle`), + `data-md-app-bar-search` and `data-md-app-bar-trailing` (`data-md-app-bar-action`, + `data-md-app-bar-overflow`); a flexible bar's big title is `data-md-app-bar-expanded`. Drawn + by resources/css/components/app-bar.css, which imports button.css, menu.css and menu-item.css + for what the view renders. --}} @props([ 'title' => null, @@ -71,30 +80,30 @@
-
+
@isset($navigation) -
{{ $navigation }}
+
{{ $navigation }}
@endisset @if ($variant === 'search') -
{{ $slot }}
+
{{ $slot }}
@elseif (filled($title)) -