diff --git a/resources/css/components.css b/resources/css/components.css index adc4de27..24ae1582 100644 --- a/resources/css/components.css +++ b/resources/css/components.css @@ -71,3 +71,4 @@ /* Navigation */ @import './components/app-bar.css'; @import './components/toolbar.css'; +@import './components/tabs.css'; diff --git a/resources/css/components/tabs.css b/resources/css/components/tabs.css index 4dc43bc4..dbbb8b85 100644 --- a/resources/css/components/tabs.css +++ b/resources/css/components/tabs.css @@ -1,25 +1,47 @@ /* - * 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). + * ``, ``: M3's tabs (PrimaryNavigationTabTokens.kt, SecondaryNavigationTabTokens.kt, + * androidx Compose Material 3, Apache-2.0; docs/reference/m3/components-navigation-selection-inputs.md + * § Tabs) — 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). + * + * [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-panel] role="tabpanel", hidden server-side until chosen (N-05) * * The active indicator is drawn in every tab and shown under the chosen one, so it is right before * Alpine starts and after a morph: 3px with rounded top corners under the content of a primary tab - * (at least 24px wide, inset 2px at each side), 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), + * (at least 24px wide, inset 2px at each side — N-18), 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 + * 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. + * still sits on the divider (N-20). That makes the bar 10px taller than M3's 48px tab. A scrollable + * set's first tab is offset 52dp from the leading edge, so it reads as scrollable (N-10). A link + * marked `aria-current="page"` (``'s tabs) takes the active indicator and colour + * exactly as `aria-selected="true"` does (N-21). */ -@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; @@ -31,117 +53,126 @@ } /* "Offset the first scrollable tab 52dp from the leading edge so it's clear that more content - is available" (docs/reference/m3/components-navigation-selection-inputs.md § Tabs). Logical, - so a right-to-left page mirrors it. */ - [data-tabs-bar][data-scrollable] { - padding-inline-start: 3.25rem; - scroll-padding-inline-start: 3.25rem; + is available" (N-10). 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; + 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] > li, - [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] { + height: 64px; } /* The state layer covers the tab and the ring's room around it, so the wash meets the divider. */ - [data-tab]::before { - content: ""; + [data-md-tab]::before { + content: ''; position: absolute; 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: 2px; } /* 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-tab]:is([aria-selected="true"], [aria-current="page"]) { + tab, and both take the variant's active colour (N-21). */ + [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] { + [data-md-tab-content] { position: relative; display: inline-flex; height: 100%; - min-width: 1.5rem; + 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); } - /* 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-tab-indicator] { + [data-md-tab-label] { + display: inline-flex; + align-items: center; + gap: var(--md-sys-measurement-space75); + } + + /* Primary indicators are inset 2dp at each side (N-18); both variants sit on the divider, which + the ring's room has pushed below the tab. */ + [data-md-tab-indicator] { position: absolute; inset-inline: 2px; bottom: calc(-1 * var(--tabs-ring)); @@ -154,23 +185,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); + } } diff --git a/resources/css/tailwind.css b/resources/css/tailwind.css index 436e3346..3e05f79e 100644 --- a/resources/css/tailwind.css +++ b/resources/css/tailwind.css @@ -18,5 +18,4 @@ @import './tokens/utilities.css'; /* Navigation — leaving in step 36 */ -@import './components/tabs.css'; @import './components/navigation.css'; diff --git a/resources/js/tabs.js b/resources/js/tabs.js index e27dd670..6a847c17 100644 --- a/resources/js/tabs.js +++ b/resources/js/tabs.js @@ -29,18 +29,18 @@ document.addEventListener('alpine:init', () => { } if (focus) { - this.$refs.bar.querySelector(`[data-tab="${CSS.escape(name)}"]`)?.focus() + this.$refs.bar.querySelector(`[data-md-tab="${CSS.escape(name)}"]`)?.focus() } }, step(by) { const tabs = this.tabs() const rtl = getComputedStyle(this.$refs.bar).direction === 'rtl' - const at = tabs.findIndex((tab) => tab.dataset.tab === this.selected) + const at = tabs.findIndex((tab) => tab.dataset.mdTab === this.selected) const next = tabs[(at + (rtl ? -by : by) + tabs.length) % tabs.length] if (next) { - this.choose(next.dataset.tab, true) + this.choose(next.dataset.mdTab, true) } }, @@ -49,7 +49,7 @@ document.addEventListener('alpine:init', () => { const tab = last ? tabs.at(-1) : tabs[0] if (tab) { - this.choose(tab.dataset.tab, true) + this.choose(tab.dataset.mdTab, true) } }, })) diff --git a/resources/views/components/section-nav.blade.php b/resources/views/components/section-nav.blade.php index 33bb3870..405375b2 100644 --- a/resources/views/components/section-nav.blade.php +++ b/resources/views/components/section-nav.blade.php @@ -62,18 +62,18 @@ @endif