srcdev-nuxt-components 9.4.4 → 9.4.6

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 (116) hide show
  1. package/.claude/commands/migrate-component.md +106 -0
  2. package/.claude/component-ledger/artifact-url.txt +1 -0
  3. package/.claude/component-ledger/audit.json +1 -0
  4. package/.claude/component-ledger/build.mjs +111 -0
  5. package/.claude/component-ledger/output.html +559 -0
  6. package/.claude/component-ledger/template.html +559 -0
  7. package/.claude/hooks/refresh-component-ledger.sh +32 -0
  8. package/.claude/settings.json +10 -0
  9. package/.claude/skills/components/animated-svg-text.md +78 -0
  10. package/.claude/skills/components/breadcrumb.md +1 -0
  11. package/.claude/skills/components/canvas-switcher.md +61 -0
  12. package/.claude/skills/components/clip-element.md +67 -0
  13. package/.claude/skills/components/clipped-panel.md +64 -0
  14. package/.claude/skills/components/content-docs.md +2 -0
  15. package/.claude/skills/components/deep-expanding-menu-classic.md +94 -0
  16. package/.claude/skills/components/deep-expanding-menu.md +92 -0
  17. package/.claude/skills/components/display-banner.md +60 -0
  18. package/.claude/skills/components/display-tooltip-defined.md +139 -0
  19. package/.claude/skills/components/display-tooltip.md +85 -0
  20. package/.claude/skills/components/marquee-scroller.md +130 -0
  21. package/.claude/skills/components/navigation-items.md +1 -0
  22. package/.claude/skills/components/responsive-header.md +3 -0
  23. package/.claude/skills/components/site-header.md +4 -0
  24. package/.claude/skills/components/site-navigation.md +1 -0
  25. package/.claude/skills/components/tab-navigation.md +1 -0
  26. package/.claude/skills/composable-tooltips-guide.md +10 -0
  27. package/.claude/skills/index.md +8 -1
  28. package/.claude/skills/storybook-add-story.md +30 -0
  29. package/.vscode/srcdev-component-animated-svg-text.code-snippets +15 -0
  30. package/.vscode/srcdev-component-breadcrumb.code-snippets +13 -0
  31. package/.vscode/srcdev-component-canvas-switcher.code-snippets +9 -0
  32. package/.vscode/srcdev-component-clip-element.code-snippets +20 -0
  33. package/.vscode/srcdev-component-clipped-panel.code-snippets +23 -0
  34. package/.vscode/srcdev-component-content-docs.code-snippets +18 -0
  35. package/.vscode/srcdev-component-deep-expanding-menu-classic.code-snippets +9 -0
  36. package/.vscode/srcdev-component-deep-expanding-menu.code-snippets +9 -0
  37. package/.vscode/srcdev-component-display-banner.code-snippets +30 -0
  38. package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +29 -0
  39. package/.vscode/srcdev-component-display-tooltip.code-snippets +38 -0
  40. package/.vscode/srcdev-component-marquee-scroller.code-snippets +93 -0
  41. package/.vscode/srcdev-component-responsive-header.code-snippets +12 -0
  42. package/.vscode/srcdev-component-site-header.code-snippets +17 -0
  43. package/.vscode/srcdev-component-site-navigation.code-snippets +30 -0
  44. package/.vscode/srcdev-component-slider-gallery.code-snippets +38 -0
  45. package/.vscode/srcdev-component-tab-navigation.code-snippets +30 -0
  46. package/app/components/01.atoms/animations/animated-svg-text/AnimatedSvgText.vue +58 -0
  47. package/app/components/01.atoms/animations/animated-svg-text/CONSUMER-STYLING.md +34 -0
  48. package/app/components/01.atoms/animations/animated-svg-text/stories/AnimatedSvgText.stories.ts +69 -0
  49. package/app/components/01.atoms/animations/animated-svg-text/tests/AnimatedSvgText.spec.ts +63 -0
  50. package/app/components/01.atoms/animations/animated-svg-text/tests/__snapshots__/AnimatedSvgText.spec.ts.snap +7 -0
  51. package/app/components/01.atoms/animations/clip-element/CONSUMER-STYLING.md +27 -0
  52. package/app/components/{clip-element → 01.atoms/animations/clip-element}/ClipElement.vue +16 -11
  53. package/app/components/01.atoms/animations/clip-element/stories/ClipElement.stories.ts +133 -0
  54. package/app/components/01.atoms/animations/clip-element/tests/ClipElement.spec.ts +70 -0
  55. package/app/components/01.atoms/animations/clip-element/tests/__snapshots__/ClipElement.spec.ts.snap +7 -0
  56. package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +94 -0
  57. package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +331 -0
  58. package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +151 -0
  59. package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +315 -0
  60. package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +28 -0
  61. package/app/components/01.atoms/canvas-switcher/CONSUMER-STYLING.md +27 -0
  62. package/app/components/01.atoms/canvas-switcher/CanvasSwitcher.vue +111 -0
  63. package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +75 -0
  64. package/app/components/01.atoms/canvas-switcher/tests/CanvasSwitcher.spec.ts +67 -0
  65. package/app/components/01.atoms/clipped-panel/CONSUMER-STYLING.md +39 -0
  66. package/app/components/01.atoms/clipped-panel/ClippedPanel.vue +68 -0
  67. package/app/components/01.atoms/clipped-panel/stories/ClippedPanel.stories.ts +105 -0
  68. package/app/components/01.atoms/clipped-panel/tests/ClippedPanel.spec.ts +67 -0
  69. package/app/components/01.atoms/clipped-panel/tests/__snapshots__/ClippedPanel.spec.ts.snap +3 -0
  70. package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +8 -2
  71. package/app/components/01.atoms/display-banner/CONSUMER-STYLING.md +35 -0
  72. package/app/components/{display-banner → 01.atoms/display-banner}/DisplayBanner.vue +13 -35
  73. package/app/components/01.atoms/display-banner/stories/DisplayBanner.stories.ts +100 -0
  74. package/app/components/01.atoms/display-banner/tests/DisplayBanner.spec.ts +78 -0
  75. package/app/components/01.atoms/display-banner/tests/__snapshots__/DisplayBanner.spec.ts.snap +8 -0
  76. package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +62 -0
  77. package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +188 -0
  78. package/app/components/01.atoms/display-tooltip/stories/DisplayTooltip.stories.ts +94 -0
  79. package/app/components/01.atoms/display-tooltip/tests/DisplayTooltip.spec.ts +75 -0
  80. package/app/components/01.atoms/display-tooltip/tests/__snapshots__/DisplayTooltip.spec.ts.snap +12 -0
  81. package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +4 -1
  82. package/app/components/02.molecules/display-tooltip-defined/CONSUMER-STYLING.md +42 -0
  83. package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +116 -0
  84. package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts +126 -0
  85. package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts +66 -0
  86. package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +17 -0
  87. package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +44 -0
  88. package/app/components/{deep-expanding-menu → 02.molecules/navigation/deep-expanding-menu}/DeepExpandingMenu.vue +53 -64
  89. package/app/components/02.molecules/navigation/deep-expanding-menu/stories/DeepExpandingMenu.stories.ts +79 -0
  90. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts +92 -0
  91. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +17 -0
  92. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +40 -0
  93. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +200 -0
  94. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/stories/DeepExpandingMenuClassic.stories.ts +79 -0
  95. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/DeepExpandingMenuClassic.spec.ts +82 -0
  96. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/__snapshots__/DeepExpandingMenuClassic.spec.ts.snap +18 -0
  97. package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +4 -1
  98. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +4 -1
  99. package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +16 -4
  100. package/app/components/03.organisms/responsive-header/NavigationItems.vue +4 -1
  101. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +16 -3
  102. package/app/components/03.organisms/site-header/SiteHeader.vue +16 -1
  103. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +1 -1
  104. package/app/types/components/display-tooltip-defined.d.ts +10 -0
  105. package/app/types/components/index.ts +2 -0
  106. package/app/types/components/marquee-scroller.d.ts +10 -0
  107. package/package.json +1 -1
  108. package/app/components/animated-svg-text/AnimatedSvgText.vue +0 -89
  109. package/app/components/canvas-switcher/CanvasSwitcher.vue +0 -104
  110. package/app/components/clipped-panels/ClippedPanel.vue +0 -87
  111. package/app/components/deep-expanding-menu/DeepExpandingMenuOld.vue +0 -218
  112. package/app/components/display-details/DisplayDetailsCore.vue +0 -122
  113. package/app/components/display-tooltip/DisplayTooltip.vue +0 -170
  114. package/app/components/display-tooltip/DisplayTooltipDefined.vue +0 -103
  115. package/app/components/magnetic-navigation/MagneticNavigation.vue +0 -162
  116. package/app/components/marquee-scroller/MarqueeScroller.vue +0 -289
@@ -13,6 +13,7 @@ current page).
13
13
  |------|------|---------|----------|
14
14
  | `items` | `BreadcrumbItem[]` | — | **yes** |
15
15
  | `separator` | `string` | `"/"` | no |
16
+ | `ariaLabel` | `string` | `"Breadcrumb"` | no — override for localisation |
16
17
  | `styleClassPassthrough` | `string \| string[]` | `[]` | no |
17
18
 
18
19
  `BreadcrumbItem` is exported from `~/types/components/breadcrumb` (and re-exported from
@@ -0,0 +1,61 @@
1
+ # CanvasSwitcher Component
2
+
3
+ ## Overview
4
+
5
+ `CanvasSwitcher` renders a row of icon buttons (mobile / tablet / laptop / desktop / full width)
6
+ for switching a bound `MediaCanvas` value — typically used to drive a preview/demo wrapper's width
7
+ while working on a responsive component in Storybook. Each button is an `InputButtonCore`
8
+ (`variant="tertiary"`, icon-only), giving it the library's standard button focus ring and hover
9
+ state for free instead of a raw `<button>`.
10
+
11
+ ## Model
12
+
13
+ | Model | Type | Required |
14
+ |---|---|---|
15
+ | `canvasName` (`v-model:canvas-name`) | `MediaCanvas` | no |
16
+
17
+ `MediaCanvas` is exported from `~/types/components/canvas-switcher` (and re-exported from
18
+ `~/types/components`):
19
+
20
+ ```typescript
21
+ type MediaCanvas = "mobileCanvas" | "tabletCanvas" | "laptopCanvas" | "desktopCanvas" | "fullWidthCanvas";
22
+ ```
23
+
24
+ ## Props
25
+
26
+ | Prop | Type | Default | Required |
27
+ |------|------|---------|----------|
28
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | no |
29
+
30
+ ## Usage
31
+
32
+ ```vue
33
+ <script setup lang="ts">
34
+ import type { MediaCanvas } from "~/types/components";
35
+
36
+ const canvasName = ref<MediaCanvas>("desktopCanvas");
37
+ </script>
38
+
39
+ <template>
40
+ <CanvasSwitcher v-model:canvas-name="canvasName" />
41
+ <div :class="canvasName">
42
+ <!-- preview content constrained to the selected canvas width -->
43
+ </div>
44
+ </template>
45
+ ```
46
+
47
+ The selected value doubles as a utility class name: `CanvasSwitcher`'s stylesheet also declares
48
+ `.mobileCanvas`/`.tabletCanvas`/`.laptopCanvas`/`.desktopCanvas`/`.fullWidthCanvas` (max-width
49
+ utilities, not scoped to `.canvas-switcher`), so binding `:class="canvasName"` directly onto a
50
+ preview wrapper constrains it to match — see `LayoutGridA`/`LayoutGridB` stories for the pattern.
51
+ These classes only ship in the bundle once `CanvasSwitcher` (or another importer of them) is
52
+ actually imported somewhere, since Nuxt only bundles a component's SFC styles when it's used.
53
+
54
+ ## Notes
55
+
56
+ - Component is auto-imported in Nuxt — no import needed.
57
+ - No accessible-name prop is needed on the buttons — each one's `button-text` ("Mobile", "Tablet",
58
+ etc.) is rendered screen-reader-only by `InputButtonCore`'s icon-only handling, and the selected
59
+ button is marked via `aria-pressed="true"` rather than a private CSS-only "current" class.
60
+ - See `CONSUMER-STYLING.md` in the component's own folder for the full `--canvas-switcher-*` token
61
+ API. Button-level tokens (surface/hover/focus) belong to `InputButtonCore`, not this component.
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: ClipElement
3
+ description: ClipElement scroll-driven clip-path reveal — props, slot API, tokens, when to use vs ScrollRevealFrame
4
+ type: reference
5
+ ---
6
+
7
+ # ClipElement
8
+
9
+ ## Overview
10
+
11
+ `ClipElement` clips its slot content in from the top edge as it scrolls into the viewport, using a
12
+ `window` scroll listener to drive a `clip-path: inset(...)` value on the content. As the element's
13
+ top position moves through the range `[0, maxClip]`, the clip inset shrinks from `maxClip` to `0`;
14
+ once the element scrolls above the viewport (`top < 0`), the inset grows again past `maxClip`,
15
+ re-clipping the content as it exits.
16
+
17
+ This is a JS/scroll-listener implementation, unlike `ScrollRevealFrame`/`ScrollRevealImage` which
18
+ use CSS Scroll-driven Animations with no listeners. Prefer `ScrollRevealFrame` for a general
19
+ parallax pan effect with no JS overhead — reach for `ClipElement` specifically when you want this
20
+ top-edge clip-in/clip-out behaviour rather than a pan.
21
+
22
+ ## Props
23
+
24
+ | Prop | Type | Default | Description |
25
+ |------|------|---------|-------------|
26
+ | `maxClip` | `number` | `100` | Scroll distance (px) over which the element clips in as it enters the viewport, and clips further out as it scrolls above it. |
27
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root `.clip-element-wrapper`. |
28
+
29
+ ## Slots
30
+
31
+ | Slot | Description |
32
+ |------|-------------|
33
+ | `default` | Content to clip. Rendered inside `.clipped-element`, which receives the computed `clip-path`. |
34
+
35
+ ## Basic usage
36
+
37
+ ```vue
38
+ <ClipElement :max-clip="150">
39
+ <img src="/images/hero.jpg" alt="Hero" style="width: 100%; display: block;" />
40
+ </ClipElement>
41
+ ```
42
+
43
+ ## How it works
44
+
45
+ - A `scroll` listener (added on `onMounted`, client-only, `{ passive: true }`) reads
46
+ `.clipped-element`'s `getBoundingClientRect().top` on every scroll event.
47
+ - `topPosition` between `0` and `maxClip`: clip inset = `maxClip - topPosition` (shrinks toward 0
48
+ as the element scrolls up into place).
49
+ - `topPosition < 0` (element scrolled above the viewport): clip inset = `maxClip + abs(topPosition)`
50
+ (grows past `maxClip`, re-clipping as it exits upward).
51
+ - `topPosition > maxClip` (element still below the visible clip range): clip inset stays `0`.
52
+ - The computed inset is written to a private `--_clip-path` custom property via inline `:style`,
53
+ consumed by `.clipped-element { clip-path: var(--_clip-path); }`.
54
+
55
+ ## CSS custom properties
56
+
57
+ `--_clip-path` is private and JS-driven — there is no public token API for this component. See
58
+ `CONSUMER-STYLING.md` for the class-passthrough override surface.
59
+
60
+ ## Notes
61
+
62
+ - `.clip-element-wrapper` has `overflow: hidden` — content that needs to escape (dropdowns,
63
+ tooltips) must be portalled outside.
64
+ - No `IntersectionObserver`; the scroll listener runs on every scroll event for the page's
65
+ lifetime while the component is mounted. Avoid mounting many instances on one long page.
66
+ - Client-only: the initial clip check and the listener are both gated behind `import.meta.client`,
67
+ so SSR renders the content unclipped until hydration.
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: ClippedPanel
3
+ description: ClippedPanel fixed-size notched/cutout shape panel — props, variants, CSS tokens
4
+ type: reference
5
+ ---
6
+
7
+ # ClippedPanel
8
+
9
+ ## Overview
10
+
11
+ `ClippedPanel` renders its slot content inside a panel clipped to one of three fixed-size
12
+ notched shapes via `clip-path: path(...)`.
13
+
14
+ ## Props
15
+
16
+ | Prop | Type | Default | Description |
17
+ |------|------|---------|-------------|
18
+ | `tag` | `"div" \| "p" \| "span" \| "section" \| "article" \| "aside" \| "header" \| "footer" \| "main" \| "nav" \| "ul" \| "ol"` | `"div"` | Root element tag. |
19
+ | `variant` | `"square" \| "rectangle" \| "circle-cutout"` | `"square"` | Which fixed-size shape to clip to. |
20
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
21
+
22
+ ## Variants
23
+
24
+ | Variant | Fixed size | Shape |
25
+ |---|---|---|
26
+ | `square` | 200×200px | Rounded square with a small pull-tab notch cut into the top edge. |
27
+ | `rectangle` | 300×200px | Same pull-tab notch shape, wider. |
28
+ | `circle-cutout` | 200×200px | Rounded square with a 50px-radius circular notch bitten out of the top-right corner. |
29
+
30
+ Each shape's `clip-path: path(...)` uses literal pixel coordinates matched to its fixed width —
31
+ see CONSUMER-STYLING.md for why these can't be resized via CSS.
32
+
33
+ ## Basic usage
34
+
35
+ ```vue
36
+ <ClippedPanel variant="square">
37
+ <p>Panel content</p>
38
+ </ClippedPanel>
39
+
40
+ <ClippedPanel variant="circle-cutout" tag="section">
41
+ <p>Panel content</p>
42
+ </ClippedPanel>
43
+ ```
44
+
45
+ ## CSS custom properties
46
+
47
+ | Property | Default | Controls |
48
+ |----------|---------|----------|
49
+ | `--clipped-panel-background-colour` | `light-dark(hsl(0, 0%, 96%), hsl(0, 0%, 12%))` | Panel background |
50
+ | `--clipped-panel-outline-colour` | `light-dark(hsl(0, 29%, 3%), hsl(0, 0%, 92%))` | Outline colour and text/icon colour |
51
+ | `--clipped-panel-outline-width` | `1px` | Outline width |
52
+
53
+ ## Notes
54
+
55
+ - `aspect-ratio: 1` is set on the shared `.clipped-panel` base class, so height tracks whatever
56
+ `width` the active variant sets.
57
+ - Shape geometry (corner radii, notch size, overall dimensions) is fixed per variant — CSS custom
58
+ properties can't be interpolated into a `path()` string. A new size/shape needs a new variant
59
+ with its own `path()`, not a token override.
60
+ - 2026-09-07 migration: the previous `circle-cutout` variant shipped with an invalid, empty
61
+ `clip-path: path("M Z")` that rendered no clip at all. It now has a real shape (described
62
+ above). The unused `--_max-x-position`/`--_curve-radius` custom properties on `rectangle` were
63
+ also removed — they were declared but never actually referenced by the hardcoded path string,
64
+ so they did nothing.
@@ -18,6 +18,8 @@
18
18
  | `docsNavLabel` | `string` | `"Navigation"` | Heading text for the `docsNav` panel's `#summary`. |
19
19
  | `docsPageNavLabel` | `string` | `"On this page"` | Heading text for the `docsPageNav` panel's `#summary`. |
20
20
  | `panelVariant` | `"modern" \| "classic"` | `"classic"` | Which panel component renders `docsNav`/`docsPageNav`: `ExpandingPanel` (`"modern"`) or `ExpandingPanelClassic` (`"classic"`, default). `"modern"` is known not to work correctly on WebKit — the mobile nav (which relies on `contentIsOnTop`) becomes unreachable there. See CLAUDE.md pitfall #19. |
21
+ | `docsNavAriaLabel` | `string` | `"Docs navigation"` | aria-label on the `docsNav` `<nav>` landmark — override for localisation. |
22
+ | `docsPageNavAriaLabel` | `string` | `"On this page"` | aria-label on the `docsPageNav` `<nav>` landmark — override for localisation. |
21
23
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `.content-docs` element. |
22
24
 
23
25
  `DocsNavItem` (from `~/types/components`):
@@ -0,0 +1,94 @@
1
+ ---
2
+ name: DeepExpandingMenuClassic
3
+ description: DeepExpandingMenuClassic <details>-based top-level nav with click-outside close, CSS token API
4
+ type: reference
5
+ ---
6
+
7
+ # DeepExpandingMenuClassic
8
+
9
+ ## Overview
10
+
11
+ `<details>`/`<summary>`-based fallback for [`DeepExpandingMenu`](deep-expanding-menu.md) — same
12
+ props, same `ResponsiveHeaderNavItem` data shape, same visual result, but built without CSS
13
+ anchor-positioning or the Popover API. Each nav item with `childLinks` renders as a native
14
+ `<details>` element; opening one and clicking outside it closes it again via `@vueuse/core`'s
15
+ `onClickOutside`.
16
+
17
+ Prefer `DeepExpandingMenu` unless the modern implementation's browser support isn't acceptable for
18
+ your target audience — see its own docs for the caveat.
19
+
20
+ ## Props
21
+
22
+ | Prop | Type | Default | Description |
23
+ |------|------|---------|-------------|
24
+ | `tag` | `"div" \| "section" \| "nav" \| "ul" \| "ol"` | `"nav"` | Root element tag. |
25
+ | `navLinks` | `ResponsiveHeaderNavItem[]` | `[]` | Top-level nav items. An item with `path` renders as a direct link; an item with `childLinks` renders as a `<details>` group. |
26
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
27
+
28
+ `ResponsiveHeaderNavItem` is exported from `~/types/components` (shared with `ResponsiveHeader`).
29
+
30
+ ## Basic usage
31
+
32
+ ```vue
33
+ <script setup lang="ts">
34
+ import type { ResponsiveHeaderNavItem } from "~/types/components";
35
+
36
+ const navLinks: ResponsiveHeaderNavItem[] = [
37
+ { name: "Home", path: "/" },
38
+ {
39
+ name: "Services",
40
+ childLinksTitle: "Our services",
41
+ childLinks: [
42
+ { name: "Haircuts", path: "/services/haircuts" },
43
+ { name: "Colouring", path: "/services/colouring" },
44
+ ],
45
+ },
46
+ ];
47
+ </script>
48
+
49
+ <template>
50
+ <DeepExpandingMenuClassic :nav-links="navLinks" />
51
+ </template>
52
+ ```
53
+
54
+ ## CSS custom properties
55
+
56
+ | Property | Default | Controls |
57
+ |----------|---------|----------|
58
+ | `--deep-expanding-menu-classic-gap` | `1.2rem` | Gap on the root grid |
59
+ | `--deep-expanding-menu-classic-item-gap` | `2.4rem` | Gap between top-level items |
60
+ | `--deep-expanding-menu-classic-link-border-width` | `0.2rem` | Bottom border width on links/toggles |
61
+ | `--deep-expanding-menu-classic-link-padding-block` | `0.8rem` | Vertical padding on links/toggles |
62
+ | `--deep-expanding-menu-classic-link-border-colour-hover` | `light-dark(var(--blue-10), var(--slate-00))` | Bottom border colour on hover/focus |
63
+ | `--deep-expanding-menu-classic-icon-size` | `1.2rem` | Caret icon size |
64
+ | `--deep-expanding-menu-classic-panel-offset-top` | `4rem` | Panel offset from the toggle |
65
+ | `--deep-expanding-menu-classic-panel-width` | `20rem` | Panel width below `768px` |
66
+ | `--deep-expanding-menu-classic-panel-width-tablet` | `40rem` | Panel width from `768px` |
67
+ | `--deep-expanding-menu-classic-panel-width-desktop` | `60rem` | Panel width from `1024px` |
68
+ | `--deep-expanding-menu-classic-panel-background-colour` | `white` | Panel background |
69
+ | `--deep-expanding-menu-classic-panel-border-width` | `0.1rem` | Panel border width |
70
+ | `--deep-expanding-menu-classic-panel-border-colour` | `black` | Panel border colour |
71
+ | `--deep-expanding-menu-classic-panel-border-radius` | `1.2rem` | Panel corner radius |
72
+ | `--deep-expanding-menu-classic-panel-shadow` | `0 0 1rem rgba(0, 0, 0, 0.1)` | Panel box-shadow |
73
+ | `--deep-expanding-menu-classic-panel-padding` | `1.2rem` | Panel padding |
74
+ | `--deep-expanding-menu-classic-panel-heading-colour` | `var(--slate-10)` | `childLinksTitle` heading colour |
75
+ | `--deep-expanding-menu-classic-panel-list-gap` | `1.2rem` | Gap between child link grid items |
76
+ | `--deep-expanding-menu-classic-group-link-colour` | `var(--slate-10)` | Child link text colour |
77
+ | `--deep-expanding-menu-classic-group-link-border-colour-hover` | `var(--slate-10)` | Child link bottom border colour on hover/focus |
78
+
79
+ See `CONSUMER-STYLING.md` for the full token API.
80
+
81
+ ## Notes
82
+
83
+ - Auto-imported in Nuxt — no manual import needed.
84
+ - Multiple `<details>` groups sharing `name="navigation-group"` means opening one does **not**
85
+ auto-close siblings (unlike a native accordion) — each group closes independently via
86
+ click-outside.
87
+ - 2026-09-07 migration: moved from an unplaced top-level folder (was `DeepExpandingMenuOld.vue`)
88
+ into `02.molecules/navigation/deep-expanding-menu-classic/`; converted options-style
89
+ `defineProps` to `interface Props` + `withDefaults`; switched its locally-declared
90
+ `ResponsiveHeaderNavItem` interface to the shared exported type from `~/types/components`
91
+ (was a duplicate, drift-prone copy); removed a dead, unused `:id="popovertarget-nav-1-..."`
92
+ attribute left over from copying the modern implementation (this component doesn't use the
93
+ Popover API); promoted 19 previously-hardcoded values to public CSS tokens.
94
+ - File: `app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue`
@@ -0,0 +1,92 @@
1
+ ---
2
+ name: DeepExpandingMenu
3
+ description: DeepExpandingMenu top-level nav with anchor-positioned popover child-link panels, browser support caveat, CSS token API
4
+ type: reference
5
+ ---
6
+
7
+ # DeepExpandingMenu
8
+
9
+ ## Overview
10
+
11
+ `DeepExpandingMenu` renders a top-level navigation row where each item is either a direct link
12
+ (`link.path` set) or a toggle button that opens a popover panel of child links. It's built on CSS
13
+ anchor-positioning (`anchor-name`/`position-anchor`/`anchor()`) and the native Popover API
14
+ (`popover`/`popovertarget`).
15
+
16
+ **Browser support caveat:** CSS anchor-positioning and the Popover API are both comparatively
17
+ recent — check current browser support before shipping this on a page that must work everywhere,
18
+ same caution as `ExpandingPanel`'s `::details-content` gap (CLAUDE.md pitfall #19). If broad
19
+ support is a hard requirement, use [`DeepExpandingMenuClassic`](deep-expanding-menu-classic.md)
20
+ instead — same API and visual result, built on `<details>`/`<summary>` +
21
+ `onClickOutside` instead.
22
+
23
+ ## Props
24
+
25
+ | Prop | Type | Default | Description |
26
+ |------|------|---------|-------------|
27
+ | `tag` | `"div" \| "section" \| "nav" \| "ul" \| "ol"` | `"nav"` | Root element tag. |
28
+ | `navLinks` | `ResponsiveHeaderNavItem[]` | `[]` | Top-level nav items. An item with `path` renders as a direct link; an item with `childLinks` renders as a toggle + popover panel. |
29
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
30
+
31
+ `ResponsiveHeaderNavItem` is exported from `~/types/components` (shared with `ResponsiveHeader`).
32
+
33
+ ## Basic usage
34
+
35
+ ```vue
36
+ <script setup lang="ts">
37
+ import type { ResponsiveHeaderNavItem } from "~/types/components";
38
+
39
+ const navLinks: ResponsiveHeaderNavItem[] = [
40
+ { name: "Home", path: "/" },
41
+ {
42
+ name: "Services",
43
+ childLinksTitle: "Our services",
44
+ childLinks: [
45
+ { name: "Haircuts", path: "/services/haircuts" },
46
+ { name: "Colouring", path: "/services/colouring" },
47
+ ],
48
+ },
49
+ ];
50
+ </script>
51
+
52
+ <template>
53
+ <DeepExpandingMenu :nav-links="navLinks" />
54
+ </template>
55
+ ```
56
+
57
+ ## CSS custom properties
58
+
59
+ | Property | Default | Controls |
60
+ |----------|---------|----------|
61
+ | `--deep-expanding-menu-gap` | `2.4rem` | Gap between top-level items |
62
+ | `--deep-expanding-menu-link-border-width` | `0.2rem` | Bottom border width on links/toggles |
63
+ | `--deep-expanding-menu-link-padding-block` | `0.8rem` | Vertical padding on links/toggles |
64
+ | `--deep-expanding-menu-link-border-colour-hover` | `light-dark(var(--blue-10), var(--slate-00))` | Bottom border colour on hover/focus |
65
+ | `--deep-expanding-menu-icon-size` | `1.2rem` | Caret icon size |
66
+ | `--deep-expanding-menu-panel-width` | `min(100%, 50vw)` | Popover panel width |
67
+ | `--deep-expanding-menu-panel-background-colour` | `white` | Popover panel background |
68
+ | `--deep-expanding-menu-panel-border-width` | `0.1rem` | Popover panel border width |
69
+ | `--deep-expanding-menu-panel-border-colour` | `black` | Popover panel border colour |
70
+ | `--deep-expanding-menu-panel-border-radius` | `1.2rem` | Popover panel corner radius |
71
+ | `--deep-expanding-menu-panel-shadow` | `0 0 1rem rgba(0, 0, 0, 0.1)` | Popover panel box-shadow |
72
+ | `--deep-expanding-menu-panel-padding` | `1.2rem` | Popover panel padding |
73
+ | `--deep-expanding-menu-panel-heading-colour` | `var(--slate-10)` | `childLinksTitle` heading colour |
74
+ | `--deep-expanding-menu-panel-list-gap` | `1.2rem` | Gap between child link grid items |
75
+ | `--deep-expanding-menu-group-link-colour` | `var(--slate-10)` | Child link text colour |
76
+ | `--deep-expanding-menu-group-link-border-colour-hover` | `var(--slate-10)` | Child link bottom border colour on hover/focus |
77
+
78
+ See `CONSUMER-STYLING.md` for the full token API and what's not tokenised.
79
+
80
+ ## Notes
81
+
82
+ - Auto-imported in Nuxt — no manual import needed.
83
+ - Anchor names and popover target ids are generated per-instance via `useId()`, so multiple
84
+ instances on one page don't collide.
85
+ - 2026-09-07 migration: moved from an unplaced top-level folder into
86
+ `02.molecules/navigation/deep-expanding-menu/`; converted options-style `defineProps` to
87
+ `interface Props` + `withDefaults`; the previous hardcoded `nav-1` anchor/popover id prefix
88
+ (a multi-instance collision bug) was replaced with a `useId()`-scoped prefix; removed a dead,
89
+ never-consumed `ref="detailsRef"` template ref; promoted 16 previously-hardcoded values (gap,
90
+ border widths/colours, panel background/border/radius/shadow/padding, heading and link colours)
91
+ to public CSS tokens.
92
+ - File: `app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue`
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: DisplayBanner
3
+ description: DisplayBanner canvas/content stacked overlay banner — props, slots, CSS tokens
4
+ type: reference
5
+ ---
6
+
7
+ # DisplayBanner
8
+
9
+ ## Overview
10
+
11
+ `DisplayBanner` stacks a `canvas` slot (background media) and a `content` slot (overlaid
12
+ text/CTAs) into a single grid area, so content renders on top of the canvas. Either slot may be
13
+ omitted; an unused slot contributes no empty wrapper `<div>` to the DOM.
14
+
15
+ ## Props
16
+
17
+ | Prop | Type | Default | Description |
18
+ |------|------|---------|-------------|
19
+ | `tag` | `"div" \| "p" \| "span" \| "section" \| "article" \| "aside" \| "header" \| "footer" \| "main" \| "nav" \| "ul" \| "ol"` | `"div"` | Root element tag. |
20
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
21
+
22
+ ## Slots
23
+
24
+ | Slot | Description |
25
+ |------|-------------|
26
+ | `canvas` | Background media (image, video, or any visual). Rendered first, so it sits behind `content`. |
27
+ | `content` | Overlaid content (text, CTAs). Rendered after `canvas`, so it sits on top. |
28
+
29
+ ## Basic usage
30
+
31
+ ```vue
32
+ <DisplayBanner>
33
+ <template #canvas>
34
+ <NuxtImg src="/hero.jpg" alt="" style="width: 100%; height: 100%; object-fit: cover;" />
35
+ </template>
36
+ <template #content>
37
+ <h2>Banner heading</h2>
38
+ </template>
39
+ </DisplayBanner>
40
+ ```
41
+
42
+ ## CSS custom properties
43
+
44
+ | Property | Default | Controls |
45
+ |----------|---------|----------|
46
+ | `--display-banner-min-height` | `auto` | Minimum height of the banner |
47
+
48
+ See `CONSUMER-STYLING.md` for the full token API.
49
+
50
+ ## Notes
51
+
52
+ - `container-type: inline-size` is set on the root, so slot content can use container queries.
53
+ - No sizing/aspect-ratio is imposed on the `canvas` slot's content itself (e.g. an image needs its
54
+ own `width`/`height`/`object-fit`) — `DisplayBanner` only positions the two slots, it doesn't
55
+ style what's inside them.
56
+ - 2026-09-07 migration: removed a dead `.canvas .image { object-fit: cover; ... }` CSS rule that
57
+ assumed consumer-slotted content would use a generic `.image` class — nothing in the template
58
+ ever rendered that class, and styling a generic class name inside a slot risks colliding with a
59
+ consumer's own `.image` class (see CLAUDE.md pitfall #16). Style slotted media directly on the
60
+ element you put in the slot instead.
@@ -0,0 +1,139 @@
1
+ ---
2
+ name: DisplayTooltipDefined
3
+ description: DisplayTooltipDefined structured title/body/action tooltip content with close button, composes DisplayTooltip
4
+ type: reference
5
+ ---
6
+
7
+ # DisplayTooltipDefined
8
+
9
+ ## Overview
10
+
11
+ `DisplayTooltipDefined` composes [`DisplayTooltip`](display-tooltip.md), supplying a structured
12
+ title/body/action content shape (each rendered with a consumer-chosen tag) plus a built-in close
13
+ button wired to the popover's `popovertarget`. Use this when you want the standard tooltip content
14
+ layout without hand-building the `tooltipContent` slot markup each time; use plain `DisplayTooltip`
15
+ directly when you need full control over the popover's content.
16
+
17
+ ## Props
18
+
19
+ | Prop | Type | Default | Description |
20
+ |------|------|---------|-------------|
21
+ | `tooltipId` | `string` | `""` | Base id for the popover. Prefixed with `nuxt-tooltip-` when provided; auto-generated via `useId()` (also prefixed) when omitted. |
22
+ | `contentText` | `TooltipContentText` | `{}` | Title/body/action content. Each field is optional — omitted fields render nothing. |
23
+ | `triggerAriaLabel` | `string` | `"Toggle the popover"` | Forwarded to the underlying `DisplayTooltip`'s `triggerAriaLabel` — override for localisation. |
24
+ | `closeButtonText` | `string` | `"Close"` | Visible text on the close button — override for localisation. |
25
+ | `closeButtonAriaLabel` | `string` | `"Close tool tip"` | aria-label on the close button — override for localisation. |
26
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the underlying `DisplayTooltip` root element. |
27
+
28
+ `TooltipContentText` is exported from `~/types/components`:
29
+
30
+ ```ts
31
+ interface TooltipContentTextItem {
32
+ tag: string; // e.g. "h4", "p", "span"
33
+ text: string;
34
+ }
35
+
36
+ interface TooltipContentText {
37
+ tooltipTitle?: TooltipContentTextItem;
38
+ tooltipContent?: TooltipContentTextItem;
39
+ tooltipAction?: TooltipContentTextItem;
40
+ }
41
+ ```
42
+
43
+ ## Slots
44
+
45
+ | Slot | Description |
46
+ |------|-------------|
47
+ | `triggerContent` | Passed through to the underlying `DisplayTooltip`'s `triggerContent` slot. |
48
+
49
+ ## Basic usage
50
+
51
+ ```vue
52
+ <script setup lang="ts">
53
+ import type { TooltipContentText } from "~/types/components";
54
+
55
+ const contentText: TooltipContentText = {
56
+ tooltipTitle: { tag: "h4", text: "Free delivery" },
57
+ tooltipContent: { tag: "p", text: "Orders over £50 qualify for free standard delivery." },
58
+ tooltipAction: { tag: "span", text: "See delivery policy" },
59
+ };
60
+ </script>
61
+
62
+ <template>
63
+ <DisplayTooltipDefined :content-text="contentText" />
64
+ </template>
65
+ ```
66
+
67
+ ## CSS custom properties
68
+
69
+ | Property | Default | Controls |
70
+ |----------|---------|----------|
71
+ | `--display-tooltip-defined-content-gap` | `1.2rem` | Gap between title/body/action/close-button |
72
+ | `--display-tooltip-defined-title-colour` | `inherit` | `tooltipTitle` text colour |
73
+ | `--display-tooltip-defined-title-margin-block` | `0 0` | `tooltipTitle` block margin |
74
+ | `--display-tooltip-defined-title-font-size` | `1.6rem` | `tooltipTitle` font size |
75
+ | `--display-tooltip-defined-title-font-weight` | `700` | `tooltipTitle` font weight |
76
+ | `--display-tooltip-defined-title-line-height` | `120%` | `tooltipTitle` line height |
77
+ | `--display-tooltip-defined-title-letter-spacing` | `normal` | `tooltipTitle` letter spacing |
78
+ | `--display-tooltip-defined-body-colour` | `inherit` | `tooltipContent` text colour |
79
+ | `--display-tooltip-defined-body-margin-block` | `0 0` | `tooltipContent` block margin |
80
+ | `--display-tooltip-defined-body-font-size` | `1.3rem` | `tooltipContent` font size |
81
+ | `--display-tooltip-defined-body-font-weight` | `inherit` | `tooltipContent` font weight |
82
+ | `--display-tooltip-defined-body-line-height` | `140%` | `tooltipContent` line height |
83
+ | `--display-tooltip-defined-body-letter-spacing` | `normal` | `tooltipContent` letter spacing |
84
+ | `--display-tooltip-defined-action-colour` | `inherit` | `tooltipAction` text colour |
85
+ | `--display-tooltip-defined-action-margin-block` | `0 0` | `tooltipAction` block margin |
86
+ | `--display-tooltip-defined-action-font-size` | `1.4rem` | `tooltipAction` font size |
87
+ | `--display-tooltip-defined-action-font-weight` | `700` | `tooltipAction` font weight |
88
+ | `--display-tooltip-defined-action-line-height` | `130%` | `tooltipAction` line height |
89
+ | `--display-tooltip-defined-action-letter-spacing` | `normal` | `tooltipAction` letter spacing |
90
+ | `--display-tooltip-defined-close-button-margin-block` | `1.2rem 0` | Space above the close button |
91
+
92
+ Also renders every `--display-tooltip-*` token from the underlying `DisplayTooltip` — see its
93
+ CONSUMER-STYLING.md for the full list.
94
+
95
+ ## Pairing with useTooltipsGuide
96
+
97
+ For a sequential, auto-advancing "guide" walkthrough (the pattern used in some guide intros — show
98
+ step 1, wait for dismissal, show step 2, ...), pair this component with
99
+ [`useTooltipsGuide`](../composable-tooltips-guide.md): put a ref on the container wrapping your
100
+ `DisplayTooltipDefined`/`DisplayTooltip` instances and pass it to the composable. The composable
101
+ only needs native `[popover]`/`popovertarget` markup, which both components already emit, so no
102
+ extra wiring is required beyond the container ref. See the `GuidedTour` story in
103
+ `DisplayTooltipDefined.stories.ts` for a full working example (it recreates the old
104
+ `app/pages/ui/tooltips.vue` demo page, removed 2026-08-23 when Storybook became the only demo
105
+ surface).
106
+
107
+ ## Notes
108
+
109
+ - Auto-imported in Nuxt — no manual import needed.
110
+ - 2026-09-07 migration: moved from an unplaced top-level folder into
111
+ `02.molecules/display-tooltip-defined/` (molecule tier, since it composes the `DisplayTooltip`
112
+ atom); converted options-style `defineProps` to `interface Props` + `withDefaults`; moved the
113
+ inline `TooltipContentText` interface out to `~/types/components/display-tooltip-defined.d.ts`
114
+ so consumers can import it (inline `.vue` types aren't importable); fixed a dead CSS selector —
115
+ the stylesheet targeted `.popover .popover-content .popover-content-defined`, but the actual
116
+ rendered classes are `.display-tooltip-popover`/`.display-tooltip-popover-content`, so the rule
117
+ never matched anything; replaced two custom properties referencing tokens never declared
118
+ anywhere in this repo (`--nuxt-text-white-header`, `--nuxt-text-white-body`) with real public
119
+ tokens defaulting to `inherit` (same class of bug as `AnimatedSvgText`, CLAUDE.md pitfall #21) —
120
+ the title/body/action text had been rendering only by accident, via CSS falling back to the
121
+ inherited colour when a `var()` reference is unresolvable, not by design.
122
+ - 2026-09-07 (same day, follow-up): `.popover-content-defined` gained `display: flex;
123
+ flex-direction: column;` and a `gap` (new `--display-tooltip-defined-content-gap` token), and
124
+ its close button is now right-aligned (`align-self: flex-end`) rather than sitting inline after
125
+ the action text — part of the same visual-polish pass as `DisplayTooltip` itself (see its skill
126
+ doc). Note `align-self` only works because `.popover-content-defined` (the button's actual DOM
127
+ parent) is the flex container — the shared `.display-tooltip-popover-content` wrapper one level
128
+ up is a different, outer flex context and can't align this button directly.
129
+ - 2026-09-07 (user follow-up): fixed a real spacing bug, not a preference tweak — `contentText.*.tag`
130
+ lets the consumer pick any tag (`h3`, `h4`, `p`, ...), and a heading tag carries its own
131
+ non-zero UA default `margin-block` (e.g. `1em`/`1em`), which was stacking on top of
132
+ `.popover-content-defined`'s flex `gap` and visually doubling the space between title/body/action.
133
+ `margin-block: 0 0` now resets that UA default on all three regardless of which tag is chosen, so
134
+ `gap` is the only spacing mechanism — see the comment in the component. Also gave each of
135
+ `tooltipTitle`/`tooltipContent`/`tooltipAction` a full typographic token set — `font-size`,
136
+ `font-weight`, `line-height`, `letter-spacing`, and `margin-block` (all overridable, in case a
137
+ consumer genuinely wants extra margin on top of `gap`) — rather than only `colour`. The close
138
+ button gained its own `margin-block` token to separate it from the action text above it.
139
+ - File: `app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue`