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
@@ -0,0 +1,66 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
3
+ import DisplayTooltipDefined from "../DisplayTooltipDefined.vue";
4
+ import type { TooltipContentText } from "~/types/components";
5
+
6
+ const contentText: TooltipContentText = {
7
+ tooltipTitle: { tag: "h4", text: "Title" },
8
+ tooltipContent: { tag: "p", text: "Body copy" },
9
+ tooltipAction: { tag: "span", text: "Learn more" },
10
+ };
11
+
12
+ describe("DisplayTooltipDefined", () => {
13
+ it("mounts without error", async () => {
14
+ const wrapper = await mountSuspended(DisplayTooltipDefined);
15
+ expect(wrapper.vm).toBeTruthy();
16
+ });
17
+
18
+ it("renders correct HTML structure", async () => {
19
+ const wrapper = await mountSuspended(DisplayTooltipDefined, { props: { contentText, tooltipId: "fixed" } });
20
+ expect(wrapper.html()).toMatchSnapshot();
21
+ });
22
+
23
+ it("renders the title, body, and action from contentText", async () => {
24
+ const wrapper = await mountSuspended(DisplayTooltipDefined, { props: { contentText } });
25
+ expect(wrapper.find(".tooltip-title").text()).toBe("Title");
26
+ expect(wrapper.find(".tooltip-body").text()).toBe("Body copy");
27
+ expect(wrapper.find(".tooltip-action").text()).toBe("Learn more");
28
+ });
29
+
30
+ it("renders each contentText field with its given tag", async () => {
31
+ const wrapper = await mountSuspended(DisplayTooltipDefined, { props: { contentText } });
32
+ expect(wrapper.find(".tooltip-title").element.tagName.toLowerCase()).toBe("h4");
33
+ expect(wrapper.find(".tooltip-body").element.tagName.toLowerCase()).toBe("p");
34
+ expect(wrapper.find(".tooltip-action").element.tagName.toLowerCase()).toBe("span");
35
+ });
36
+
37
+ it("omits a field when not present in contentText", async () => {
38
+ const wrapper = await mountSuspended(DisplayTooltipDefined, {
39
+ props: { contentText: { tooltipTitle: { tag: "h4", text: "Only a title" } } },
40
+ });
41
+ expect(wrapper.find(".tooltip-title").exists()).toBe(true);
42
+ expect(wrapper.find(".tooltip-body").exists()).toBe(false);
43
+ expect(wrapper.find(".tooltip-action").exists()).toBe(false);
44
+ });
45
+
46
+ it("renders a close button wired to the tooltip id", async () => {
47
+ const wrapper = await mountSuspended(DisplayTooltipDefined, { props: { tooltipId: "my-tip" } });
48
+ const closeButton = wrapper.find(".display-tooltip-close-button");
49
+ expect(closeButton.attributes("popovertarget")).toBe("nuxt-tooltip-my-tip");
50
+ expect(closeButton.attributes("popovertargetaction")).toBe("hide");
51
+ });
52
+
53
+ it("renders the triggerContent slot", async () => {
54
+ const wrapper = await mountSuspended(DisplayTooltipDefined, {
55
+ slots: { triggerContent: "<span class='slot-trigger'>?</span>" },
56
+ });
57
+ expect(wrapper.find(".slot-trigger").text()).toBe("?");
58
+ });
59
+
60
+ it("applies styleClassPassthrough classes to the underlying DisplayTooltip", async () => {
61
+ const wrapper = await mountSuspended(DisplayTooltipDefined, {
62
+ props: { styleClassPassthrough: ["custom-class"] },
63
+ });
64
+ expect(wrapper.classes()).toContain("custom-class");
65
+ });
66
+ });
@@ -0,0 +1,17 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`DisplayTooltipDefined > renders correct HTML structure 1`] = `
4
+ "<div class="display-tooltip-core" style="--_anchor-name: --tooltip-anchor-v-0-0-0;">
5
+ <div class="display-tooltip-trigger-wrapper body-md">
6
+ <!--v-if--><button popovertarget="nuxt-tooltip-fixed" popovertargetaction="toggle" class="display-tooltip-trigger-button" aria-label="Toggle the popover"><span class="iconify i-fa7-solid:circle-question display-tooltip-trigger-icon" aria-hidden="true"></span></button>
7
+ </div>
8
+ <div id="nuxt-tooltip-fixed" popover="" class="display-tooltip-popover">
9
+ <div class="display-tooltip-popover-content">
10
+ <div class="popover-content-defined">
11
+ <h4 class="tooltip-title subtitle-sm">Title</h4>
12
+ <p class="tooltip-body body-sm">Body copy</p><span class="tooltip-action input-value">Learn more</span><button popovertarget="nuxt-tooltip-fixed" popovertargetaction="hide" class="display-tooltip-close-button" aria-label="Close tool tip">Close</button>
13
+ </div>
14
+ </div>
15
+ </div>
16
+ </div>"
17
+ `;
@@ -0,0 +1,44 @@
1
+ # DeepExpandingMenu — Consumer Styling Guide
2
+
3
+ ## Public token API
4
+
5
+ | Token | Default | Controls |
6
+ |---|---|---|
7
+ | `--deep-expanding-menu-gap` | `2.4rem` | Gap between top-level items |
8
+ | `--deep-expanding-menu-link-border-width` | `0.2rem` | Bottom border width on links and group toggles |
9
+ | `--deep-expanding-menu-link-padding-block` | `0.8rem` | Vertical padding on links and group toggles |
10
+ | `--deep-expanding-menu-link-border-colour-hover` | `light-dark(var(--blue-10), var(--slate-00))` | Bottom border colour on hover/focus |
11
+ | `--deep-expanding-menu-icon-size` | `1.2rem` | Caret icon size |
12
+ | `--deep-expanding-menu-panel-width` | `min(100%, 50vw)` | Popover panel width |
13
+ | `--deep-expanding-menu-panel-background-colour` | `white` | Popover panel background |
14
+ | `--deep-expanding-menu-panel-border-width` | `0.1rem` | Popover panel border width |
15
+ | `--deep-expanding-menu-panel-border-colour` | `black` | Popover panel border colour |
16
+ | `--deep-expanding-menu-panel-border-radius` | `1.2rem` | Popover panel corner radius |
17
+ | `--deep-expanding-menu-panel-shadow` | `0 0 1rem rgba(0, 0, 0, 0.1)` | Popover panel box-shadow |
18
+ | `--deep-expanding-menu-panel-padding` | `1.2rem` | Popover panel padding |
19
+ | `--deep-expanding-menu-panel-heading-colour` | `var(--slate-10)` | `childLinksTitle` heading colour inside the panel |
20
+ | `--deep-expanding-menu-panel-list-gap` | `1.2rem` | Gap between child link grid items |
21
+ | `--deep-expanding-menu-group-link-colour` | `var(--slate-10)` | Child link text colour |
22
+ | `--deep-expanding-menu-group-link-border-colour-hover` | `var(--slate-10)` | Child link bottom border colour on hover/focus |
23
+
24
+ ```css
25
+ .my-page {
26
+ --deep-expanding-menu-panel-background-colour: #1a1a1a;
27
+ --deep-expanding-menu-panel-border-colour: transparent;
28
+ --deep-expanding-menu-group-link-colour: white;
29
+ }
30
+ ```
31
+
32
+ Or scope to a single instance via `styleClassPassthrough`:
33
+
34
+ ```vue
35
+ <DeepExpandingMenu style-class-passthrough="main-nav">...</DeepExpandingMenu>
36
+ ```
37
+
38
+ ## Not tokenised
39
+
40
+ - The `@position-try` fallback offsets (`--anchor-left`/`--anchor-right`) are fixed at `1rem` —
41
+ these are CSS anchor-positioning fallback rules, not runtime custom properties, so they can't be
42
+ overridden per-instance without a new `@position-try` block.
43
+ - Anchor names and popover target ids are generated per-instance via `useId()` and are not
44
+ consumer-configurable — they only need to be unique, not meaningful.
@@ -4,13 +4,13 @@
4
4
  <template v-for="(link, key) in navLinks" :key="key">
5
5
  <NuxtLink v-if="link.path" :to="link.path" class="navigation-link">{{ link.name }}</NuxtLink>
6
6
 
7
- <div v-else ref="detailsRef" class="navigation-group" :style="`--_anchor-name: --anchor-nav-1-${key};`">
8
- <button :popovertarget="`popovertarget-nav-1-${key}`" class="navigation-group-toggle">
7
+ <div v-else class="navigation-group" :style="`--_anchor-name: --anchor-${uid}-${key};`">
8
+ <button :popovertarget="`popovertarget-${uid}-${key}`" class="navigation-group-toggle">
9
9
  <span>{{ link.name }}</span>
10
10
  <Icon name="bi:caret-down-fill" class="icon" />
11
11
  </button>
12
12
 
13
- <div :id="`popovertarget-nav-1-${key}`" class="navigation-group-panel" popover role="menu">
13
+ <div :id="`popovertarget-${uid}-${key}`" class="navigation-group-panel" popover role="menu">
14
14
  <h4 class="page-heading-4 mb-6">{{ link.childLinksTitle }}</h4>
15
15
  <ul class="navigation-group-list">
16
16
  <li v-for="childLink in link.childLinks" :key="childLink.name" class="navigation-group-item">
@@ -25,27 +25,24 @@
25
25
  </template>
26
26
 
27
27
  <script setup lang="ts">
28
- import type { ResponsiveHeaderNavItem } from "../../types/components";
29
- const props = defineProps({
30
- tag: {
31
- type: String,
32
- default: "nav",
33
- validator(value: string) {
34
- return ["div", "section", "nav", "ul", "ol"].includes(value);
35
- },
36
- },
37
- navLinks: {
38
- type: Array as PropType<ResponsiveHeaderNavItem[]>,
39
- default: () => [],
40
- },
41
- styleClassPassthrough: {
42
- type: [String, Array] as PropType<string | string[]>,
43
- default: () => [],
44
- },
28
+ import type { ResponsiveHeaderNavItem } from "~/types/components";
29
+
30
+ interface Props {
31
+ tag?: "div" | "section" | "nav" | "ul" | "ol";
32
+ navLinks?: ResponsiveHeaderNavItem[];
33
+ styleClassPassthrough?: string | string[];
34
+ }
35
+
36
+ const props = withDefaults(defineProps<Props>(), {
37
+ tag: "nav",
38
+ navLinks: () => [],
39
+ styleClassPassthrough: () => [],
45
40
  });
46
41
 
47
42
  const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
48
- // const detailsRef = useTemplateRef('detailsRef');
43
+
44
+ // Scopes anchor-name/popovertarget ids so multiple instances on one page don't collide.
45
+ const uid = useId();
49
46
 
50
47
  watch(
51
48
  () => props.styleClassPassthrough,
@@ -60,19 +57,17 @@ watch(
60
57
  @layer deep-expanding-menu-setup {
61
58
  @position-try --anchor-left {
62
59
  inset: auto;
63
- top: calc(anchor(bottom) + 10px);
64
- left: calc(anchor(left) + 10px);
60
+ top: calc(anchor(bottom) + 1rem);
61
+ left: calc(anchor(left) + 1rem);
65
62
  }
66
63
 
67
64
  @position-try-fallbacks --anchor-right {
68
65
  inset: auto;
69
- top: calc(anchor(bottom) + 10px);
70
- right: calc(anchor(right) + 10px);
66
+ top: calc(anchor(bottom) + 1rem);
67
+ right: calc(anchor(right) + 1rem);
71
68
  }
72
69
 
73
70
  .deep-expanding-menu {
74
- --_gap-between-top-level-items: 24px;
75
-
76
71
  container-type: inline-size;
77
72
  display: grid;
78
73
  grid-template-areas: "element-stack";
@@ -80,28 +75,25 @@ watch(
80
75
  .inner {
81
76
  grid-area: element-stack;
82
77
  display: flex;
83
- gap: var(--_gap-between-top-level-items);
78
+ gap: var(--deep-expanding-menu-gap, 2.4rem);
84
79
  align-items: center;
85
80
 
86
81
  .navigation-link,
87
82
  .navigation-group-toggle {
88
83
  all: unset;
89
- border-bottom: 2px solid transparent;
90
- padding-block: 8px;
84
+ border-bottom: var(--deep-expanding-menu-link-border-width, 0.2rem) solid transparent;
85
+ padding-block: var(--deep-expanding-menu-link-padding-block, 0.8rem);
91
86
 
92
87
  transition: border-color 200ms;
93
88
 
94
- &:hover {
95
- cursor: pointer;
96
- border-color: light-dark(var(--blue-10), var(--slate-00));
97
- }
98
-
99
- &:focus {
100
- border-color: light-dark(var(--blue-10), var(--slate-00));
101
- }
102
-
89
+ &:hover,
90
+ &:focus,
103
91
  &:focus-visible {
104
- border-color: light-dark(var(--blue-10), var(--slate-00));
92
+ cursor: pointer;
93
+ border-color: var(
94
+ --deep-expanding-menu-link-border-colour-hover,
95
+ light-dark(var(--blue-10), var(--slate-00))
96
+ );
105
97
  }
106
98
  }
107
99
 
@@ -114,11 +106,11 @@ watch(
114
106
 
115
107
  display: flex;
116
108
  align-items: center;
117
- gap: 12px;
109
+ gap: 1.2rem;
118
110
 
119
111
  .icon {
120
112
  display: block;
121
- font-size: 1.2rem;
113
+ font-size: var(--deep-expanding-menu-icon-size, 1.2rem);
122
114
 
123
115
  transform: var(--_icon-transform);
124
116
  transition: transform 200ms;
@@ -130,9 +122,8 @@ watch(
130
122
  position: absolute;
131
123
  position-anchor: var(--_anchor-name);
132
124
  margin: 0;
133
- /* inset: auto; */
134
- top: calc(anchor(bottom) + 10px);
135
- left: calc(anchor(left) + 0px);
125
+ top: calc(anchor(bottom) + 1rem);
126
+ left: calc(anchor(left) + 0rem);
136
127
 
137
128
  opacity: 0;
138
129
  transition:
@@ -141,13 +132,14 @@ watch(
141
132
  overlay 200ms;
142
133
  transition-behavior: allow-discrete;
143
134
 
144
- width: min(100%, 50vw);
135
+ width: var(--deep-expanding-menu-panel-width, min(100%, 50vw));
145
136
 
146
- background-color: white;
147
- border: 1px solid black;
148
- border-radius: 12px;
149
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
150
- padding: 12px;
137
+ background-color: var(--deep-expanding-menu-panel-background-colour, white);
138
+ border: var(--deep-expanding-menu-panel-border-width, 0.1rem) solid
139
+ var(--deep-expanding-menu-panel-border-colour, black);
140
+ border-radius: var(--deep-expanding-menu-panel-border-radius, 1.2rem);
141
+ box-shadow: var(--deep-expanding-menu-panel-shadow, 0 0 1rem rgba(0, 0, 0, 0.1));
142
+ padding: var(--deep-expanding-menu-panel-padding, 1.2rem);
151
143
  overflow: clip;
152
144
 
153
145
  &:popover-open {
@@ -161,36 +153,33 @@ watch(
161
153
  }
162
154
 
163
155
  h4 {
164
- color: var(--slate-10);
156
+ color: var(--deep-expanding-menu-panel-heading-colour, var(--slate-10));
165
157
  }
166
158
 
167
159
  .navigation-group-list {
168
160
  display: grid;
169
- grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
170
- gap: 12px;
161
+ grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
162
+ gap: var(--deep-expanding-menu-panel-list-gap, 1.2rem);
171
163
  padding-inline-start: 0;
172
- margin-block-end: 8px;
164
+ margin-block-end: 0.8rem;
173
165
 
174
166
  .navigation-group-item {
175
167
  display: block;
176
168
 
177
169
  a.navigation-group-link {
178
170
  display: inline-block;
179
- color: var(--slate-10);
171
+ color: var(--deep-expanding-menu-group-link-colour, var(--slate-10));
180
172
  text-decoration: none;
181
- padding-block: 8px;
173
+ padding-block: 0.8rem;
182
174
 
183
- border-bottom: 2px solid transparent;
175
+ border-bottom: var(--deep-expanding-menu-link-border-width, 0.2rem) solid transparent;
184
176
 
185
177
  transition: border-color 200ms;
186
178
 
187
- &:hover {
188
- cursor: pointer;
189
- border-color: var(--slate-10);
190
- }
191
-
179
+ &:hover,
192
180
  &:focus-visible {
193
- border-color: var(--slate-10);
181
+ cursor: pointer;
182
+ border-color: var(--deep-expanding-menu-group-link-border-colour-hover, var(--slate-10));
194
183
  }
195
184
  }
196
185
  }
@@ -0,0 +1,79 @@
1
+ import DeepExpandingMenu from "../DeepExpandingMenu.vue";
2
+ import type { Meta, StoryObj } from "@nuxtjs/storybook";
3
+ import type { ResponsiveHeaderNavItem } from "~/types/components";
4
+
5
+ const navLinks: ResponsiveHeaderNavItem[] = [
6
+ { name: "Home", path: "/" },
7
+ {
8
+ name: "Services",
9
+ childLinksTitle: "Our services",
10
+ childLinks: [
11
+ { name: "Haircuts", path: "/services/haircuts" },
12
+ { name: "Colouring", path: "/services/colouring" },
13
+ { name: "Styling", path: "/services/styling" },
14
+ ],
15
+ },
16
+ { name: "Contact", path: "/contact" },
17
+ ];
18
+
19
+ const meta: Meta<typeof DeepExpandingMenu> = {
20
+ title: "Molecules/DeepExpandingMenu",
21
+ component: DeepExpandingMenu,
22
+ argTypes: {
23
+ tag: {
24
+ control: { type: "select" },
25
+ options: ["div", "section", "nav", "ul", "ol"],
26
+ description: "Root element tag",
27
+ table: { category: "Basic" },
28
+ },
29
+ navLinks: { table: { disable: true } },
30
+ styleClassPassthrough: { table: { disable: true } },
31
+ },
32
+ args: {
33
+ tag: "nav",
34
+ navLinks,
35
+ },
36
+ parameters: {
37
+ docs: {
38
+ description: {
39
+ component:
40
+ "Modern implementation of a top-level nav with expanding child-link panels, using CSS anchor-positioning and the Popover API. See `DeepExpandingMenuClassic` for a `<details>`-based fallback with wider browser support.",
41
+ },
42
+ },
43
+ },
44
+ };
45
+
46
+ export default meta;
47
+ type Story = StoryObj<typeof DeepExpandingMenu>;
48
+
49
+ export const Default: Story = {
50
+ render: (args) => ({
51
+ components: { DeepExpandingMenu },
52
+ setup() {
53
+ return { args };
54
+ },
55
+ template: `<DeepExpandingMenu v-bind="args" style="padding: 4rem;" />`,
56
+ }),
57
+ };
58
+
59
+ export const CustomColours: Story = {
60
+ render: (args) => ({
61
+ components: { DeepExpandingMenu },
62
+ setup() {
63
+ return { args };
64
+ },
65
+ template: `
66
+ <DeepExpandingMenu
67
+ v-bind="args"
68
+ style="padding: 4rem; --deep-expanding-menu-panel-background-colour: #1a1a1a; --deep-expanding-menu-panel-border-colour: transparent; --deep-expanding-menu-group-link-colour: white; --deep-expanding-menu-panel-heading-colour: #aaaaaa;"
69
+ />
70
+ `,
71
+ }),
72
+ parameters: {
73
+ docs: {
74
+ description: {
75
+ story: "Overriding the public tokens via inline `--deep-expanding-menu-*` custom properties.",
76
+ },
77
+ },
78
+ },
79
+ };
@@ -0,0 +1,92 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
3
+ import DeepExpandingMenu from "../DeepExpandingMenu.vue";
4
+ import type { ResponsiveHeaderNavItem } from "~/types/components";
5
+
6
+ const defaultNavLinks: ResponsiveHeaderNavItem[] = [
7
+ { name: "Home", path: "/" },
8
+ {
9
+ name: "Services",
10
+ childLinksTitle: "Our services",
11
+ childLinks: [
12
+ { name: "Haircuts", path: "/services/haircuts" },
13
+ { name: "Colouring", path: "/services/colouring" },
14
+ ],
15
+ },
16
+ { name: "Contact", path: "/contact" },
17
+ ];
18
+
19
+ describe("DeepExpandingMenu", () => {
20
+ it("mounts without error", async () => {
21
+ const wrapper = await mountSuspended(DeepExpandingMenu, { props: { navLinks: defaultNavLinks } });
22
+ expect(wrapper.vm).toBeTruthy();
23
+ });
24
+
25
+ it("renders correct HTML structure", async () => {
26
+ const wrapper = await mountSuspended(DeepExpandingMenu, { props: { navLinks: defaultNavLinks } });
27
+ expect(wrapper.html()).toMatchSnapshot();
28
+ });
29
+
30
+ it("renders a <nav> by default", async () => {
31
+ const wrapper = await mountSuspended(DeepExpandingMenu, { props: { navLinks: defaultNavLinks } });
32
+ expect(wrapper.element.tagName.toLowerCase()).toBe("nav");
33
+ });
34
+
35
+ it("renders the given tag", async () => {
36
+ const wrapper = await mountSuspended(DeepExpandingMenu, {
37
+ props: { navLinks: defaultNavLinks, tag: "div" },
38
+ });
39
+ expect(wrapper.element.tagName.toLowerCase()).toBe("div");
40
+ });
41
+
42
+ it("renders a direct link for items with a path", async () => {
43
+ const wrapper = await mountSuspended(DeepExpandingMenu, { props: { navLinks: defaultNavLinks } });
44
+ const links = wrapper.findAll(".navigation-link");
45
+ expect(links.map((l) => l.text())).toEqual(["Home", "Contact"]);
46
+ });
47
+
48
+ it("renders a toggle group for items with childLinks", async () => {
49
+ const wrapper = await mountSuspended(DeepExpandingMenu, { props: { navLinks: defaultNavLinks } });
50
+ expect(wrapper.find(".navigation-group-toggle").text()).toContain("Services");
51
+ });
52
+
53
+ it("renders each child link inside the group panel", async () => {
54
+ const wrapper = await mountSuspended(DeepExpandingMenu, { props: { navLinks: defaultNavLinks } });
55
+ const childLinks = wrapper.findAll(".navigation-group-link");
56
+ expect(childLinks.map((l) => l.text())).toEqual(["Haircuts", "Colouring"]);
57
+ });
58
+
59
+ it("renders the childLinksTitle heading", async () => {
60
+ const wrapper = await mountSuspended(DeepExpandingMenu, { props: { navLinks: defaultNavLinks } });
61
+ expect(wrapper.find(".navigation-group-panel h4").text()).toBe("Our services");
62
+ });
63
+
64
+ it("gives each group toggle a unique popovertarget", async () => {
65
+ const navLinks: ResponsiveHeaderNavItem[] = [
66
+ { name: "A", childLinksTitle: "A", childLinks: [{ name: "A1", path: "/a1" }] },
67
+ { name: "B", childLinksTitle: "B", childLinks: [{ name: "B1", path: "/b1" }] },
68
+ ];
69
+ const wrapper = await mountSuspended(DeepExpandingMenu, { props: { navLinks } });
70
+ const toggles = wrapper.findAll(".navigation-group-toggle");
71
+ const targets = toggles.map((t) => t.attributes("popovertarget"));
72
+ expect(new Set(targets).size).toBe(2);
73
+ });
74
+
75
+ it("applies styleClassPassthrough classes", async () => {
76
+ const wrapper = await mountSuspended(DeepExpandingMenu, {
77
+ props: { navLinks: defaultNavLinks, styleClassPassthrough: ["custom-class"] },
78
+ });
79
+ expect(wrapper.classes()).toContain("custom-class");
80
+ });
81
+
82
+ it("resets classes when styleClassPassthrough prop changes", async () => {
83
+ const wrapper = await mountSuspended(DeepExpandingMenu, {
84
+ props: { navLinks: defaultNavLinks, styleClassPassthrough: ["initial-class"] },
85
+ });
86
+ expect(wrapper.classes()).toContain("initial-class");
87
+
88
+ await wrapper.setProps({ styleClassPassthrough: ["updated-class"] });
89
+ expect(wrapper.classes()).not.toContain("initial-class");
90
+ expect(wrapper.classes()).toContain("updated-class");
91
+ });
92
+ });
@@ -0,0 +1,17 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`DeepExpandingMenu > renders correct HTML structure 1`] = `
4
+ "<nav class="deep-expanding-menu">
5
+ <div class="inner"><a href="/" class="navigation-link">Home</a>
6
+ <div class="navigation-group" style="--_anchor-name: --anchor-v-0-0-1;"><button popovertarget="popovertarget-v-0-0-1" class="navigation-group-toggle"><span>Services</span><span class="iconify i-bi:caret-down-fill icon" aria-hidden="true"></span></button>
7
+ <div id="popovertarget-v-0-0-1" class="navigation-group-panel" popover="" role="menu">
8
+ <h4 class="page-heading-4 mb-6">Our services</h4>
9
+ <ul class="navigation-group-list">
10
+ <li class="navigation-group-item"><a href="/services/haircuts" class="navigation-group-link">Haircuts</a></li>
11
+ <li class="navigation-group-item"><a href="/services/colouring" class="navigation-group-link">Colouring</a></li>
12
+ </ul>
13
+ </div>
14
+ </div><a href="/contact" class="navigation-link">Contact</a>
15
+ </div>
16
+ </nav>"
17
+ `;
@@ -0,0 +1,40 @@
1
+ # DeepExpandingMenuClassic — Consumer Styling Guide
2
+
3
+ ## Public token API
4
+
5
+ | Token | Default | Controls |
6
+ |---|---|---|
7
+ | `--deep-expanding-menu-classic-gap` | `1.2rem` | Gap on the root grid |
8
+ | `--deep-expanding-menu-classic-item-gap` | `2.4rem` | Gap between top-level items |
9
+ | `--deep-expanding-menu-classic-link-border-width` | `0.2rem` | Bottom border width on links and group toggles |
10
+ | `--deep-expanding-menu-classic-link-padding-block` | `0.8rem` | Vertical padding on links and group toggles |
11
+ | `--deep-expanding-menu-classic-link-border-colour-hover` | `light-dark(var(--blue-10), var(--slate-00))` | Bottom border colour on hover/focus |
12
+ | `--deep-expanding-menu-classic-icon-size` | `1.2rem` | Caret icon size |
13
+ | `--deep-expanding-menu-classic-panel-offset-top` | `4rem` | Panel offset from the toggle |
14
+ | `--deep-expanding-menu-classic-panel-width` | `20rem` | Panel width below the `768px` breakpoint |
15
+ | `--deep-expanding-menu-classic-panel-width-tablet` | `40rem` | Panel width from `768px` |
16
+ | `--deep-expanding-menu-classic-panel-width-desktop` | `60rem` | Panel width from `1024px` |
17
+ | `--deep-expanding-menu-classic-panel-background-colour` | `white` | Panel background |
18
+ | `--deep-expanding-menu-classic-panel-border-width` | `0.1rem` | Panel border width |
19
+ | `--deep-expanding-menu-classic-panel-border-colour` | `black` | Panel border colour |
20
+ | `--deep-expanding-menu-classic-panel-border-radius` | `1.2rem` | Panel corner radius |
21
+ | `--deep-expanding-menu-classic-panel-shadow` | `0 0 1rem rgba(0, 0, 0, 0.1)` | Panel box-shadow |
22
+ | `--deep-expanding-menu-classic-panel-padding` | `1.2rem` | Panel padding |
23
+ | `--deep-expanding-menu-classic-panel-heading-colour` | `var(--slate-10)` | `childLinksTitle` heading colour inside the panel |
24
+ | `--deep-expanding-menu-classic-panel-list-gap` | `1.2rem` | Gap between child link grid items |
25
+ | `--deep-expanding-menu-classic-group-link-colour` | `var(--slate-10)` | Child link text colour |
26
+ | `--deep-expanding-menu-classic-group-link-border-colour-hover` | `var(--slate-10)` | Child link bottom border colour on hover/focus |
27
+
28
+ ```css
29
+ .my-page {
30
+ --deep-expanding-menu-classic-panel-background-colour: #1a1a1a;
31
+ --deep-expanding-menu-classic-panel-border-colour: transparent;
32
+ --deep-expanding-menu-classic-group-link-colour: white;
33
+ }
34
+ ```
35
+
36
+ Or scope to a single instance via `styleClassPassthrough`:
37
+
38
+ ```vue
39
+ <DeepExpandingMenuClassic style-class-passthrough="main-nav">...</DeepExpandingMenuClassic>
40
+ ```