srcdev-nuxt-components 9.3.0 → 9.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.claude/hooks/check-component-update.sh +66 -0
  2. package/.claude/settings.json +1 -1
  3. package/.claude/skills/component-local-style-override.md +45 -0
  4. package/.claude/skills/components/accordian-core.md +26 -5
  5. package/.claude/skills/components/expanding-panel-classic.md +221 -0
  6. package/.claude/skills/components/expanding-panel.md +8 -8
  7. package/.claude/skills/components/navigation-items.md +51 -0
  8. package/.claude/skills/components/responsive-header.md +161 -0
  9. package/.claude/skills/components/site-header.md +98 -0
  10. package/.claude/skills/index.md +6 -1
  11. package/.claude/skills/page-transitions.md +116 -0
  12. package/.claude/skills/theming-form-geometry-tokens.md +12 -0
  13. package/.claude/skills/theming-override-default.md +14 -0
  14. package/.claude/skills/theming-partial-override.md +17 -0
  15. package/.vscode/srcdev-component-accordian-core.code-snippets +74 -0
  16. package/.vscode/srcdev-component-expanding-panel-classic.code-snippets +121 -0
  17. package/.vscode/srcdev-component-expanding-panel.code-snippets +1 -3
  18. package/.vscode/srcdev-component-responsive-header.code-snippets +46 -0
  19. package/.vscode/srcdev-component-site-header.code-snippets +74 -0
  20. package/app/components/02.molecules/expandable/accordian/AccordianCore.vue +19 -11
  21. package/app/components/02.molecules/expandable/accordian/CONSUMER-STYLING.md +93 -0
  22. package/app/components/02.molecules/expandable/accordian/stories/AccordianCore.stories.ts +31 -0
  23. package/app/components/02.molecules/expandable/accordian/tests/AccordianCore.spec.ts +31 -0
  24. package/app/components/02.molecules/expandable/accordian/tests/__snapshots__/AccordianCore.spec.ts.snap +7 -21
  25. package/app/components/02.molecules/expandable/expanding-panel/CONSUMER-STYLING.md +10 -10
  26. package/app/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue +35 -37
  27. package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts +5 -6
  28. package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +5 -13
  29. package/app/components/02.molecules/expandable/expanding-panel-classic/CONSUMER-STYLING.md +103 -0
  30. package/app/components/02.molecules/expandable/expanding-panel-classic/ExpandingPanelClassic.vue +191 -0
  31. package/app/components/02.molecules/expandable/expanding-panel-classic/stories/ExpandingPanelClassic.stories.ts +293 -0
  32. package/app/components/02.molecules/expandable/expanding-panel-classic/tests/ExpandingPanelClassic.spec.ts +514 -0
  33. package/app/components/02.molecules/expandable/expanding-panel-classic/tests/__snapshots__/ExpandingPanelClassic.spec.ts.snap +59 -0
  34. package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +66 -0
  35. package/app/components/{responsive-header → 03.organisms/responsive-header}/NavigationItems.vue +21 -28
  36. package/app/components/{responsive-header → 03.organisms/responsive-header}/ResponsiveHeader.vue +141 -43
  37. package/app/components/03.organisms/responsive-header/stories/NavigationItems.stories.ts +80 -0
  38. package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +122 -0
  39. package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +155 -0
  40. package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +319 -0
  41. package/app/components/03.organisms/responsive-header/tests/__snapshots__/NavigationItems.spec.ts.snap +34 -0
  42. package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +72 -0
  43. package/app/components/03.organisms/site-header/CONSUMER-STYLING.md +60 -0
  44. package/app/components/03.organisms/site-header/SiteHeader.vue +96 -0
  45. package/app/components/03.organisms/site-header/stories/SiteHeader.stories.ts +135 -0
  46. package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +103 -0
  47. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +15 -0
  48. package/app/components/05.forms/input-select/CONSUMER-STYLING.md +42 -0
  49. package/app/components/05.forms/input-select/InputSelectCore.vue +21 -9
  50. package/app/components/05.forms/input-text/CONSUMER-STYLING.md +60 -0
  51. package/app/components/05.forms/input-text/InputTextCore.vue +17 -8
  52. package/app/layouts/default.vue +64 -67
  53. package/package.json +4 -1
@@ -0,0 +1,161 @@
1
+ # ResponsiveHeader Component
2
+
3
+ ## Overview
4
+
5
+ `ResponsiveHeader` is an adaptive main-navigation bar: it measures each nav item's real
6
+ rendered width on mount (and on resize), decides which items fit, and collapses whatever
7
+ doesn't fit into a burger/overflow dropdown rendered by `NavigationItems`. Supports
8
+ multi-level dropdowns (`childLinks`) at both the top-bar and overflow-panel level.
9
+
10
+ Most consumers should reach for [`SiteHeader`](site-header.md) instead, which wraps this
11
+ component together with `PageRow` and `SkipLinks` — use `ResponsiveHeader` directly only if
12
+ you need a different outer page-header structure.
13
+
14
+ [`NavigationItems`](navigation-items.md) is an internal component rendered inside
15
+ `ResponsiveHeader`'s overflow panel — it's not meant to be used standalone in a real app,
16
+ though it's independently testable/storyable.
17
+
18
+ ---
19
+
20
+ ## The measurement pipeline (read this before touching layout/CSS here)
21
+
22
+ On mount, a two-phase pass measures `navigationWrapperRects`/`secondaryNavRects`/each nav
23
+ item's `getBoundingClientRect()`/`offsetWidth`, then marks each item's `config.visible`.
24
+ Items with `visible: false` get a `visually-hidden` class in the top bar and are handed to
25
+ `NavigationItems` to render in the overflow panel instead. The pass re-runs on
26
+ `ResizeObserver` events targeting the root `.navigation` element, and once more after
27
+ `document.fonts.ready` resolves (a fonts-still-loading correction).
28
+
29
+ **This pipeline only re-measures when the *observed wrapper's own box size* changes.** Two
30
+ known failure modes, both fixed 2026-08-21 — keep them in mind before adding new nav content:
31
+
32
+ 1. **Unsized async content (icons).** The chevron and any `iconName` decorator icons render
33
+ via the `Icon` component, which resolves its SVG asynchronously. If an icon element has no
34
+ reserved `width`/`height`, it measures at ~0 during the pass and then pops in and widens
35
+ the item afterward — with nothing to detect it, since the wrapper's box didn't change size.
36
+ `.decorator-icon` and the chevron `.icon` both reserve `1.35em` square for this reason.
37
+ Any new icon-bearing element added to a nav item must reserve its own size too.
38
+ 2. **Viewport-relative (`vw`/`clamp()`) font-size.** `html` has `scrollbar-gutter: stable`,
39
+ which keeps the wrapper's own box width constant when the scrollbar toggles — but does
40
+ nothing to stabilise the `vw` unit itself, so a fluid ancestor font-size can drift nav-item
41
+ text width without ever re-triggering a re-measurement. This is why
42
+ `--responsive-header-link-font-size` exists (see tokens below) — set it to a fixed value
43
+ rather than leaving it to inherit a fluid ancestor font-size.
44
+
45
+ ---
46
+
47
+ ## The dropdown "safe triangle" (read this before touching hover-close logic)
48
+
49
+ `handleNavigationItemHover()` used to close every open top-bar dropdown the instant the mouse
50
+ entered **any** `.main-navigation-item` — including a sibling item the cursor only crossed in
51
+ transit. Moving the mouse diagonally from a dropdown's summary down into its own
52
+ `.main-navigation-sub-nav` panel briefly dips through the 12px gap between them
53
+ (`.main-navigation-sub-nav`'s `translate: 0 12px`), and at some cursor angles through a
54
+ neighbouring item's hit area — closing the dropdown before the user reached it.
55
+
56
+ **A pure-CSS "safe triangle" bridge (`clip-path` on a pseudo-element) was tried first and
57
+ reverted — don't reintroduce it without solving the problem below first.** For such a bridge to
58
+ escape `.main-navigation-item`'s `overflow: hidden` the same way `.main-navigation-sub-nav`
59
+ does, its *containing block* must resolve to `.navigation` (the only positioned ancestor above
60
+ the clipping box). But `.main-navigation-sub-nav` only manages this because it has no explicit
61
+ `top`/`left` (containing-block choice is irrelevant to where it visually lands) — a bridge
62
+ placed to *visually* sit under a specific trigger needs `top`/`left`/`inline-size` that resolve
63
+ against something local, which forces it onto a positioned ancestor, which is always either
64
+ already inside the `overflow: hidden` box (gets clipped, invisible, protects nothing) or itself
65
+ becomes the sub-nav's new containing block (moves the *panel* inside the clipped box instead —
66
+ the first attempt's mistake). A commented-out `/* position: relative; */` still sitting in the
67
+ `:last-child` override is evidence someone hit this exact trap once before, independently.
68
+ Neither approach is fixable without restructuring how the panel escapes clipping, which the
69
+ collapse-measurement pipeline depends on.
70
+
71
+ **Fixed (2026-08-21) with a JS hover-intent delay instead.** `handleNavigationItemHover()` now
72
+ calls `scheduleCloseAllNavigationDetails()`, which delays the actual close by
73
+ `HOVER_CLOSE_DELAY` (200ms) rather than firing it immediately. Two things cancel the pending
74
+ close before it fires:
75
+
76
+ - `handleSummaryHover()` (reaching a summary — cancels, then does its own close-others/toggle)
77
+ - a `mouseenter` on `.main-navigation-sub-nav` itself, via `handleSubNavHover()`
78
+
79
+ So a brief diagonal dip through a sibling's hit area survives (the close never actually runs
80
+ before the cursor reaches its destination), while genuinely moving to a different part of the
81
+ page still closes the dropdown promptly. `closeAllTimer` is cleared `onUnmounted` to avoid a
82
+ stray close firing against stale refs after the component's gone.
83
+
84
+ **`handleSummaryHover` never toggles — it only ever ensures the summary it's called on is
85
+ open.** A real mouse click moves focus to the clicked element *before* the click event fires, so
86
+ clicking a summary the mouse had just hover-opened dispatches both a `focusin` (→
87
+ `handleSummaryHover`) and a `click` (→ `handleSummaryAction`) in quick succession. If
88
+ `handleSummaryHover` toggled (as it did until 2026-08-21), that focusin would flip an
89
+ already-open item closed, and the click's own toggle would immediately flip it back open — a
90
+ visible open→closed→open flicker on every click, and the click effectively did nothing. Only
91
+ `handleSummaryAction` (the explicit click) is allowed to close a dropdown; hover/focus is
92
+ idempotent-safe to fire redundantly. `@vue/test-utils`' `.trigger("click")` doesn't synthesize
93
+ this implicit focus side effect on its own — a test asserting click-to-close must explicitly
94
+ `.trigger("focusin")` before `.trigger("click")` to reproduce it, or it'll pass against the
95
+ buggy toggle-based version too.
96
+
97
+ ---
98
+
99
+ ## Props reference
100
+
101
+ | Prop | Type | Default | Notes |
102
+ |------|------|---------|-------|
103
+ | `responsiveNavLinks` | `ResponsiveHeaderProp` (`{ [groupKey]: ResponsiveHeaderNavItem[] }`) | `{}` | Nav groups. Each item is either a link (`path`) or a dropdown (`childLinksTitle` + `childLinks`). A single group with no dropdowns is valid for simple sites. |
104
+ | `gapBetweenFirstAndSecondNav` | `number` | `12` | Pixel gap reserved between the first and second nav groups (also factored into the overflow-collapse width math). |
105
+ | `overflowDetailsSummaryIcons` | `Record<string, string>` | `{ more: "gravity-ui:ellipsis", burger: "gravity-ui:bars" }` | Icon names for the overflow button's two states: `more` shows when only *some* items collapsed, `burger` shows when `allowNavigationCollapse` is active (whole nav collapsed). |
106
+ | `collapseBreakpoint` | `number \| null` | `null` | A fixed pixel width below which the whole main nav collapses into the overflow burger, instead of the default per-item responsive collapse. |
107
+ | `collapseAtMainNavIntersection` | `boolean` | `false` | Like `collapseBreakpoint`, but the breakpoint is derived automatically from the main nav's own measured width rather than a fixed number. |
108
+ | `allowExpandOnGesture` | `boolean` | `true` | When `true`, hovering/focusing a dropdown summary opens it (in addition to click). When `false`, only click toggles it. |
109
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `.navigation` element. |
110
+
111
+ ## Slots
112
+
113
+ | Slot | Purpose |
114
+ |------|---------|
115
+ | `#secondaryNavigation` | Extra content rendered after the overflow burger button (e.g. a settings icon link). Only rendered when the slot is provided. |
116
+
117
+ ---
118
+
119
+ ## Public CSS token API
120
+
121
+ All tokens are read via `var(--token, default)` — see the full list in the component's own
122
+ `<style>` block comment. Highlights:
123
+
124
+ | Token | Default | Controls |
125
+ |---|---|---|
126
+ | `--responsive-header-link-font-size` | `inherit` | Nav-link font-size. **Set this to a fixed value** — see the measurement-pipeline note above for why leaving it `inherit` from a fluid ancestor is a footgun. |
127
+ | `--responsive-header-link-color` | `inherit` | Link/summary text colour. |
128
+ | `--responsive-header-bg` / `--responsive-header-padding-*` / `--responsive-header-border*` | transparent / `0` / `none` | Root element theming. |
129
+ | `--responsive-header-overflow-btn-*` | various | Overflow burger button sizing/colour. |
130
+ | `--responsive-header-sub-nav-*` / `--responsive-header-overflow-nav-*` | various | Top-bar dropdown panel and overflow-panel container theming. |
131
+ | `--responsive-nav-decorator-indicator-color` / `--responsive-nav-decorator-hovered-*` | `currentColor` / inherits | The sliding active/hover indicator bar under the main nav. |
132
+
133
+ ---
134
+
135
+ ## Usage example
136
+
137
+ ```vue
138
+ <script setup lang="ts">
139
+ const responsiveNavLinks = {
140
+ firstNav: [
141
+ { name: "Home", path: "/" },
142
+ {
143
+ name: "Components",
144
+ childLinksTitle: "UI Components",
145
+ childLinks: [{ name: "Buttons", path: "/forms/examples/buttons" }],
146
+ },
147
+ ],
148
+ secondNav: [{ name: "Contact", path: "/contact" }],
149
+ };
150
+ </script>
151
+
152
+ <template>
153
+ <ResponsiveHeader :responsive-nav-links="responsiveNavLinks" :style-class-passthrough="['site-header-nav']">
154
+ <template #secondaryNavigation>
155
+ <NuxtLink to="/settings" aria-label="Settings">
156
+ <Icon name="material-symbols:settings-outline-rounded" />
157
+ </NuxtLink>
158
+ </template>
159
+ </ResponsiveHeader>
160
+ </template>
161
+ ```
@@ -0,0 +1,98 @@
1
+ # SiteHeader Component
2
+
3
+ ## Overview
4
+
5
+ `SiteHeader` composes the standard page-header trio into one drop-in component: `PageRow` (header row wrapper) + `SkipLinks` (a11y skip-to-content/footer anchors with a home-link slot) + `ResponsiveHeader` (the adaptive/overflow-collapsing nav with dropdowns). It replaces manually assembling those three in every consuming app's `layouts/default.vue`.
6
+
7
+ This is **not** a classic multi-column "mega menu" (large flyout panels with images/promo blocks) — it's the existing `ResponsiveHeader` nav (main links, dropdown sub-nav, overflow burger collapse) wrapped for reuse. A single flat nav group with no dropdowns works fine too (see the `SimpleFlatNav` story) — you don't need to use the dropdown/overflow behaviour to benefit from the composition.
8
+
9
+ Branding (logo/wordmark) is entirely consumer-authored via the `#branding` slot — `SiteHeader` has no opinion on brand markup or colours.
10
+
11
+ ---
12
+
13
+ ## Props reference
14
+
15
+ | Prop | Type | Default | Notes |
16
+ |------|------|---------|-------|
17
+ | `responsiveNavLinks` | `ResponsiveHeaderProp` | `{}` | Forwarded to `ResponsiveHeader`. Keyed nav groups; each item is either a link (`path`) or a dropdown (`childLinksTitle` + `childLinks`). A single group with no dropdowns is valid for simple sites. |
18
+ | `gapBetweenFirstAndSecondNav` | `number` | `12` | Forwarded to `ResponsiveHeader`. |
19
+ | `overflowDetailsSummaryIcons` | `Record<string, string>` | `{ more: "gravity-ui:ellipsis", burger: "gravity-ui:bars" }` | Forwarded to `ResponsiveHeader`. |
20
+ | `collapseBreakpoint` | `number \| null` | `null` | Forwarded to `ResponsiveHeader`. |
21
+ | `collapseAtMainNavIntersection` | `boolean` | `false` | Forwarded to `ResponsiveHeader`. |
22
+ | `allowExpandOnGesture` | `boolean` | `true` | Forwarded to `ResponsiveHeader`. |
23
+ | `pageRowVariant` | `"full" \| "popout" \| "content" \| "inset-content"` | `"content"` | Forwarded to the root `PageRow`'s `variant` prop. |
24
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes on the root `PageRow` — targets `--site-header-*` tokens (see `CONSUMER-STYLING.md`). |
25
+ | `navStyleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes on the nested `ResponsiveHeader` — targets its own `--responsive-header-*`/`--overflow-nav-*` tokens. |
26
+
27
+ ---
28
+
29
+ ## Slots
30
+
31
+ | Slot | Purpose |
32
+ |------|---------|
33
+ | `#branding` | Logo/wordmark markup, rendered inside `SkipLinks`' `#homeLink` slot. Consumer supplies the `NuxtLink`/`NuxtImg`/text — `SiteHeader` doesn't wrap or style it. |
34
+ | `#secondaryNavigation` | Forwarded through to `ResponsiveHeader`'s own `#secondaryNavigation` slot (e.g. a settings icon link). Only rendered when provided. |
35
+
36
+ ---
37
+
38
+ ## Usage examples
39
+
40
+ ### Full nav with dropdowns (mega-menu-style)
41
+
42
+ ```vue
43
+ <SiteHeader
44
+ :responsive-nav-links="responsiveNavLinks"
45
+ :style-class-passthrough="['header']"
46
+ :nav-style-class-passthrough="['site-header-nav']"
47
+ >
48
+ <template #branding>
49
+ <NuxtLink to="/" class="home-link">Brand</NuxtLink>
50
+ </template>
51
+ </SiteHeader>
52
+ ```
53
+
54
+ ### Simple flat nav (few pages, no dropdowns)
55
+
56
+ ```vue
57
+ <script setup lang="ts">
58
+ const responsiveNavLinks = {
59
+ main: [
60
+ { name: "Home", path: "/" },
61
+ { name: "About", path: "/about" },
62
+ { name: "Services", path: "/services" },
63
+ { name: "Contact", path: "/contact" },
64
+ ],
65
+ };
66
+ </script>
67
+
68
+ <template>
69
+ <SiteHeader :responsive-nav-links="responsiveNavLinks" :style-class-passthrough="['header']">
70
+ <template #branding>
71
+ <NuxtLink to="/" class="home-link">Brand</NuxtLink>
72
+ </template>
73
+ </SiteHeader>
74
+ </template>
75
+ ```
76
+
77
+ ### With a secondary navigation slot
78
+
79
+ ```vue
80
+ <SiteHeader :responsive-nav-links="responsiveNavLinks">
81
+ <template #branding>
82
+ <NuxtLink to="/" class="home-link">Brand</NuxtLink>
83
+ </template>
84
+ <template #secondaryNavigation>
85
+ <NuxtLink to="/settings" aria-label="Settings">
86
+ <Icon name="material-symbols:settings-outline-rounded" />
87
+ </NuxtLink>
88
+ </template>
89
+ </SiteHeader>
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Migration notes
95
+
96
+ - Consuming layouts previously assembling `PageRow` + `SkipLinks` + `ResponsiveHeader` (or `TabNavigation`) by hand can switch to `SiteHeader` directly; the two `styleClassPassthrough` props map 1:1 onto the two independent passthrough hooks that pattern used.
97
+ - `TabNavigation`'s `NavItemData` shape (`{ main: [{ text, href, iconName, cssName }] }`) is **not** the same shape as `ResponsiveHeaderProp` (`{ groupKey: [{ name, path, childLinksTitle, childLinks }] }`) — migrating a `TabNavigation` consumer means reshaping the nav data, not just swapping the component. `TabNavigation`'s `navAlign` prop has no `SiteHeader` equivalent since `ResponsiveHeader`'s layout is fixed (branding column + nav column).
98
+ - Requires the consuming layout's main/footer regions to carry `id="main-content"` / `id="footer-content"` — same requirement as using `SkipLinks` directly.
@@ -36,6 +36,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
36
36
  ├── component-dynamic-slots.md — named dynamic slots ($slots iteration) vs indexed dynamic slots (itemCount pattern)
37
37
  ├── component-local-style-override.md — styleClassPassthrough + scoped style block for per-usage visual customisation
38
38
  ├── component-prop-driven-container-layout.md — vary CSS grid layout inside @container queries using data-* attribute selectors
39
+ ├── page-transitions.md — pageTransition/layoutTransition setup; the self-wrapped <NuxtLayout> + layout:false anti-pattern that pulls the header/nav into the transition boundary; fade CSS
39
40
  ├── css-nesting-conventions.md — native CSS nesting rules: why &__child Sass BEM concatenation silently breaks, correct patterns
40
41
  ├── css-grid-max-width-gutters.md — cap a centre grid column width by growing gutters, with start/center alignment variants
41
42
  ├── css-animation-utilities.md — scroll-driven animation utility classes: scroller-x (carousel), entry-zoom-reveal, entry-slide-in, entry-exit-blur, auto-rotate
@@ -98,7 +99,11 @@ Each skill is a single markdown file named `<area>-<task>.md`.
98
99
  ├── carousel-flip.md — CarouselFlip: FLIP-animated carousel, carouselDataIds slot API, buttonLayout variants (sides/controls-flanking/controls-grouped-right/overlay), CSS tokens
99
100
  ├── samaritan-prompt-mixed.md — SamaritanPromptMixed: animated text prompt, typewriter/word-pulse effects, MessageConfig API, aria-live accessibility, CSS tokens
100
101
  ├── display-toast.md — DisplayToast (standalone v-model) + DisplayToastProvider + useToastQueue (app-wide queue): stacking, FLIP dismiss, maxVisible, SemanticTheme × 4, masked SVG glass variant
101
- └── display-prompt.md — DisplayPrompt: inline notification banner, SemanticTheme × 4, local vs parent-controlled dismiss, outlined modifier, CSS token override
102
+ ├── display-prompt.md — DisplayPrompt: inline notification banner, SemanticTheme × 4, local vs parent-controlled dismiss, outlined modifier, CSS token override
103
+ ├── expanding-panel-classic.md — ExpandingPanelClassic: grid-template-rows animation (no Baseline-2025 dependency), same API as ExpandingPanel, cross-browser animation parity trade-off
104
+ ├── site-header.md — SiteHeader: PageRow + SkipLinks + ResponsiveHeader composition, #branding/#secondaryNavigation slots, dual styleClassPassthrough hooks
105
+ ├── responsive-header.md — ResponsiveHeader: overflow-collapsing adaptive nav, measurement-pipeline gotchas (unsized icons, vw font-size drift), full CSS token API
106
+ └── navigation-items.md — NavigationItems: internal overflow-panel renderer for ResponsiveHeader, complement-visibility logic, not used standalone
102
107
  ```
103
108
 
104
109
  ## Skill file template
@@ -0,0 +1,116 @@
1
+ # Page Transitions
2
+
3
+ ## Overview
4
+
5
+ `pageTransition`/`layoutTransition` in `nuxt.config.ts` name the Vue `<Transition>` that wraps
6
+ `<NuxtPage>`/`<NuxtLayout>`. Getting the app's template structure wrong here means the whole page —
7
+ header, nav, footer included — fades/animates on every navigation, not just the routed content.
8
+ This is silent: no build error, no warning, it just looks visually wrong ("the header flickers/
9
+ fades too") once a transition actually has a visible duration.
10
+
11
+ ## The anti-pattern
12
+
13
+ Each page self-wraps its own content in `<NuxtLayout name="default">` and sets
14
+ `definePageMeta({ layout: false })` to stop Nuxt's automatic layout wrapping:
15
+
16
+ ```vue
17
+ <!-- pages/contact.vue — WRONG -->
18
+ <template>
19
+ <div>
20
+ <NuxtLayout name="default">
21
+ <div class="hero">...</div>
22
+ </NuxtLayout>
23
+ </div>
24
+ </template>
25
+
26
+ <script setup lang="ts">
27
+ definePageMeta({ layout: false })
28
+ </script>
29
+ ```
30
+
31
+ This often isn't a deliberate choice — it's commonly left over from a Nuxt 3→4 directory-structure
32
+ migration, or copied from a page that had a real reason to opt out of the layout (rare). Once it's
33
+ in one page it tends to get copy-pasted to every page.
34
+
35
+ **Why it's wrong**: `<NuxtLayout name="default">` here is INSIDE the page component's own render
36
+ tree. Since `<NuxtPage>` (wrapped in the `pageTransition`) renders the matched page component as
37
+ its child, the layout — including the header, nav, and footer defined in `layouts/default.vue` —
38
+ becomes part of the transitioning subtree. Every navigation unmounts and remounts the *entire*
39
+ layout, and it visually fades/animates along with the page content.
40
+
41
+ ## The correct pattern
42
+
43
+ Don't set `layout: false`. Don't wrap page content in `<NuxtLayout>` inside the page itself. Let
44
+ Nuxt's default `app.vue` (no need to write your own) apply the layout once, persistently, at the
45
+ app level:
46
+
47
+ ```vue
48
+ <!-- pages/contact.vue — CORRECT -->
49
+ <template>
50
+ <div class="contact-page-content">
51
+ <div class="hero">...</div>
52
+ </div>
53
+ </template>
54
+
55
+ <script setup lang="ts">
56
+ // no definePageMeta({ layout: false }) needed
57
+ </script>
58
+ ```
59
+
60
+ `layouts/default.vue` itself needs no changes — it already receives page content via its
61
+ `<slot name="default">` (or plain `<slot>`), which is exactly what Nuxt's automatic wrapping
62
+ provides.
63
+
64
+ With this structure, `pageTransition` only ever wraps the page component's own root element
65
+ (`.contact-page-content` etc.) — the header/nav/footer live in the persistent `<NuxtLayout>`
66
+ *outside* `<NuxtPage>`, so they never unmount, never fade, and stay visually static across every
67
+ navigation. Verified via Playwright: with a real transition duration in place, the header DOM node
68
+ and its computed `opacity` stay unchanged for the header across the whole navigation, while only
69
+ the page-content element's class list cycles through
70
+ `page-leave-from` → `page-leave-active`/`page-leave-to` → `page-enter-from`/`page-enter-active` →
71
+ `page-enter-to`.
72
+
73
+ ## Migrating an app off the anti-pattern
74
+
75
+ For every page:
76
+
77
+ 1. Remove `definePageMeta({ layout: false })` (delete the whole call if that was its only option).
78
+ 2. Remove the `<NuxtLayout name="default">`/`</NuxtLayout>` wrapper tags from the template,
79
+ de-indenting the content that was inside them by one level. Keep the page's own outer element
80
+ (add a scoping class here if it didn't have one — see the body-class warning in
81
+ `component-local-style-override.md`, since this is also the right place to move any
82
+ `bodyAttrs.class`-scoped local `<style>` to).
83
+
84
+ Verify with `npx tsc --noEmit` (or `nuxt build`) afterward — a stray extra `</NuxtLayout>` or
85
+ mismatched brace is easy to leave behind when doing this across many files by hand.
86
+
87
+ ## Giving the transition an actual CSS effect
88
+
89
+ `pageTransition: { name: "page", mode: "out-in" }` alone does nothing visible — Vue's `<Transition>`
90
+ needs matching CSS classes or it's an instant, unanimated swap. A simple fade:
91
+
92
+ ```css
93
+ .page-enter-active,
94
+ .page-leave-active {
95
+ transition: opacity 0.2s ease;
96
+ }
97
+ .page-enter-from,
98
+ .page-leave-to {
99
+ opacity: 0;
100
+ }
101
+
102
+ @media (prefers-reduced-motion: reduce) {
103
+ .page-enter-active,
104
+ .page-leave-active {
105
+ transition: none;
106
+ }
107
+ }
108
+ ```
109
+
110
+ `mode: "out-in"` fades the old page fully out before the new one fades in — no overlap, no need for
111
+ `position: absolute` tricks during the transition.
112
+
113
+ ## See also
114
+
115
+ - `component-local-style-override.md` — the body-class race this pattern also exposes, and the
116
+ `{name}-page-content` naming convention used above
@@ -17,6 +17,18 @@ imported before or after this layer's `main.css` in `nuxt.config.ts`'s `css` arr
17
17
  always wins either way. `theming-override-default.md`/`theming-partial-override.md` already rely
18
18
  on this same mechanism for colour tokens; it applies identically here.
19
19
 
20
+ > **⚠️ Do not "fix" this by wrapping your override in `@layer consumer`** to mirror the library's
21
+ > own `@layer reset, colours, theming, form-tokens, typography, a11y, components, utilities,
22
+ > consumer;` stack — it looks like the intended slot but is unsafe in practice. Cascade layer
23
+ > priority is fixed by whichever layer name is first referenced *anywhere in the document*, not by
24
+ > its position in that pre-declaration. Nuxt/Nitro inlines many small per-component/per-page CSS
25
+ > chunks as `<style>` tags directly in `<head>` for SSR performance — if an inlined chunk declares
26
+ > `@layer consumer` before the library's main stylesheet (which carries the master statement) loads
27
+ > via its `<link>`, `consumer` gets registered first and pushed to the *lowest* priority instead of
28
+ > the highest, so `@layer form-tokens`/`theming` silently win instead, with no build error.
29
+ > Confirmed by reproducing against a real `npm run build` + preview, not just `nuxt dev`. Stay
30
+ > unlayered — that's what makes the guarantee above hold.
31
+
20
32
  So: to change one geometry value, redeclare just that one custom property in your app's own CSS
21
33
  (e.g. `app/assets/styles/setup/03.theming/_default.css`, or a new file imported the same way).
22
34
  There is no dependency between these tokens — overriding `--button-padding-inline` alone does not
@@ -16,6 +16,20 @@ For the full architecture see `theming-colour-ramps.md`.
16
16
 
17
17
  ## Steps
18
18
 
19
+ > **⚠️ Keep every file in this skill unlayered — never wrap them in `@layer consumer` (or any
20
+ > named layer)**, even though the library reserves `consumer` as the last name in its own
21
+ > `@layer reset, colours, theming, form-tokens, typography, a11y, components, utilities, consumer;`
22
+ > stack. Cascade layer priority is fixed by whichever layer name is first referenced *anywhere in
23
+ > the document*, not by its position in that pre-declaration. Nuxt/Nitro inlines many small
24
+ > per-component/per-page CSS chunks as `<style>` tags directly in `<head>` for SSR performance —
25
+ > if one of those happens to declare `@layer consumer` before the library's main stylesheet (which
26
+ > carries the master `@layer` statement) loads via its `<link>`, `consumer` gets registered first
27
+ > and pushed to the *lowest* priority, so `@layer theming` silently wins instead — e.g. every
28
+ > button reverting to the library's default blue with no build error. Confirmed by reproducing
29
+ > against a real `npm run build` + preview, not just `nuxt dev`. Unlayered CSS always wins over
30
+ > every named layer regardless of document order, which is what makes the "wins the cascade
31
+ > automatically" claim below actually true.
32
+
19
33
  ### 1. Create `ramps.config.mjs` in your project root
20
34
 
21
35
  Define the palettes you want. You can add brand-new ones, reuse a built-in name to replace
@@ -14,6 +14,23 @@ All library tokens are declared inside `@layer theming`. Any CSS written outside
14
14
  automatically — no `!important` or specificity tricks required. Override files just need to load
15
15
  after the layer styles.
16
16
 
17
+ > **⚠️ Never wrap your override CSS in a named `@layer` (e.g. `@layer consumer`), even though the
18
+ > library reserves `consumer` as the last name in its own `@layer reset, colours, theming,
19
+ > form-tokens, typography, a11y, components, utilities, consumer;` stack and it looks like the
20
+ > "correct" slot to write into.
21
+ >
22
+ > Cascade layer *priority* order is fixed by whichever layer name is first referenced anywhere in
23
+ > the document — not by where it sits in a later `@layer a, b, c;` pre-declaration. Nuxt/Nitro
24
+ > inlines many small per-component/per-page CSS chunks as `<style>` tags directly in `<head>` for
25
+ > SSR performance. If an inlined chunk declares `@layer consumer { ... }` before the library's
26
+ > main stylesheet (which carries the master `@layer` statement) has loaded via its `<link>`,
27
+ > `consumer` gets registered *first* — pushed to the *lowest* priority instead of the intended
28
+ > highest — and `@layer theming`/`@layer components` silently win instead, with no build error.
29
+ > This reproduces in a real `npm run build` + preview, not just `nuxt dev`.
30
+ >
31
+ > Keep all consumer-app override CSS **unlayered**. Unlayered CSS always wins over every named
32
+ > layer regardless of document/inlining order, which is what "wins automatically" above relies on.
33
+
17
34
  ## Semantic slots — the tokens to override
18
35
 
19
36
  All themed components share an 11-slot vocabulary (see `_theme-slots.css`). Overriding these
@@ -0,0 +1,74 @@
1
+ {
2
+ "SRCDEV AccordianCore Basic": {
3
+ "description": "AccordianCore grouped into an exclusive-open accordion (shared name)",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<AccordianCore name=\"$1faq\" :item-count=\"$2\">",
7
+ " <template #accordian-0-summary>",
8
+ " <span>$3Question one?</span>",
9
+ " </template>",
10
+ " <template #accordian-0-content>",
11
+ " <p>$4Answer one.</p>",
12
+ " </template>",
13
+ "",
14
+ " <template #accordian-1-summary>",
15
+ " <span>$5Question two?</span>",
16
+ " </template>",
17
+ " <template #accordian-1-content>",
18
+ " <p>$6Answer two.</p>",
19
+ " </template>",
20
+ "</AccordianCore>"
21
+ ]
22
+ },
23
+ "SRCDEV AccordianCore Classic Variant": {
24
+ "description": "AccordianCore using ExpandingPanelClassic panels (animation runs in every browser)",
25
+ "scope": "vue,html",
26
+ "body": [
27
+ "<AccordianCore name=\"$1faq\" :item-count=\"$2\" variant=\"classic\">",
28
+ " <template #accordian-0-summary>",
29
+ " <span>$3Question one?</span>",
30
+ " </template>",
31
+ " <template #accordian-0-content>",
32
+ " <p>$4Answer one.</p>",
33
+ " </template>",
34
+ "",
35
+ " <template #accordian-1-summary>",
36
+ " <span>$5Question two?</span>",
37
+ " </template>",
38
+ " <template #accordian-1-content>",
39
+ " <p>$6Answer two.</p>",
40
+ " </template>",
41
+ "</AccordianCore>"
42
+ ]
43
+ },
44
+ "SRCDEV AccordianCore Programmatic Slots": {
45
+ "description": "AccordianCore with slots generated from an items array via v-for",
46
+ "scope": "vue,html",
47
+ "body": [
48
+ "<AccordianCore :item-count=\"$1items.length\">",
49
+ " <template v-for=\"(item, i) in $1items\" #[`accordian-\\${i}-summary`] :key=\"i\">",
50
+ " <span>{{ item.$2title }}</span>",
51
+ " </template>",
52
+ " <template v-for=\"(item, i) in $1items\" #[`accordian-\\${i}-content`] :key=\"i\">",
53
+ " <p>{{ item.$3body }}</p>",
54
+ " </template>",
55
+ "</AccordianCore>"
56
+ ]
57
+ },
58
+ "SRCDEV AccordianCore CSS Override": {
59
+ "description": "CSS override scaffold for AccordianCore panel-level styling",
60
+ "scope": "css",
61
+ "body": [
62
+ ".$1my-accordian {",
63
+ " .display-accordian {",
64
+ " /* max-width: none; */",
65
+ "",
66
+ " .accordian-item.expanding-panel {",
67
+ " /* border-block-end: 1px solid currentColor; */",
68
+ " /* border-radius: $2; */",
69
+ " }",
70
+ " }",
71
+ "}"
72
+ ]
73
+ }
74
+ }