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.
- package/.claude/skills/component-compliance-checklist.md +8 -2
- package/.claude/skills/component-dynamic-slots.md +3 -3
- package/.claude/skills/component-export-types.md +10 -16
- package/.claude/skills/component-naming.md +1 -1
- package/.claude/skills/components/alert-content.md +9 -1
- package/.claude/skills/components/breadcrumb.md +5 -0
- package/.claude/skills/components/container-glow.md +21 -8
- package/.claude/skills/components/display-avatar.md +13 -7
- package/.claude/skills/components/display-chip.md +37 -24
- package/.claude/skills/components/display-pill.md +58 -8
- package/.claude/skills/components/display-prompt.md +8 -5
- package/.claude/skills/components/display-toast.md +21 -4
- package/.claude/skills/components/glowing-border.md +20 -2
- package/.claude/skills/components/input-button.md +3 -0
- package/.claude/skills/components/responsive-header.md +59 -0
- package/.claude/skills/components/select-menu.md +54 -5
- package/.claude/skills/components/service-detail.md +32 -0
- package/.claude/skills/components/service-summary.md +8 -0
- package/.claude/skills/components/services-card-grid.md +5 -3
- package/.claude/skills/components/services-card.md +22 -12
- package/.claude/skills/components/site-header.md +2 -2
- package/.claude/skills/components/slider-gallery.md +10 -4
- package/.claude/skills/components/tabbed-content.md +300 -0
- package/.claude/skills/composable-anchor-scroll.md +16 -15
- package/.claude/skills/index.md +4 -5
- package/.claude/skills/storybook-add-story.md +53 -0
- package/.claude/skills/theming-status-tokens.md +60 -0
- package/.vscode/srcdev-component-display-avatar.code-snippets +2 -1
- package/.vscode/srcdev-component-display-chip.code-snippets +3 -2
- package/.vscode/srcdev-component-display-pill.code-snippets +10 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +16 -0
- package/.vscode/srcdev-component-select-menu.code-snippets +28 -0
- package/.vscode/srcdev-component-service-detail.code-snippets +1 -0
- package/.vscode/srcdev-component-service-summary.code-snippets +3 -1
- package/.vscode/srcdev-component-services-card.code-snippets +38 -36
- package/.vscode/srcdev-component-services-grids.code-snippets +2 -1
- package/.vscode/{srcdev-component-tabs-core.code-snippets → srcdev-component-tabbed-content.code-snippets} +35 -16
- package/app/assets/styles/setup/03.theming/_status.css +24 -0
- package/app/assets/styles/setup/03.theming/index.css +1 -0
- package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +39 -35
- package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +25 -15
- package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +130 -24
- package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +45 -0
- package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +2 -2
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +76 -19
- package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +12 -10
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +201 -42
- package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +15 -4
- package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +1 -1
- package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +78 -9
- package/app/components/01.atoms/display-pill/DisplayPill.vue +59 -23
- package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +240 -16
- package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +43 -14
- package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +3 -3
- package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +16 -4
- package/app/components/01.atoms/navigation/breadcrumb/CONSUMER-STYLING.md +28 -5
- package/app/components/01.atoms/navigation/breadcrumb/stories/Breadcrumb.stories.ts +94 -0
- package/app/components/01.atoms/navigation/breadcrumb/tests/Breadcrumb.spec.ts +27 -0
- package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +201 -0
- package/app/components/01.atoms/tabbed-content/TabbedContent.vue +750 -0
- package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +279 -0
- package/app/components/01.atoms/tabbed-content/tests/TabbedContent.spec.ts +816 -0
- package/app/components/02.molecules/alert-content/AlertContent.vue +11 -2
- package/app/components/02.molecules/alert-content/AlertContentInner.vue +19 -0
- package/app/components/02.molecules/alert-content/CONSUMER-STYLING.md +17 -4
- package/app/components/02.molecules/alert-content/stories/AlertContent.stories.ts +200 -17
- package/app/components/02.molecules/alert-content/tests/AlertContent.spec.ts +33 -0
- package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +7 -5
- package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +6 -2
- package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +24 -3
- package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +19 -0
- package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +2 -1
- package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +52 -19
- package/app/components/02.molecules/display-chip/DisplayChip.vue +72 -54
- package/app/components/02.molecules/display-chip/stories/DisplayChip.stories.ts +103 -6
- package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +98 -29
- package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +8 -4
- package/app/components/02.molecules/prompt/CONSUMER-STYLING.md +9 -1
- package/app/components/02.molecules/prompt/DisplayPrompt.vue +2 -2
- package/app/components/02.molecules/prompt/stories/DisplayPrompt.stories.ts +208 -71
- package/app/components/02.molecules/prompt/tests/DisplayPrompt.spec.ts +19 -6
- package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +66 -8
- package/app/components/02.molecules/select-menu/SelectMenu.vue +141 -18
- package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +232 -2
- package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +151 -0
- package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +6 -2
- package/app/components/02.molecules/toast/CONSUMER-STYLING.md +91 -27
- package/app/components/02.molecules/toast/DisplayToast.vue +109 -50
- package/app/components/02.molecules/toast/DisplayToastProvider.vue +109 -45
- package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +23 -0
- package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +73 -0
- package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +132 -18
- package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +105 -39
- package/app/components/03.organisms/image-galleries/slider-gallery/CONSUMER-STYLING.md +19 -1
- package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +65 -7
- package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +201 -63
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +41 -2
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +6 -6
- package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +42 -11
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +60 -16
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +86 -24
- package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +52 -0
- package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +20 -0
- package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +40 -0
- package/app/components/03.organisms/services/service-detail/CONSUMER-STYLING.md +92 -41
- package/app/components/03.organisms/services/service-detail/ServiceDetail.vue +198 -73
- package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts +157 -9
- package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +58 -0
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +5 -4
- package/app/components/03.organisms/services/service-summary/CONSUMER-STYLING.md +30 -0
- package/app/components/03.organisms/services/service-summary/ServiceSummary.vue +32 -8
- package/app/components/03.organisms/services/service-summary/stories/ServiceSummary.stories.ts +147 -0
- package/app/components/03.organisms/services/service-summary/tests/ServiceSummary.spec.ts +26 -0
- package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +2 -2
- package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +104 -63
- package/app/components/03.organisms/services/services-card/ServicesCard.vue +92 -66
- package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +194 -27
- package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +34 -8
- package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +8 -8
- package/app/components/03.organisms/services/services-grids/CONSUMER-STYLING.md +9 -0
- package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +2 -1
- package/app/components/03.organisms/services/services-grids/stories/ServiceSummaryGrid.stories.ts +46 -6
- package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +89 -11
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +8 -0
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +6 -6
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +24 -24
- package/app/components/05.forms/input-button/CONSUMER-STYLING.md +8 -0
- package/app/components/05.forms/input-button/InputButton.vue +3 -1
- package/app/composables/tests/useAnchorScroll.spec.ts +8 -0
- package/app/composables/useAnchorScroll.ts +1 -1
- package/app/composables/useTabs.ts +148 -206
- package/app/types/components/display-chip.d.ts +8 -4
- package/app/types/components/display-toast.d.ts +2 -0
- package/app/types/components/index.ts +0 -1
- package/app/types/components/select-menu.d.ts +2 -0
- package/app/utils/pausableTimeout.ts +41 -0
- package/nuxt.config.ts +1 -1
- package/package.json +1 -1
- package/.claude/skills/components/site-navigation.md +0 -145
- package/.claude/skills/components/tab-navigation.md +0 -272
- package/.claude/skills/components/tabs-core.md +0 -187
- package/.vscode/srcdev-component-site-navigation.code-snippets +0 -38
- package/.vscode/srcdev-component-tab-navigation.code-snippets +0 -30
- package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +0 -120
- package/app/components/01.atoms/navigation/tabs/TabsCore.vue +0 -272
- package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +0 -199
- package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +0 -274
- package/app/components/02.molecules/navigation/site-navigation/CONSUMER-STYLING.md +0 -109
- package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +0 -751
- package/app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts +0 -356
- package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +0 -367
- package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +0 -30
- package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +0 -150
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +0 -518
- package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +0 -340
- package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +0 -339
- package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +0 -33
- package/app/composables/useNavCollapse.ts +0 -86
- 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
|
-
|