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,274 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { mountSuspended } from "@nuxt/test-utils/runtime";
3
- import TabsCore from "../TabsCore.vue";
4
-
5
- class MockResizeObserver {
6
- observe = vi.fn();
7
- unobserve = vi.fn();
8
- disconnect = vi.fn();
9
- }
10
-
11
- const slots = {
12
- "tab-0-trigger": "Tab One",
13
- "tab-0-content": "<p class='panel-0'>Content one</p>",
14
- "tab-1-trigger": "Tab Two",
15
- "tab-1-content": "<p class='panel-1'>Content two</p>",
16
- "tab-2-trigger": "Tab Three",
17
- "tab-2-content": "<p class='panel-2'>Content three</p>",
18
- };
19
-
20
- const fiveSlots = {
21
- ...slots,
22
- "tab-3-trigger": "Tab Four",
23
- "tab-3-content": "<p class='panel-3'>Content four</p>",
24
- "tab-4-trigger": "Tab Five",
25
- "tab-4-content": "<p class='panel-4'>Content five</p>",
26
- };
27
-
28
- describe("TabsCore", () => {
29
- beforeEach(() => {
30
- vi.stubGlobal("ResizeObserver", MockResizeObserver);
31
- });
32
-
33
- // ─── Mount ───────────────────────────────────────────────────────────────
34
-
35
- it("mounts without error", async () => {
36
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
37
- expect(wrapper.vm).toBeTruthy();
38
- });
39
-
40
- it("renders one trigger and one panel per itemCount", async () => {
41
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
42
- expect(wrapper.findAll(".tabs-list-item")).toHaveLength(3);
43
- expect(wrapper.findAll(".tab-content")).toHaveLength(3);
44
- });
45
-
46
- // ─── Accessibility structure ─────────────────────────────────────────────
47
-
48
- it("renders a tablist with the default aria-label", async () => {
49
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
50
- const tablist = wrapper.find("[role='tablist']");
51
- expect(tablist.exists()).toBe(true);
52
- expect(tablist.attributes("aria-label")).toBe("Tabs");
53
- });
54
-
55
- it("uses a custom ariaLabel when provided", async () => {
56
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3, ariaLabel: "Settings" }, slots });
57
- expect(wrapper.find("[role='tablist']").attributes("aria-label")).toBe("Settings");
58
- });
59
-
60
- it("renders each trigger as a type=button with role=tab", async () => {
61
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
62
- const triggers = wrapper.findAll(".tabs-list-item");
63
- triggers.forEach((trigger) => {
64
- expect(trigger.attributes("type")).toBe("button");
65
- expect(trigger.attributes("role")).toBe("tab");
66
- });
67
- });
68
-
69
- it("renders each panel as role=tabpanel labelled by its trigger", async () => {
70
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
71
- const panels = wrapper.findAll(".tab-content");
72
- panels.forEach((panel, index) => {
73
- expect(panel.attributes("role")).toBe("tabpanel");
74
- expect(panel.attributes("aria-labelledby")).toBe(`tab-${index}-trigger`);
75
- });
76
- });
77
-
78
- // ─── Active tab state ────────────────────────────────────────────────────
79
-
80
- it("marks the first tab active and its panel visible/unhidden on mount", async () => {
81
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
82
- const triggers = wrapper.findAll(".tabs-list-item");
83
- const panels = wrapper.findAll(".tab-content");
84
-
85
- expect(triggers[0]!.attributes("aria-selected")).toBe("true");
86
- expect(triggers[0]!.attributes("tabindex")).toBe("0");
87
- expect(triggers[1]!.attributes("aria-selected")).toBe("false");
88
- expect(triggers[1]!.attributes("tabindex")).toBe("-1");
89
-
90
- expect(panels[0]!.attributes("aria-hidden")).toBe("false");
91
- expect((panels[0]!.element as HTMLElement).style.display).toBe("block");
92
- expect(panels[1]!.attributes("aria-hidden")).toBe("true");
93
- expect((panels[1]!.element as HTMLElement).style.display).toBe("none");
94
- });
95
-
96
- it("activates a tab on click and updates aria-selected, tabindex, and panel visibility", async () => {
97
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
98
- const triggers = wrapper.findAll(".tabs-list-item");
99
- const panels = wrapper.findAll(".tab-content");
100
-
101
- await triggers[1]!.trigger("click");
102
-
103
- expect(triggers[0]!.attributes("aria-selected")).toBe("false");
104
- expect(triggers[0]!.attributes("tabindex")).toBe("-1");
105
- expect(triggers[1]!.attributes("aria-selected")).toBe("true");
106
- expect(triggers[1]!.attributes("tabindex")).toBe("0");
107
-
108
- expect(panels[0]!.attributes("aria-hidden")).toBe("true");
109
- expect(panels[1]!.attributes("aria-hidden")).toBe("false");
110
- expect((panels[1]!.element as HTMLElement).style.display).toBe("block");
111
- });
112
-
113
- // ─── Keyboard navigation ─────────────────────────────────────────────────
114
-
115
- it("moves focus and activates the next tab on ArrowRight (axis x)", async () => {
116
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
117
- const triggers = wrapper.findAll(".tabs-list-item");
118
-
119
- await triggers[0]!.trigger("keydown", { key: "ArrowRight" });
120
-
121
- expect(triggers[1]!.attributes("aria-selected")).toBe("true");
122
- expect(triggers[1]!.attributes("tabindex")).toBe("0");
123
- });
124
-
125
- it("wraps to the first tab on ArrowRight from the last tab", async () => {
126
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
127
- const triggers = wrapper.findAll(".tabs-list-item");
128
-
129
- await triggers[2]!.trigger("click");
130
- await triggers[2]!.trigger("keydown", { key: "ArrowRight" });
131
-
132
- expect(triggers[0]!.attributes("aria-selected")).toBe("true");
133
- });
134
-
135
- it("moves focus to the previous tab on ArrowLeft (axis x)", async () => {
136
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
137
- const triggers = wrapper.findAll(".tabs-list-item");
138
-
139
- await triggers[1]!.trigger("click");
140
- await triggers[1]!.trigger("keydown", { key: "ArrowLeft" });
141
-
142
- expect(triggers[0]!.attributes("aria-selected")).toBe("true");
143
- });
144
-
145
- it("uses ArrowUp/ArrowDown instead of ArrowLeft/ArrowRight when axis is y", async () => {
146
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3, axis: "y" }, slots });
147
- const triggers = wrapper.findAll(".tabs-list-item");
148
-
149
- await triggers[0]!.trigger("keydown", { key: "ArrowRight" });
150
- expect(triggers[0]!.attributes("aria-selected")).toBe("true");
151
-
152
- await triggers[0]!.trigger("keydown", { key: "ArrowDown" });
153
- expect(triggers[1]!.attributes("aria-selected")).toBe("true");
154
- });
155
-
156
- it("jumps to the last tab on End and the first tab on Home", async () => {
157
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
158
- const triggers = wrapper.findAll(".tabs-list-item");
159
-
160
- await triggers[0]!.trigger("keydown", { key: "End" });
161
- expect(triggers[2]!.attributes("aria-selected")).toBe("true");
162
-
163
- await triggers[2]!.trigger("keydown", { key: "Home" });
164
- expect(triggers[0]!.attributes("aria-selected")).toBe("true");
165
- });
166
-
167
- // ─── trackHover / trackActive / trackIndicator ──────────────────────────
168
-
169
- it("renders all three indicator decorators by default", async () => {
170
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
171
- expect(wrapper.find(".nav__hovered").exists()).toBe(true);
172
- expect(wrapper.find(".nav__active").exists()).toBe(true);
173
- expect(wrapper.find(".nav__active-indicator").exists()).toBe(true);
174
- });
175
-
176
- it("omits the hover decorator when trackHover is false", async () => {
177
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3, trackHover: false }, slots });
178
- expect(wrapper.find(".nav__hovered").exists()).toBe(false);
179
- });
180
-
181
- it("omits the active decorator when trackActive is false", async () => {
182
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3, trackActive: false }, slots });
183
- expect(wrapper.find(".nav__active").exists()).toBe(false);
184
- });
185
-
186
- it("omits the underline indicator when trackIndicator is false", async () => {
187
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3, trackIndicator: false }, slots });
188
- expect(wrapper.find(".nav__active-indicator").exists()).toBe(false);
189
- });
190
-
191
- // ─── Hover indicator movement ────────────────────────────────────────────
192
-
193
- it("moves the hover indicator position on mouseenter", async () => {
194
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
195
- const tabsList = wrapper.find(".tabs-list").element as HTMLElement;
196
- const triggers = wrapper.findAll(".tabs-list-item");
197
-
198
- expect(tabsList.style.getPropertyValue("--_width-hovered")).toBe("");
199
-
200
- await triggers[1]!.trigger("mouseenter");
201
-
202
- expect(tabsList.style.getPropertyValue("--_width-hovered")).not.toBe("");
203
- });
204
-
205
- it("resets the hover indicator to the active tab on mouseleave", async () => {
206
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
207
- const tabsList = wrapper.find(".tabs-list");
208
- const triggers = wrapper.findAll(".tabs-list-item");
209
-
210
- await triggers[1]!.trigger("mouseenter");
211
- await tabsList.trigger("mouseleave");
212
-
213
- // Should not throw, and the hover indicator settles back onto the active (first) tab.
214
- expect((tabsList.element as HTMLElement).style.getPropertyValue("--_x-hovered")).toBe("0px");
215
- });
216
-
217
- it("clears a pending settle timeout when hovering a second tab before the first settles", async () => {
218
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
219
- const triggers = wrapper.findAll(".tabs-list-item");
220
- const clearTimeoutSpy = vi.spyOn(global, "clearTimeout");
221
-
222
- await triggers[1]!.trigger("mouseenter");
223
- await triggers[2]!.trigger("mouseenter");
224
-
225
- expect(clearTimeoutSpy).toHaveBeenCalled();
226
- });
227
-
228
- // ─── Regression: more than 3 tabs ────────────────────────────────────────
229
- // The original hand-rolled version of this component had an indicator-positioning issue
230
- // that only showed up with itemCount > 3 — these lock in correct behaviour at 5.
231
-
232
- it("activates a distant tab (index 0 -> index 4) directly via click", async () => {
233
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 5 }, slots: fiveSlots });
234
- const triggers = wrapper.findAll(".tabs-list-item");
235
-
236
- await triggers[4]!.trigger("click");
237
-
238
- expect(triggers[0]!.attributes("aria-selected")).toBe("false");
239
- expect(triggers[4]!.attributes("aria-selected")).toBe("true");
240
- expect(triggers[4]!.attributes("tabindex")).toBe("0");
241
- expect(wrapper.findAll(".tab-content")[4]!.attributes("aria-hidden")).toBe("false");
242
- });
243
-
244
- it("does not recolour tabs spanned by a distant activation jump (regression: intermediate labels going invisible)", async () => {
245
- // The old implementation force-applied a "transitioning" class to every tab spanned by a
246
- // jump, forcing it to the active-indicator's text colour for the whole transition — even
247
- // though the sliding indicator only visually reaches each spanned tab partway through, and
248
- // for a forward jump doesn't reach the middle tabs until late in the transition. That made
249
- // intermediate labels flash a colour matching neither their own background nor (yet) the
250
- // indicator's, going invisible. Spanned tabs should only ever carry aria-selected/hover
251
- // state of their own — never a class forcing them to look active.
252
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 5 }, slots: fiveSlots });
253
- const triggers = wrapper.findAll(".tabs-list-item");
254
-
255
- // Active tab starts at index 0, so activating index 4 spans every tab in between.
256
- await triggers[4]!.trigger("click");
257
-
258
- [1, 2, 3].forEach((index) => {
259
- expect(triggers[index]!.attributes("aria-selected")).toBe("false");
260
- expect(triggers[index]!.classes()).not.toContain("transitioning");
261
- });
262
- });
263
-
264
- it("wraps correctly with End then ArrowRight across 5 tabs", async () => {
265
- const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 5 }, slots: fiveSlots });
266
- const triggers = wrapper.findAll(".tabs-list-item");
267
-
268
- await triggers[0]!.trigger("keydown", { key: "End" });
269
- expect(triggers[4]!.attributes("aria-selected")).toBe("true");
270
-
271
- await triggers[4]!.trigger("keydown", { key: "ArrowRight" });
272
- expect(triggers[0]!.attributes("aria-selected")).toBe("true");
273
- });
274
- });
@@ -1,109 +0,0 @@
1
- # SiteNavigation — Consumer Styling Guide
2
-
3
- ## Public token API
4
-
5
- Defaults that read `var(--slate-01, currentColor)` use the library palette when it's loaded and fall
6
- back to the inherited text colour otherwise.
7
-
8
- ### Horizontal nav
9
-
10
- | Token | Default | Controls |
11
- |---|---|---|
12
- | `--site-nav-link-color` | `var(--slate-01, currentColor)` | Link colour |
13
- | `--site-nav-link-hover-color` | `var(--slate-04, <link colour>)` | Link colour on hover |
14
- | `--site-nav-link-active-color` | `var(--slate-01, <link colour>)` | Active link colour |
15
- | `--site-nav-link-accent` | `var(--slate-01, currentColor)` | Link accent colour |
16
- | `--site-nav-link-size` | `1.6rem` | Link font size |
17
- | `--site-nav-link-weight` | `400` | Link font weight |
18
- | `--site-nav-link-tracking` | `0.06em` | Link letter spacing |
19
- | `--site-nav-gap` | `2.2rem` | Gap between links |
20
- | `--site-nav-transition` | `250ms ease` | Link colour transition |
21
- | `--site-nav-decorator-indicator-color` | `var(--slate-01, currentColor)` | Sliding underline bar |
22
- | `--site-nav-decorator-hovered-bg` | `transparent` | Sliding pill behind the hovered link |
23
- | `--site-nav-decorator-active-bg` | `transparent` | Pill behind the active link |
24
-
25
- > Changed 2026-09-27: `--site-nav-decorator-hovered-bg` and `--site-nav-decorator-active-bg` are
26
- > new. The values were hardcoded `transparent` with no override, unlike the panel equivalents.
27
-
28
- ### Collapsed panel
29
-
30
- | Token | Default | Controls |
31
- |---|---|---|
32
- | `--site-nav-panel-bg` | `var(--page-bg, #1a1614)` | Panel background |
33
- | `--site-nav-panel-border-color` | `color-mix(in oklch, var(--slate-01, #c0847a) 35%, transparent)` | Top border when open |
34
- | `--site-nav-panel-item-border` | `color-mix(in oklch, var(--slate-01, white) 8%, transparent)` | Divider between panel links |
35
- | `--site-nav-panel-link-color` | `var(--slate-01, currentColor)` | Panel link colour |
36
- | `--site-nav-panel-link-hover-color` | `var(--slate-04, <panel link colour>)` | Panel link hover colour |
37
- | `--site-nav-panel-link-active-color` | `var(--slate-01, <panel link colour>)` | Active panel link colour |
38
- | `--site-nav-panel-padding-block` | `1.4rem` | Panel link vertical padding |
39
- | `--site-nav-panel-padding-inline` | `1.5rem` | Panel link horizontal padding |
40
- | `--site-nav-panel-slide-duration` | `350ms` | Open/close slide duration |
41
- | `--site-nav-panel-slide-easing` | `cubic-bezier(0.4, 0, 0.2, 1)` | Open/close easing |
42
- | `--site-nav-panel-decorator-indicator-color` | `var(--slate-01, currentColor)` | Panel indicator bar |
43
- | `--site-nav-panel-decorator-hovered-bg` | `transparent` | Pill behind the hovered panel link |
44
- | `--site-nav-panel-decorator-active-bg` | `transparent` | Pill behind the active panel link |
45
- | `--site-nav-panel-indicator-left` | `0` | Indicator bar left position |
46
- | `--site-nav-panel-indicator-right` | `auto` | Indicator bar right position |
47
-
48
- ### Burger button
49
-
50
- | Token | Default | Controls |
51
- |---|---|---|
52
- | `--site-nav-burger-color` | `var(--slate-01, currentColor)` | Bar colour |
53
- | `--site-nav-burger-width` | `22px` | Bar width |
54
- | `--site-nav-burger-height` | `1.5px` | Bar thickness |
55
- | `--site-nav-burger-gap` | `5px` | Gap between bars |
56
- | `--site-nav-burger-transition` | `300ms ease` | Open/close morph |
57
-
58
- ### Backdrop (teleported to `<body>`)
59
-
60
- | Token | Default | Controls |
61
- |---|---|---|
62
- | `--site-nav-backdrop-bg` | `oklch(0% 0 0 / 55%)` | Scrim colour |
63
- | `--site-nav-backdrop-blur` | `3px` | Blur behind the scrim |
64
- | `--site-nav-backdrop-duration` | `350ms` | Fade duration |
65
- | `--site-nav-backdrop-z-index` | `10` | Scrim stacking order; keep your header above it |
66
-
67
- > Changed 2026-09-27: `--site-nav-backdrop-z-index` is new (was a hardcoded `10`).
68
-
69
- The `--_*` variables declared on `.site-navigation` are the resolved copies of the tokens above
70
- that are read in more than one place: not public API.
71
-
72
- ---
73
-
74
- ## State hooks
75
-
76
- | Hook | Element | When |
77
- |---|---|---|
78
- | `.site-navigation--left`, `--center`, `--right` | root `<nav>` | `nav-align` |
79
- | `.is-collapsed` | root | Links don't fit, so the burger and panel are shown |
80
- | `.is-loaded` | root | First measurement done (the nav is `opacity: 0` until then) |
81
- | `.menu-open` | root | Panel open |
82
- | `.is-animated` | root | Indicator transitions enabled (after first layout) |
83
- | `li.is-active` | nav and panel items | Item matches the current route |
84
- | `.is-open` | `.site-nav-burger`, `.site-nav-panel`, `.site-nav-backdrop` | Panel open |
85
-
86
- Inner classes: `.site-nav-list`, `.site-nav-link`, `.site-nav-burger`, `.burger-bar`,
87
- `.site-nav-panel`, `.site-nav-panel-inner`, `.site-nav-panel-list`, `.site-nav-panel-link`,
88
- `.site-nav-backdrop`; decorators `.nav__hovered`, `.nav__active`, `.nav__active-indicator`.
89
- Each item `li` also gets its `NavItem.cssName`.
90
-
91
- ---
92
-
93
- ## Local overrides
94
-
95
- Set the tokens above on an element you own (a page or section class, or a class added with
96
- `:style-class-passthrough`): they inherit down into the component. Keep the block **unlayered** (no
97
- `@layer` wrapper) so it beats the library's `@layer components`. If your own file uses
98
- `<style scoped>`, tokens set on your element still work, but selectors that reach inside the component need
99
- `:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
100
-
101
- **Caveat:** the backdrop is teleported to `<body>`, outside your header, so the
102
- `--site-nav-backdrop-*` tokens only land when set on `:root`, `html` or `body`.
103
-
104
- ---
105
-
106
- ## Class passthrough
107
-
108
- `style-class-passthrough` adds classes to the root `<nav>`. Reactive after mount. It doesn't reach
109
- the teleported backdrop.