srcdev-nuxt-components 9.5.7 → 9.6.1

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 (159) hide show
  1. package/.claude/skills/component-compliance-checklist.md +8 -2
  2. package/.claude/skills/component-dynamic-slots.md +3 -3
  3. package/.claude/skills/component-export-types.md +10 -16
  4. package/.claude/skills/component-naming.md +1 -1
  5. package/.claude/skills/components/alert-content.md +9 -1
  6. package/.claude/skills/components/breadcrumb.md +5 -0
  7. package/.claude/skills/components/container-glow.md +21 -8
  8. package/.claude/skills/components/display-avatar.md +13 -7
  9. package/.claude/skills/components/display-chip.md +37 -24
  10. package/.claude/skills/components/display-pill.md +58 -8
  11. package/.claude/skills/components/display-prompt.md +8 -5
  12. package/.claude/skills/components/display-toast.md +21 -4
  13. package/.claude/skills/components/glowing-border.md +20 -2
  14. package/.claude/skills/components/input-button.md +3 -0
  15. package/.claude/skills/components/responsive-header.md +59 -0
  16. package/.claude/skills/components/select-menu.md +54 -5
  17. package/.claude/skills/components/service-detail.md +32 -0
  18. package/.claude/skills/components/service-summary.md +8 -0
  19. package/.claude/skills/components/services-card-grid.md +5 -3
  20. package/.claude/skills/components/services-card.md +22 -12
  21. package/.claude/skills/components/site-header.md +2 -2
  22. package/.claude/skills/components/slider-gallery.md +10 -4
  23. package/.claude/skills/components/tabbed-content.md +300 -0
  24. package/.claude/skills/composable-anchor-scroll.md +16 -15
  25. package/.claude/skills/index.md +4 -5
  26. package/.claude/skills/storybook-add-story.md +53 -0
  27. package/.claude/skills/theming-status-tokens.md +60 -0
  28. package/.vscode/srcdev-component-display-avatar.code-snippets +2 -1
  29. package/.vscode/srcdev-component-display-chip.code-snippets +3 -2
  30. package/.vscode/srcdev-component-display-pill.code-snippets +10 -0
  31. package/.vscode/srcdev-component-responsive-header.code-snippets +16 -0
  32. package/.vscode/srcdev-component-select-menu.code-snippets +28 -0
  33. package/.vscode/srcdev-component-service-detail.code-snippets +1 -0
  34. package/.vscode/srcdev-component-service-summary.code-snippets +3 -1
  35. package/.vscode/srcdev-component-services-card.code-snippets +38 -36
  36. package/.vscode/srcdev-component-services-grids.code-snippets +2 -1
  37. package/.vscode/{srcdev-component-tabs-core.code-snippets → srcdev-component-tabbed-content.code-snippets} +35 -16
  38. package/app/assets/styles/setup/03.theming/_status.css +24 -0
  39. package/app/assets/styles/setup/03.theming/index.css +1 -0
  40. package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +39 -35
  41. package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +25 -15
  42. package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +130 -24
  43. package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +45 -0
  44. package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +2 -2
  45. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +76 -19
  46. package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +12 -10
  47. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +201 -42
  48. package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +15 -4
  49. package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +1 -1
  50. package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +78 -9
  51. package/app/components/01.atoms/display-pill/DisplayPill.vue +59 -23
  52. package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +240 -16
  53. package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +43 -14
  54. package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +3 -3
  55. package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +16 -4
  56. package/app/components/01.atoms/navigation/breadcrumb/CONSUMER-STYLING.md +28 -5
  57. package/app/components/01.atoms/navigation/breadcrumb/stories/Breadcrumb.stories.ts +94 -0
  58. package/app/components/01.atoms/navigation/breadcrumb/tests/Breadcrumb.spec.ts +27 -0
  59. package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +201 -0
  60. package/app/components/01.atoms/tabbed-content/TabbedContent.vue +750 -0
  61. package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +279 -0
  62. package/app/components/01.atoms/tabbed-content/tests/TabbedContent.spec.ts +816 -0
  63. package/app/components/02.molecules/alert-content/AlertContent.vue +11 -2
  64. package/app/components/02.molecules/alert-content/AlertContentInner.vue +19 -0
  65. package/app/components/02.molecules/alert-content/CONSUMER-STYLING.md +17 -4
  66. package/app/components/02.molecules/alert-content/stories/AlertContent.stories.ts +200 -17
  67. package/app/components/02.molecules/alert-content/tests/AlertContent.spec.ts +33 -0
  68. package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +7 -5
  69. package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +6 -2
  70. package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +24 -3
  71. package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +19 -0
  72. package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +2 -1
  73. package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +52 -19
  74. package/app/components/02.molecules/display-chip/DisplayChip.vue +72 -54
  75. package/app/components/02.molecules/display-chip/stories/DisplayChip.stories.ts +103 -6
  76. package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +98 -29
  77. package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +8 -4
  78. package/app/components/02.molecules/prompt/CONSUMER-STYLING.md +9 -1
  79. package/app/components/02.molecules/prompt/DisplayPrompt.vue +2 -2
  80. package/app/components/02.molecules/prompt/stories/DisplayPrompt.stories.ts +208 -71
  81. package/app/components/02.molecules/prompt/tests/DisplayPrompt.spec.ts +19 -6
  82. package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +66 -8
  83. package/app/components/02.molecules/select-menu/SelectMenu.vue +141 -18
  84. package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +232 -2
  85. package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +151 -0
  86. package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +6 -2
  87. package/app/components/02.molecules/toast/CONSUMER-STYLING.md +91 -27
  88. package/app/components/02.molecules/toast/DisplayToast.vue +109 -50
  89. package/app/components/02.molecules/toast/DisplayToastProvider.vue +109 -45
  90. package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +23 -0
  91. package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +73 -0
  92. package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +132 -18
  93. package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +105 -39
  94. package/app/components/03.organisms/image-galleries/slider-gallery/CONSUMER-STYLING.md +19 -1
  95. package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +65 -7
  96. package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +201 -63
  97. package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +41 -2
  98. package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +6 -6
  99. package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +42 -11
  100. package/app/components/03.organisms/responsive-header/NavigationItems.vue +60 -16
  101. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +86 -24
  102. package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +52 -0
  103. package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +20 -0
  104. package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +40 -0
  105. package/app/components/03.organisms/services/service-detail/CONSUMER-STYLING.md +92 -41
  106. package/app/components/03.organisms/services/service-detail/ServiceDetail.vue +198 -73
  107. package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts +157 -9
  108. package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +58 -0
  109. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +5 -4
  110. package/app/components/03.organisms/services/service-summary/CONSUMER-STYLING.md +30 -0
  111. package/app/components/03.organisms/services/service-summary/ServiceSummary.vue +32 -8
  112. package/app/components/03.organisms/services/service-summary/stories/ServiceSummary.stories.ts +147 -0
  113. package/app/components/03.organisms/services/service-summary/tests/ServiceSummary.spec.ts +26 -0
  114. package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +2 -2
  115. package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +104 -63
  116. package/app/components/03.organisms/services/services-card/ServicesCard.vue +92 -66
  117. package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +194 -27
  118. package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +34 -8
  119. package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +8 -8
  120. package/app/components/03.organisms/services/services-grids/CONSUMER-STYLING.md +9 -0
  121. package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +2 -1
  122. package/app/components/03.organisms/services/services-grids/stories/ServiceSummaryGrid.stories.ts +46 -6
  123. package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +89 -11
  124. package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +8 -0
  125. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +6 -6
  126. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +24 -24
  127. package/app/components/05.forms/input-button/CONSUMER-STYLING.md +8 -0
  128. package/app/components/05.forms/input-button/InputButton.vue +3 -1
  129. package/app/composables/tests/useAnchorScroll.spec.ts +8 -0
  130. package/app/composables/useAnchorScroll.ts +1 -1
  131. package/app/composables/useTabs.ts +148 -206
  132. package/app/types/components/display-chip.d.ts +8 -4
  133. package/app/types/components/display-toast.d.ts +2 -0
  134. package/app/types/components/index.ts +0 -1
  135. package/app/types/components/select-menu.d.ts +2 -0
  136. package/app/utils/pausableTimeout.ts +41 -0
  137. package/nuxt.config.ts +1 -1
  138. package/package.json +1 -1
  139. package/.claude/skills/components/site-navigation.md +0 -145
  140. package/.claude/skills/components/tab-navigation.md +0 -272
  141. package/.claude/skills/components/tabs-core.md +0 -187
  142. package/.vscode/srcdev-component-site-navigation.code-snippets +0 -38
  143. package/.vscode/srcdev-component-tab-navigation.code-snippets +0 -30
  144. package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +0 -120
  145. package/app/components/01.atoms/navigation/tabs/TabsCore.vue +0 -272
  146. package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +0 -199
  147. package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +0 -274
  148. package/app/components/02.molecules/navigation/site-navigation/CONSUMER-STYLING.md +0 -109
  149. package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +0 -751
  150. package/app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts +0 -356
  151. package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +0 -367
  152. package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +0 -30
  153. package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +0 -150
  154. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +0 -518
  155. package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +0 -340
  156. package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +0 -339
  157. package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +0 -33
  158. package/app/composables/useNavCollapse.ts +0 -86
  159. package/app/types/components/nav-item.d.ts +0 -11
@@ -1,367 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { mountSuspended } from "@nuxt/test-utils/runtime";
3
- import { defineComponent, nextTick } from "vue";
4
- import SiteNavigation from "../SiteNavigation.vue";
5
- import type { NavItemData } from "~/types/components/nav-item.d";
6
-
7
- // useResizeObserver (from @vueuse/core) requires ResizeObserver
8
- // Vitest 4: a mock's implementation must be a regular function (not an
9
- // arrow function) to remain constructible via `new`.
10
- const mockResizeObserver = vi.fn(function () {
11
- return {
12
- observe: vi.fn(),
13
- unobserve: vi.fn(),
14
- disconnect: vi.fn(),
15
- };
16
- });
17
-
18
- beforeEach(() => {
19
- vi.stubGlobal("ResizeObserver", mockResizeObserver);
20
- // ⚠️ Do NOT call vi.unstubAllGlobals() in afterEach —
21
- // it removes global stubs from vitest.setup.ts ($fetch, etc.)
22
- });
23
-
24
- const defaultNavItemData: NavItemData = {
25
- main: [
26
- { text: "Home", href: "/" },
27
- { text: "About", href: "/about" },
28
- { text: "Contact", href: "/contact" },
29
- ],
30
- };
31
-
32
- describe("SiteNavigation", () => {
33
- it("mounts without error", async () => {
34
- const wrapper = await mountSuspended(SiteNavigation, {
35
- props: { navItemData: defaultNavItemData },
36
- });
37
- expect(wrapper.vm).toBeTruthy();
38
- });
39
-
40
- it("renders a nav element as root", async () => {
41
- const wrapper = await mountSuspended(SiteNavigation, {
42
- props: { navItemData: defaultNavItemData },
43
- });
44
- expect(wrapper.element.tagName.toLowerCase()).toBe("nav");
45
- });
46
-
47
- it("has aria-label on the nav", async () => {
48
- const wrapper = await mountSuspended(SiteNavigation, {
49
- props: { navItemData: defaultNavItemData },
50
- });
51
- expect(wrapper.attributes("aria-label")).toBe("Site navigation");
52
- });
53
-
54
- it("renders correct HTML structure", async () => {
55
- const wrapper = await mountSuspended(SiteNavigation, {
56
- props: { navItemData: defaultNavItemData },
57
- });
58
- expect(wrapper.html()).toMatchSnapshot();
59
- });
60
-
61
- // ─── isLoaded ──────────────────────────────────────────────────────────────
62
-
63
- it("applies is-loaded class after mounting", async () => {
64
- const wrapper = await mountSuspended(SiteNavigation, {
65
- props: { navItemData: defaultNavItemData },
66
- });
67
- await nextTick();
68
- expect(wrapper.classes()).toContain("is-loaded");
69
- });
70
-
71
- // ─── Nav list ──────────────────────────────────────────────────────────────
72
-
73
- it("renders the nav list when not collapsed", async () => {
74
- const wrapper = await mountSuspended(SiteNavigation, {
75
- props: { navItemData: defaultNavItemData },
76
- });
77
- expect(wrapper.find(".site-nav-list").exists()).toBe(true);
78
- });
79
-
80
- it("renders the correct number of nav items", async () => {
81
- const wrapper = await mountSuspended(SiteNavigation, {
82
- props: { navItemData: defaultNavItemData },
83
- });
84
- // Filter out .nav-indicator-li elements injected by initNavDecorators
85
- const items = wrapper.findAll(".site-nav-list li:not(.nav-indicator-li)");
86
- expect(items.length).toBe(3);
87
- });
88
-
89
- it("renders item text correctly", async () => {
90
- const wrapper = await mountSuspended(SiteNavigation, {
91
- props: { navItemData: defaultNavItemData },
92
- });
93
- const links = wrapper.findAll(".site-nav-link");
94
- expect(links[0]!.text()).toContain("Home");
95
- expect(links[1]!.text()).toContain("About");
96
- expect(links[2]!.text()).toContain("Contact");
97
- });
98
-
99
- it("renders item href correctly", async () => {
100
- const wrapper = await mountSuspended(SiteNavigation, {
101
- props: { navItemData: defaultNavItemData },
102
- });
103
- const links = wrapper.findAll(".site-nav-link");
104
- expect(links[0]!.attributes("href")).toBe("/");
105
- expect(links[1]!.attributes("href")).toBe("/about");
106
- expect(links[2]!.attributes("href")).toBe("/contact");
107
- });
108
-
109
- it("applies cssName as class on the li element", async () => {
110
- const navItemData: NavItemData = {
111
- main: [{ text: "Home", href: "/", cssName: "is-active" }],
112
- };
113
- const wrapper = await mountSuspended(SiteNavigation, {
114
- props: { navItemData },
115
- });
116
- expect(wrapper.find(".site-nav-list li").classes()).toContain("is-active");
117
- });
118
-
119
- it("renders an icon when iconName is provided", async () => {
120
- const navItemData: NavItemData = {
121
- main: [{ text: "Home", href: "/", iconName: "heroicons:home" }],
122
- };
123
- const wrapper = await mountSuspended(SiteNavigation, {
124
- props: { navItemData },
125
- });
126
- // Icon renders with aria-hidden="true"
127
- const firstItem = wrapper.find(".site-nav-list li:not(.nav-indicator-li)");
128
- expect(firstItem.find("[aria-hidden='true']").exists()).toBe(true);
129
- });
130
-
131
- it("does not render an icon when iconName is not provided", async () => {
132
- const wrapper = await mountSuspended(SiteNavigation, {
133
- props: { navItemData: defaultNavItemData },
134
- });
135
- const firstLink = wrapper.find(".site-nav-link");
136
- expect(firstLink.find("[aria-hidden='true']").exists()).toBe(false);
137
- });
138
-
139
- it("renders external links with the correct href", async () => {
140
- const navItemData: NavItemData = {
141
- main: [{ text: "External", href: "https://example.com", isExternal: true }],
142
- };
143
- const wrapper = await mountSuspended(SiteNavigation, {
144
- props: { navItemData },
145
- });
146
- expect(wrapper.find(".site-nav-link").attributes("href")).toBe("https://example.com");
147
- });
148
-
149
- // ─── Alignment variants ────────────────────────────────────────────────────
150
-
151
- it("applies site-navigation--left class by default", async () => {
152
- const wrapper = await mountSuspended(SiteNavigation, {
153
- props: { navItemData: defaultNavItemData },
154
- });
155
- expect(wrapper.classes()).toContain("site-navigation--left");
156
- });
157
-
158
- it("applies site-navigation--center class when navAlign is center", async () => {
159
- const wrapper = await mountSuspended(SiteNavigation, {
160
- props: { navItemData: defaultNavItemData, navAlign: "center" },
161
- });
162
- expect(wrapper.classes()).toContain("site-navigation--center");
163
- });
164
-
165
- it("applies site-navigation--right class when navAlign is right", async () => {
166
- const wrapper = await mountSuspended(SiteNavigation, {
167
- props: { navItemData: defaultNavItemData, navAlign: "right" },
168
- });
169
- expect(wrapper.classes()).toContain("site-navigation--right");
170
- });
171
-
172
- // ─── styleClassPassthrough ─────────────────────────────────────────────────
173
-
174
- it("applies styleClassPassthrough classes to the nav element", async () => {
175
- const wrapper = await mountSuspended(SiteNavigation, {
176
- props: {
177
- navItemData: defaultNavItemData,
178
- styleClassPassthrough: ["custom-nav", "theme-dark"],
179
- },
180
- });
181
- expect(wrapper.classes()).toContain("custom-nav");
182
- expect(wrapper.classes()).toContain("theme-dark");
183
- });
184
-
185
- it("updates classes when styleClassPassthrough prop changes", async () => {
186
- const wrapper = await mountSuspended(SiteNavigation, {
187
- props: {
188
- navItemData: defaultNavItemData,
189
- styleClassPassthrough: ["initial-class"],
190
- },
191
- });
192
- expect(wrapper.classes()).toContain("initial-class");
193
-
194
- await wrapper.setProps({ styleClassPassthrough: ["updated-class"] });
195
- await nextTick();
196
-
197
- expect(wrapper.classes()).toContain("updated-class");
198
- expect(wrapper.classes()).not.toContain("initial-class");
199
- });
200
-
201
- // ─── Non-collapsed state (default in JSDOM — dimensions are all 0) ─────────
202
-
203
- it("does not show the burger button when not collapsed", async () => {
204
- const wrapper = await mountSuspended(SiteNavigation, {
205
- props: { navItemData: defaultNavItemData },
206
- });
207
- expect(wrapper.find(".site-nav-burger").exists()).toBe(false);
208
- });
209
-
210
- it("does not show the nav panel when not collapsed", async () => {
211
- const wrapper = await mountSuspended(SiteNavigation, {
212
- props: { navItemData: defaultNavItemData },
213
- });
214
- expect(wrapper.find(".site-nav-panel").exists()).toBe(false);
215
- });
216
-
217
- it("does not have is-collapsed class when not collapsed", async () => {
218
- const wrapper = await mountSuspended(SiteNavigation, {
219
- props: { navItemData: defaultNavItemData },
220
- });
221
- expect(wrapper.classes()).not.toContain("is-collapsed");
222
- });
223
-
224
- // ─── Collapsed state ───────────────────────────────────────────────────────
225
- // isCollapsed is driven by DOM layout measurements (scrollWidth > clientWidth).
226
- // In JSDOM both values are 0 so we cannot trigger collapse via resize.
227
- // Instead we force the state directly through the component's setup context.
228
-
229
- describe("when collapsed", () => {
230
- // setupState auto-unwraps refs — set the plain value directly
231
- interface SiteNavSetup {
232
- isCollapsed: boolean;
233
- isMenuOpen: boolean;
234
- }
235
-
236
- async function mountCollapsed(extraProps: Record<string, unknown> = {}) {
237
- const wrapper = await mountSuspended(SiteNavigation, {
238
- props: { navItemData: defaultNavItemData, ...extraProps },
239
- });
240
- const setup = (wrapper.vm as unknown as { $: { setupState: SiteNavSetup } }).$.setupState;
241
- setup.isCollapsed = true;
242
- await nextTick();
243
- return { wrapper, setup };
244
- }
245
-
246
- it("applies is-collapsed class", async () => {
247
- const { wrapper } = await mountCollapsed();
248
- expect(wrapper.classes()).toContain("is-collapsed");
249
- });
250
-
251
- it("hides the horizontal nav list", async () => {
252
- const { wrapper } = await mountCollapsed();
253
- expect(wrapper.find(".site-nav-list").exists()).toBe(false);
254
- });
255
-
256
- it("shows the burger button", async () => {
257
- const { wrapper } = await mountCollapsed();
258
- expect(wrapper.find(".site-nav-burger").exists()).toBe(true);
259
- });
260
-
261
- it("shows the nav panel", async () => {
262
- const { wrapper } = await mountCollapsed();
263
- expect(wrapper.find(".site-nav-panel").exists()).toBe(true);
264
- });
265
-
266
- it("burger button has aria-expanded false when menu is closed", async () => {
267
- const { wrapper } = await mountCollapsed();
268
- expect(wrapper.find(".site-nav-burger").attributes("aria-expanded")).toBe("false");
269
- });
270
-
271
- it("panel does not have is-open class when menu is closed", async () => {
272
- const { wrapper } = await mountCollapsed();
273
- expect(wrapper.find(".site-nav-panel").classes()).not.toContain("is-open");
274
- });
275
-
276
- it("renders panel links matching main nav items", async () => {
277
- const { wrapper } = await mountCollapsed();
278
- const panelLinks = wrapper.findAll(".site-nav-panel-link");
279
- expect(panelLinks.length).toBe(3);
280
- expect(panelLinks[0]!.text()).toContain("Home");
281
- expect(panelLinks[1]!.text()).toContain("About");
282
- expect(panelLinks[2]!.text()).toContain("Contact");
283
- });
284
-
285
- it("toggles isMenuOpen on burger click", async () => {
286
- const { wrapper, setup } = await mountCollapsed();
287
- expect(setup.isMenuOpen).toBe(false);
288
- await wrapper.find(".site-nav-burger").trigger("click");
289
- expect(setup.isMenuOpen).toBe(true);
290
- await wrapper.find(".site-nav-burger").trigger("click");
291
- expect(setup.isMenuOpen).toBe(false);
292
- });
293
-
294
- it("applies menu-open class when menu is open", async () => {
295
- const { wrapper } = await mountCollapsed();
296
- await wrapper.find(".site-nav-burger").trigger("click");
297
- await nextTick();
298
- expect(wrapper.classes()).toContain("menu-open");
299
- });
300
-
301
- it("burger has aria-expanded true when menu is open", async () => {
302
- const { wrapper } = await mountCollapsed();
303
- await wrapper.find(".site-nav-burger").trigger("click");
304
- await nextTick();
305
- expect(wrapper.find(".site-nav-burger").attributes("aria-expanded")).toBe("true");
306
- });
307
-
308
- it("panel has is-open class when menu is open", async () => {
309
- const { wrapper } = await mountCollapsed();
310
- await wrapper.find(".site-nav-burger").trigger("click");
311
- await nextTick();
312
- expect(wrapper.find(".site-nav-panel").classes()).toContain("is-open");
313
- });
314
-
315
- it("panel is inert when menu is closed", async () => {
316
- const { wrapper } = await mountCollapsed();
317
- expect(wrapper.find(".site-nav-panel").attributes("inert")).toBeDefined();
318
- });
319
-
320
- it("panel is not inert when menu is open", async () => {
321
- const { wrapper } = await mountCollapsed();
322
- await wrapper.find(".site-nav-burger").trigger("click");
323
- await nextTick();
324
- expect(wrapper.find(".site-nav-panel").attributes("inert")).toBeUndefined();
325
- });
326
-
327
- it("burger aria-controls points at the panel's own id", async () => {
328
- const { wrapper } = await mountCollapsed();
329
- const panelId = wrapper.find(".site-nav-panel").attributes("id");
330
- expect(panelId).toMatch(/^site-nav-panel-/);
331
- expect(wrapper.find(".site-nav-burger").attributes("aria-controls")).toBe(panelId);
332
- });
333
-
334
- it("gives two instances on the same page distinct panel ids", async () => {
335
- const Host = defineComponent({
336
- components: { SiteNavigation },
337
- setup: () => ({ navItemData: defaultNavItemData }),
338
- template: `<div><SiteNavigation :nav-item-data="navItemData" /><SiteNavigation :nav-item-data="navItemData" /></div>`,
339
- });
340
- const host = await mountSuspended(Host);
341
- const navs = host.findAllComponents(SiteNavigation);
342
- for (const nav of navs) {
343
- (nav.vm as unknown as { $: { setupState: SiteNavSetup } }).$.setupState.isCollapsed = true;
344
- }
345
- await nextTick();
346
- const ids = host.findAll(".site-nav-panel").map((p) => p.attributes("id"));
347
- expect(ids).toHaveLength(2);
348
- expect(ids[0]).not.toBe(ids[1]);
349
- });
350
-
351
- it("uses the default burger labels", async () => {
352
- const { wrapper } = await mountCollapsed();
353
- expect(wrapper.find(".site-nav-burger").text()).toContain("Open navigation menu");
354
- await wrapper.find(".site-nav-burger").trigger("click");
355
- await nextTick();
356
- expect(wrapper.find(".site-nav-burger").text()).toContain("Close navigation menu");
357
- });
358
-
359
- it("uses openMenuLabel and closeMenuLabel when given", async () => {
360
- const { wrapper } = await mountCollapsed({ openMenuLabel: "Ouvrir le menu", closeMenuLabel: "Fermer le menu" });
361
- expect(wrapper.find(".site-nav-burger").text()).toContain("Ouvrir le menu");
362
- await wrapper.find(".site-nav-burger").trigger("click");
363
- await nextTick();
364
- expect(wrapper.find(".site-nav-burger").text()).toContain("Fermer le menu");
365
- });
366
- });
367
- });
@@ -1,30 +0,0 @@
1
- // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
-
3
- exports[`SiteNavigation > renders correct HTML structure 1`] = `
4
- "<nav class="site-navigation site-navigation--left is-loaded" aria-label="Site navigation">
5
- <ul class="site-nav-list" style="--_transition-duration: 0ms; --_x-active: 0px; --_width-active: NaN; --_x-hovered: 0px; --_width-hovered: NaN;">
6
- <li class="is-active"><a href="/" class="site-nav-link" data-nav-item="">
7
- <!--v-if--> Home
8
- </a></li>
9
- <li class=""><a href="/about" class="site-nav-link" data-nav-item="">
10
- <!--v-if--> About
11
- </a></li>
12
- <li class=""><a href="/contact" class="site-nav-link" data-nav-item="">
13
- <!--v-if--> Contact
14
- </a></li>
15
- <li aria-hidden="true" role="none" class="nav-indicator-li">
16
- <div class="nav__hovered"></div>
17
- </li>
18
- <li aria-hidden="true" role="none" class="nav-indicator-li">
19
- <div class="nav__active"></div>
20
- </li>
21
- <li aria-hidden="true" role="none" class="nav-indicator-li">
22
- <div class="nav__active-indicator"></div>
23
- </li>
24
- </ul>
25
- <!--v-if-->
26
- <!--teleport start-->
27
- <!--teleport end-->
28
- <!--v-if-->
29
- </nav>"
30
- `;
@@ -1,150 +0,0 @@
1
- # TabNavigation — Consumer Styling Guide
2
-
3
- ## Public token API
4
-
5
- All `--tab-nav-*` tokens are the stable override surface. Set them at any scope (global, page, or
6
- instance) without touching the component itself.
7
-
8
- ### Horizontal nav
9
-
10
- | Token | Default | Controls |
11
- |---|---|---|
12
- | `--tab-nav-link-color` | `var(--slate-01, currentColor)` | Link text colour (rest state) |
13
- | `--tab-nav-link-hover-color` | `var(--slate-04, currentColor)` | Link text colour on hover |
14
- | `--tab-nav-link-active-color` | `var(--slate-01, currentColor)` | Link text colour when route is active |
15
- | `--tab-nav-link-size` | `1.6rem` | Link font size |
16
- | `--tab-nav-link-weight` | `400` | Link font weight |
17
- | `--tab-nav-link-tracking` | `0.06em` | Link letter spacing |
18
- | `--tab-nav-gap` | `2.2rem` | Gap between nav items |
19
- | `--tab-nav-transition` | `250ms ease` | Colour transition on hover/active |
20
-
21
- ### Indicator decorators
22
-
23
- | Token | Default | Controls |
24
- |---|---|---|
25
- | `--tab-nav-decorator-indicator-color` | `var(--slate-01, currentColor)` | Active-item underline bar colour |
26
- | `--tab-nav-decorator-hovered-bg` | `transparent` | Background fill pill that follows the pointer |
27
-
28
- ### Mobile panel
29
-
30
- | Token | Default | Controls |
31
- |---|---|---|
32
- | `--tab-nav-panel-bg` | `var(--page-bg, #1a1614)` | Panel background colour |
33
- | `--tab-nav-panel-border-color` | `color-mix(in oklch, var(--slate-01, #c0847a) 35%, transparent)` | Border between nav bar and open panel |
34
- | `--tab-nav-panel-item-border` | `color-mix(in oklch, var(--slate-01, white) 8%, transparent)` | Divider between panel items |
35
- | `--tab-nav-panel-link-color` | `var(--slate-01, currentColor)` | Panel link text colour |
36
- | `--tab-nav-panel-link-hover-color` | `var(--slate-04, currentColor)` | Panel link text colour on hover |
37
- | `--tab-nav-panel-link-active-color` | `var(--slate-01, currentColor)` | Panel active link colour |
38
- | `--tab-nav-panel-padding-block` | `1.4rem` | Panel link vertical padding |
39
- | `--tab-nav-panel-padding-inline` | `1.5rem` | Panel link horizontal padding |
40
- | `--tab-nav-panel-slide-duration` | `350ms` | Panel open/close animation duration |
41
- | `--tab-nav-panel-slide-easing` | `cubic-bezier(0.4, 0, 0.2, 1)` | Panel open/close easing |
42
-
43
- ### Burger button
44
-
45
- | Token | Default | Controls |
46
- |---|---|---|
47
- | `--tab-nav-burger-color` | `var(--slate-01, currentColor)` | Burger bar colour |
48
- | `--tab-nav-burger-width` | `22px` | Width of each burger bar |
49
- | `--tab-nav-burger-height` | `1.5px` | Height of each burger bar |
50
- | `--tab-nav-burger-gap` | `5px` | Gap between burger bars |
51
- | `--tab-nav-burger-transition` | `300ms ease` | Burger open/close animation |
52
-
53
- ### Backdrop
54
-
55
- | Token | Default | Controls |
56
- |---|---|---|
57
- | `--tab-nav-backdrop-bg` | `oklch(0% 0 0 / 55%)` | Backdrop overlay background colour |
58
- | `--tab-nav-backdrop-blur` | `3px` | Backdrop blur amount |
59
- | `--tab-nav-backdrop-duration` | `350ms` | Backdrop fade duration |
60
-
61
- ---
62
-
63
- ## Global theming
64
-
65
- Create `assets/styles/setup/07.components/tab-navigation.css` in the consuming app and set tokens
66
- on `:root`. These values apply to every `TabNavigation` instance across the site.
67
-
68
- ```css
69
- /* assets/styles/setup/07.components/tab-navigation.css */
70
- :root {
71
- --tab-nav-link-color: var(--brand-text);
72
- --tab-nav-link-hover-color: var(--brand-text-muted);
73
- --tab-nav-link-active-color: var(--brand-accent);
74
- --tab-nav-decorator-indicator-color: var(--brand-accent);
75
- --tab-nav-decorator-hovered-bg: oklch(from var(--brand-accent) l c h / 0.1);
76
- --tab-nav-gap: 3rem;
77
- }
78
- ```
79
-
80
- ---
81
-
82
- ## Local overrides
83
-
84
- Set the tokens above on an element you own (a page or section class, or a class added with
85
- `:style-class-passthrough`): they inherit down into the component. Keep the block **unlayered** (no
86
- `@layer` wrapper) so it beats the library's `@layer components`. If your own file uses
87
- `<style scoped>`, tokens set on your element still work, but selectors that reach inside the component need
88
- `:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
89
-
90
- ### Page or section
91
-
92
- Override tokens for a single page by scoping them under a page wrapper. No `:deep()` is required
93
- (component styles are unscoped).
94
-
95
- ```css
96
- /* In the consuming page's unscoped <style> block */
97
- .about-page {
98
- .tab-navigation {
99
- --tab-nav-link-color: var(--brand-warm-text);
100
- --tab-nav-decorator-indicator-color: var(--brand-warm-accent);
101
- --tab-nav-gap: 4rem;
102
- }
103
- }
104
- ```
105
-
106
- ### One instance: inline style
107
-
108
- ```vue
109
- <TabNavigation
110
- :nav-item-data="navData"
111
- style="
112
- --tab-nav-decorator-indicator-color: oklch(65% 0.2 230);
113
- --tab-nav-link-active-color: oklch(65% 0.2 230);
114
- --tab-nav-gap: 3rem;
115
- "
116
- />
117
- ```
118
-
119
- ### One instance: style-class-passthrough
120
-
121
- ```vue
122
- <TabNavigation :nav-item-data="navData" :style-class-passthrough="['brand-nav']" />
123
- ```
124
-
125
- ```css
126
- .tab-navigation.brand-nav {
127
- --tab-nav-decorator-indicator-color: var(--brand-accent);
128
- --tab-nav-decorator-hovered-bg: oklch(from var(--brand-accent) l c h / 0.1);
129
- --tab-nav-link-active-color: var(--brand-accent);
130
- --tab-nav-link-size: 1.4rem;
131
- --tab-nav-link-tracking: 0.08em;
132
- }
133
- ```
134
-
135
- ---
136
-
137
- ## Notes
138
-
139
- - The active underline indicator and hover pill use CSS Anchor Positioning. They animate via
140
- `left`/`right` transitions, so `transition` on `.nav__active-indicator` and `.nav__hovered` is
141
- handled internally — only set `--tab-nav-transition` to control the link colour fade.
142
- - `--tab-nav-decorator-hovered-bg: transparent` (the default) hides the hover pill entirely; set
143
- a semi-transparent colour to enable it.
144
- - Panel and burger tokens only take visual effect when the nav has collapsed to the burger state
145
- (i.e. when the nav list overflows its container).
146
- - `--tab-nav-panel-bg` should match `--page-bg` so the panel blends with the page background in
147
- the collapsed state.
148
- - The backdrop (`--tab-nav-backdrop-*`) appears behind the open panel and above page content at
149
- `z-index: 10`. It teleports to `<body>` so it is unaffected by parent stacking contexts.
150
-