srcdev-nuxt-components 9.6.0 → 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/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 -10
- 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/responsive-header.md +11 -1
- package/.claude/skills/components/select-menu.md +54 -5
- package/.claude/skills/components/site-header.md +2 -2
- package/.claude/skills/components/tabbed-content.md +300 -0
- package/.claude/skills/composable-anchor-scroll.md +16 -15
- package/.claude/skills/index.md +4 -4
- 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 +6 -0
- package/.vscode/srcdev-component-select-menu.code-snippets +28 -0
- 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 +75 -9
- package/app/components/01.atoms/display-pill/DisplayPill.vue +57 -24
- 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/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/responsive-header/CONSUMER-STYLING.md +38 -11
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +27 -12
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +35 -20
- package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +1 -1
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +1 -1
- package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +1 -1
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +3 -3
- 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/tab-navigation.md +0 -288
- package/.claude/skills/components/tabs-core.md +0 -187
- 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/tab-navigation/CONSUMER-STYLING.md +0 -191
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +0 -550
- package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +0 -259
- package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +0 -379
- 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
package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
import { ref, reactive, computed } from "vue";
|
|
2
|
-
import type { Meta, StoryFn, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
-
import TabNavigationComponent from "../TabNavigation.vue";
|
|
4
|
-
import CanvasSwitcher from "../../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
5
|
-
import type { NavItemData } from "~/types/components/nav-item.d";
|
|
6
|
-
import type { MediaCanvas } from "~/types/components";
|
|
7
|
-
|
|
8
|
-
const meta: Meta<typeof TabNavigationComponent> = {
|
|
9
|
-
title: "Molecules/TabNavigation",
|
|
10
|
-
component: TabNavigationComponent,
|
|
11
|
-
parameters: {
|
|
12
|
-
docs: {
|
|
13
|
-
description: {
|
|
14
|
-
component:
|
|
15
|
-
"**Deprecated: use ResponsiveHeader.** It handles the same route and `#anchor` links (with " +
|
|
16
|
-
"`anchorScrollOffset`) and collapses item by item rather than all-or-nothing. See \"Migrating from " +
|
|
17
|
-
"TabNavigation\" in the ResponsiveHeader skill doc. TabNavigation stays until existing consumers have moved.",
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
argTypes: {
|
|
22
|
-
navAlign: {
|
|
23
|
-
control: { type: "select" },
|
|
24
|
-
options: ["left", "center", "right"],
|
|
25
|
-
description: "Horizontal alignment of the nav list",
|
|
26
|
-
},
|
|
27
|
-
ariaLabel: { control: "text", description: "aria-label on the nav landmark; override for localisation" },
|
|
28
|
-
openMenuLabel: { control: "text", description: "Burger button label while the menu is closed" },
|
|
29
|
-
closeMenuLabel: { control: "text", description: "Burger button label while the menu is open" },
|
|
30
|
-
navItemData: { table: { disable: true } },
|
|
31
|
-
styleClassPassthrough: { table: { disable: true } },
|
|
32
|
-
},
|
|
33
|
-
args: {
|
|
34
|
-
navAlign: "left",
|
|
35
|
-
ariaLabel: "Site navigation",
|
|
36
|
-
openMenuLabel: "Open navigation menu",
|
|
37
|
-
closeMenuLabel: "Close navigation menu",
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export default meta;
|
|
42
|
-
|
|
43
|
-
const navItemData: NavItemData = {
|
|
44
|
-
main: [
|
|
45
|
-
{ text: "Home", href: "#" },
|
|
46
|
-
{ text: "About", href: "#" },
|
|
47
|
-
{ text: "Services", href: "#" },
|
|
48
|
-
{ text: "Work", href: "#" },
|
|
49
|
-
{ text: "Contact", href: "#" },
|
|
50
|
-
],
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// Playground: every control starts unset, so the nav shows the component's own (light) defaults.
|
|
54
|
-
// Picking a colour sets that token as an override; Reset removes it again.
|
|
55
|
-
const tokenControls = [
|
|
56
|
-
{ group: "Indicator", label: "Indicator colour", token: "--tab-nav-decorator-indicator-color" },
|
|
57
|
-
{ group: "Horizontal nav links", label: "Link colour", token: "--tab-nav-link-color" },
|
|
58
|
-
{ group: "Horizontal nav links", label: "Hover colour", token: "--tab-nav-link-hover-color" },
|
|
59
|
-
{ group: "Horizontal nav links", label: "Active colour", token: "--tab-nav-link-active-color" },
|
|
60
|
-
{ group: "Horizontal nav links", label: "Focus ring colour", token: "--tab-nav-focus-ring-colour" },
|
|
61
|
-
{ group: "Mobile panel", label: "Panel background", token: "--tab-nav-panel-bg" },
|
|
62
|
-
{ group: "Mobile panel", label: "Panel link colour", token: "--tab-nav-panel-link-color" },
|
|
63
|
-
{ group: "Burger", label: "Burger colour", token: "--tab-nav-burger-color" },
|
|
64
|
-
] as const;
|
|
65
|
-
|
|
66
|
-
type Token = (typeof tokenControls)[number]["token"];
|
|
67
|
-
|
|
68
|
-
const DefaultTemplate: StoryFn<typeof TabNavigationComponent> = (args) => ({
|
|
69
|
-
components: { TabNavigationComponent },
|
|
70
|
-
setup() {
|
|
71
|
-
const overrides = reactive<Partial<Record<Token, string>>>({});
|
|
72
|
-
const groups = [...new Set(tokenControls.map((control) => control.group))].map((group) => ({
|
|
73
|
-
group,
|
|
74
|
-
controls: tokenControls.filter((control) => control.group === group),
|
|
75
|
-
}));
|
|
76
|
-
|
|
77
|
-
const setOverride = (token: Token, event: Event) => {
|
|
78
|
-
overrides[token] = (event.target as HTMLInputElement).value;
|
|
79
|
-
};
|
|
80
|
-
const resetOverride = (token: Token) => {
|
|
81
|
-
overrides[token] = undefined;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const activeOverrides = computed(() => Object.entries(overrides).filter(([, value]) => value));
|
|
85
|
-
|
|
86
|
-
const navStyle = computed(() => Object.fromEntries(activeOverrides.value));
|
|
87
|
-
|
|
88
|
-
const cssSnippet = computed(() => {
|
|
89
|
-
const lines = activeOverrides.value.map(([token, value]) => ` ${token}: ${value};`);
|
|
90
|
-
return lines.length
|
|
91
|
-
? `.your-selector {\n${lines.join("\n")}\n}`
|
|
92
|
-
: "/* No overrides: the nav is using its default (light theme) colours. */";
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
const copied = ref(false);
|
|
96
|
-
const copySnippet = async () => {
|
|
97
|
-
await navigator.clipboard.writeText(cssSnippet.value);
|
|
98
|
-
copied.value = true;
|
|
99
|
-
setTimeout(() => {
|
|
100
|
-
copied.value = false;
|
|
101
|
-
}, 2000);
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
return { args, navItemData, groups, overrides, setOverride, resetOverride, navStyle, cssSnippet, copied, copySnippet };
|
|
105
|
-
},
|
|
106
|
-
template: `
|
|
107
|
-
<div class="sb-tabnav-story">
|
|
108
|
-
|
|
109
|
-
<div class="sb-tabnav-note">
|
|
110
|
-
Resize the browser window to see the navigation collapse into a burger menu.
|
|
111
|
-
The active indicator uses CSS <code>anchor-name</code> / <code>position-anchor</code> —
|
|
112
|
-
no JavaScript required. Unsupported browsers fall back to link colour changes only.
|
|
113
|
-
The colours below start at the component defaults (light theme); dark mode is the consumer app's
|
|
114
|
-
call, e.g. with <code>light-dark()</code> in its own token overrides.
|
|
115
|
-
</div>
|
|
116
|
-
|
|
117
|
-
<div class="sb-tabnav-header" :style="navStyle">
|
|
118
|
-
<div class="sb-tabnav-logo">LOGO</div>
|
|
119
|
-
<TabNavigationComponent v-bind="args" :nav-item-data="navItemData" />
|
|
120
|
-
</div>
|
|
121
|
-
|
|
122
|
-
<div class="sb-tabnav-playground">
|
|
123
|
-
<fieldset v-for="{ group, controls } in groups" :key="group">
|
|
124
|
-
<legend>{{ group }}</legend>
|
|
125
|
-
<div v-for="control in controls" :key="control.token" class="sb-control-row">
|
|
126
|
-
<label :for="control.token">{{ control.label }}</label>
|
|
127
|
-
<input
|
|
128
|
-
:id="control.token"
|
|
129
|
-
type="color"
|
|
130
|
-
:value="overrides[control.token] ?? '#000000'"
|
|
131
|
-
@input="setOverride(control.token, $event)"
|
|
132
|
-
/>
|
|
133
|
-
<button
|
|
134
|
-
v-if="overrides[control.token]"
|
|
135
|
-
type="button"
|
|
136
|
-
class="sb-reset-btn"
|
|
137
|
-
@click="resetOverride(control.token)"
|
|
138
|
-
>
|
|
139
|
-
Reset
|
|
140
|
-
</button>
|
|
141
|
-
<span v-else class="sb-default-badge">default</span>
|
|
142
|
-
</div>
|
|
143
|
-
</fieldset>
|
|
144
|
-
</div>
|
|
145
|
-
|
|
146
|
-
<div class="sb-css-snippet">
|
|
147
|
-
<div class="sb-css-snippet-header">
|
|
148
|
-
<strong>CSS Token Snippet</strong>
|
|
149
|
-
<button class="sb-copy-btn" type="button" @click="copySnippet">{{ copied ? 'Copied!' : 'Copy' }}</button>
|
|
150
|
-
</div>
|
|
151
|
-
<pre class="sb-css-snippet-code">{{ cssSnippet }}</pre>
|
|
152
|
-
</div>
|
|
153
|
-
|
|
154
|
-
<component is="style">
|
|
155
|
-
.sb-tabnav-story { font-size: 1.4rem; display: grid; gap: 2.4rem; padding: 2.4rem; }
|
|
156
|
-
.sb-tabnav-note { font-size: 1.3rem; opacity: 0.7; font-style: italic; }
|
|
157
|
-
.sb-tabnav-note code { font-family: monospace; background: rgba(128,128,128,0.15); padding: 0.1em 0.4em; border-radius: 0.2rem; }
|
|
158
|
-
.sb-tabnav-header {
|
|
159
|
-
display: flex; align-items: center; gap: 3.2rem; padding: 1.2rem 2.4rem;
|
|
160
|
-
position: relative; min-height: 6rem; border-radius: 0.4rem;
|
|
161
|
-
border: 1px solid rgba(128,128,128,0.3);
|
|
162
|
-
}
|
|
163
|
-
.sb-tabnav-logo { font-size: 1.8rem; font-weight: 700; letter-spacing: 0.1em; flex-shrink: 0; opacity: 0.5; }
|
|
164
|
-
.sb-tabnav-header .tab-navigation { flex: 1; min-width: 0; }
|
|
165
|
-
.sb-tabnav-playground { display: grid; grid-template-columns: repeat(auto-fit, minmax(25rem, 1fr)); gap: 1.6rem; }
|
|
166
|
-
.sb-tabnav-playground fieldset { border: 1px solid #ccc; border-radius: 0.4rem; padding: 1.6rem; }
|
|
167
|
-
.sb-tabnav-playground legend { font-weight: bold; padding: 0 0.8rem; font-size: 1.4rem; }
|
|
168
|
-
.sb-control-row { display: grid; grid-template-columns: 1fr 8rem 6rem; gap: 1rem; align-items: center; padding: 0.5rem 0; font-size: 1.3rem; }
|
|
169
|
-
.sb-control-row input[type="color"] { height: 3.2rem; width: 100%; padding: 0.2rem; border: 1px solid #ccc; border-radius: 0.2rem; cursor: pointer; background: transparent; }
|
|
170
|
-
.sb-default-badge { font-size: 1.1rem; opacity: 0.6; text-align: center; }
|
|
171
|
-
.sb-css-snippet { border: 1px solid #ccc; border-radius: 0.4rem; overflow: hidden; }
|
|
172
|
-
.sb-css-snippet-header { display: flex; justify-content: space-between; align-items: center; padding: 1rem 1.6rem; border-bottom: 1px solid #ccc; font-size: 1.4rem; }
|
|
173
|
-
.sb-css-snippet-code { margin: 0; padding: 1.6rem; font-family: monospace; font-size: 1.3rem; line-height: 1.6; overflow-x: auto; white-space: pre; }
|
|
174
|
-
.sb-copy-btn, .sb-reset-btn { padding: 0.4rem 1.2rem; border: 1px solid #ccc; border-radius: 0.2rem; cursor: pointer; background: transparent; font-size: 1.3rem; color: inherit; }
|
|
175
|
-
.sb-copy-btn:hover, .sb-reset-btn:hover { background: #e0e0e0; }
|
|
176
|
-
</component>
|
|
177
|
-
</div>
|
|
178
|
-
`,
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
export const Default = DefaultTemplate.bind({});
|
|
182
|
-
Default.args = { navAlign: "left" };
|
|
183
|
-
|
|
184
|
-
// ─── Stress test ──────────────────────────────────────────────────────────────
|
|
185
|
-
|
|
186
|
-
const longGerman = "Haarverlängerungsbehandlungsberatungsterminvereinbarung";
|
|
187
|
-
|
|
188
|
-
const stressNavItemData: NavItemData = {
|
|
189
|
-
main: [
|
|
190
|
-
{ text: "Startseite", href: "#start" },
|
|
191
|
-
{ text: `${longGerman} und mehr`, href: "#lang" },
|
|
192
|
-
{ text: "", href: "#blank" },
|
|
193
|
-
{ text: " ", href: "#spaces" },
|
|
194
|
-
{ text: "😀 Emoji first", href: "#emoji", iconName: "this-icon:does-not-exist" },
|
|
195
|
-
{ text: "<b>not bold</b>", href: "#html" },
|
|
196
|
-
{ text: "تسريحات شعر جديدة", href: "#rtl" },
|
|
197
|
-
{ text: "https://example.com/a/very/long/url/that/never/breaks/at/all", href: "https://example.com", isExternal: true },
|
|
198
|
-
{ text: "Same href one", href: "#same" },
|
|
199
|
-
{ text: "Same href two", href: "#same" },
|
|
200
|
-
{ text: "No href" },
|
|
201
|
-
{ text: "A", href: "#a" },
|
|
202
|
-
],
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
export const StressTest: StoryObj<typeof TabNavigationComponent> = {
|
|
206
|
-
name: "Stress Test (Worst-Case Data)",
|
|
207
|
-
args: {
|
|
208
|
-
ariaLabel: "Hauptnavigation",
|
|
209
|
-
openMenuLabel: "Navigationsmenü öffnen",
|
|
210
|
-
closeMenuLabel: "Navigationsmenü schließen",
|
|
211
|
-
},
|
|
212
|
-
render: (args) => ({
|
|
213
|
-
components: { TabNavigationComponent },
|
|
214
|
-
setup() {
|
|
215
|
-
return { args, stressNavItemData };
|
|
216
|
-
},
|
|
217
|
-
template: `
|
|
218
|
-
<div
|
|
219
|
-
style="position: relative; display: flex; align-items: center; gap: 2.4rem; padding: 1.2rem 2.4rem; min-height: 6rem;"
|
|
220
|
-
>
|
|
221
|
-
<strong style="flex-shrink: 0;">LOGO</strong>
|
|
222
|
-
<TabNavigationComponent v-bind="args" :nav-item-data="stressNavItemData" style="flex: 1; min-inline-size: 0;" />
|
|
223
|
-
</div>
|
|
224
|
-
`,
|
|
225
|
-
}),
|
|
226
|
-
decorators: [
|
|
227
|
-
(story, context) => ({
|
|
228
|
-
components: { story, CanvasSwitcher },
|
|
229
|
-
setup() {
|
|
230
|
-
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
231
|
-
return { canvasName };
|
|
232
|
-
},
|
|
233
|
-
template: `
|
|
234
|
-
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
235
|
-
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
236
|
-
</div>
|
|
237
|
-
<div :class="canvasName" style="margin-inline: auto; padding-block-end: 40rem; outline: 1px dashed currentColor;">
|
|
238
|
-
<story />
|
|
239
|
-
</div>
|
|
240
|
-
`,
|
|
241
|
-
}),
|
|
242
|
-
],
|
|
243
|
-
parameters: {
|
|
244
|
-
layout: "fullscreen",
|
|
245
|
-
initialCanvas: "fullWidthCanvas",
|
|
246
|
-
docs: {
|
|
247
|
-
description: {
|
|
248
|
-
story:
|
|
249
|
-
"Deliberately hostile data to find breakage: twelve items including a long German compound word, an unbroken " +
|
|
250
|
-
"URL, emoji (with a broken icon name), HTML-like text (must render as text), right-to-left Arabic, a single " +
|
|
251
|
-
"character, two items sharing an href, one with no href, and two blank labels (skipped: a link with no text " +
|
|
252
|
-
"has no accessible name), plus German landmark and burger labels." +
|
|
253
|
-
" Check at every canvas width: the bar " +
|
|
254
|
-
"collapses to the burger as soon as the items don\x27t fit rather than overflowing; in the open panel every " +
|
|
255
|
-
"label wraps inside the panel; Tab shows a focus ring on each link; and the burger\x27s label switches language.",
|
|
256
|
-
},
|
|
257
|
-
},
|
|
258
|
-
},
|
|
259
|
-
};
|
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
-
import { nextTick } from "vue";
|
|
4
|
-
import TabNavigation from "../TabNavigation.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("TabNavigation", () => {
|
|
33
|
-
it("mounts without error", async () => {
|
|
34
|
-
const wrapper = await mountSuspended(TabNavigation, {
|
|
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(TabNavigation, {
|
|
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(TabNavigation, {
|
|
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(TabNavigation, {
|
|
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(TabNavigation, {
|
|
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(TabNavigation, {
|
|
75
|
-
props: { navItemData: defaultNavItemData },
|
|
76
|
-
});
|
|
77
|
-
expect(wrapper.find(".tab-nav-list").exists()).toBe(true);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("renders the correct number of nav items", async () => {
|
|
81
|
-
const wrapper = await mountSuspended(TabNavigation, {
|
|
82
|
-
props: { navItemData: defaultNavItemData },
|
|
83
|
-
});
|
|
84
|
-
const items = wrapper.findAll(".tab-nav-list li:not(.nav-indicator-li)");
|
|
85
|
-
expect(items.length).toBe(3);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("renders item text correctly", async () => {
|
|
89
|
-
const wrapper = await mountSuspended(TabNavigation, {
|
|
90
|
-
props: { navItemData: defaultNavItemData },
|
|
91
|
-
});
|
|
92
|
-
const links = wrapper.findAll(".tab-nav-link");
|
|
93
|
-
expect(links[0]!.text()).toContain("Home");
|
|
94
|
-
expect(links[1]!.text()).toContain("About");
|
|
95
|
-
expect(links[2]!.text()).toContain("Contact");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("renders item href correctly", async () => {
|
|
99
|
-
const wrapper = await mountSuspended(TabNavigation, {
|
|
100
|
-
props: { navItemData: defaultNavItemData },
|
|
101
|
-
});
|
|
102
|
-
const links = wrapper.findAll(".tab-nav-link");
|
|
103
|
-
expect(links[0]!.attributes("href")).toBe("/");
|
|
104
|
-
expect(links[1]!.attributes("href")).toBe("/about");
|
|
105
|
-
expect(links[2]!.attributes("href")).toBe("/contact");
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("applies cssName as class on the li element", async () => {
|
|
109
|
-
const navItemData: NavItemData = {
|
|
110
|
-
main: [{ text: "Home", href: "/", cssName: "is-active" }],
|
|
111
|
-
};
|
|
112
|
-
const wrapper = await mountSuspended(TabNavigation, {
|
|
113
|
-
props: { navItemData },
|
|
114
|
-
});
|
|
115
|
-
expect(wrapper.find(".tab-nav-list li").classes()).toContain("is-active");
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("renders an icon when iconName is provided", async () => {
|
|
119
|
-
const navItemData: NavItemData = {
|
|
120
|
-
main: [{ text: "Home", href: "/", iconName: "heroicons:home" }],
|
|
121
|
-
};
|
|
122
|
-
const wrapper = await mountSuspended(TabNavigation, {
|
|
123
|
-
props: { navItemData },
|
|
124
|
-
});
|
|
125
|
-
const firstItem = wrapper.find(".tab-nav-list li:not(.nav-indicator-li)");
|
|
126
|
-
expect(firstItem.find("[aria-hidden='true']").exists()).toBe(true);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("does not render an icon when iconName is not provided", async () => {
|
|
130
|
-
const wrapper = await mountSuspended(TabNavigation, {
|
|
131
|
-
props: { navItemData: defaultNavItemData },
|
|
132
|
-
});
|
|
133
|
-
const firstLink = wrapper.find(".tab-nav-link");
|
|
134
|
-
expect(firstLink.find("[aria-hidden='true']").exists()).toBe(false);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("renders external links with the correct href", async () => {
|
|
138
|
-
const navItemData: NavItemData = {
|
|
139
|
-
main: [{ text: "External", href: "https://example.com", isExternal: true }],
|
|
140
|
-
};
|
|
141
|
-
const wrapper = await mountSuspended(TabNavigation, {
|
|
142
|
-
props: { navItemData },
|
|
143
|
-
});
|
|
144
|
-
expect(wrapper.find(".tab-nav-link").attributes("href")).toBe("https://example.com");
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("renders two indicator elements inside the nav list", async () => {
|
|
148
|
-
const wrapper = await mountSuspended(TabNavigation, {
|
|
149
|
-
props: { navItemData: defaultNavItemData },
|
|
150
|
-
});
|
|
151
|
-
const indicators = wrapper.findAll(".tab-nav-list .nav-indicator-li");
|
|
152
|
-
expect(indicators.length).toBe(2);
|
|
153
|
-
expect(wrapper.find(".nav__hovered").exists()).toBe(true);
|
|
154
|
-
expect(wrapper.find(".nav__active-indicator").exists()).toBe(true);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// ─── Alignment variants ────────────────────────────────────────────────────
|
|
158
|
-
|
|
159
|
-
it("applies tab-navigation--left class by default", async () => {
|
|
160
|
-
const wrapper = await mountSuspended(TabNavigation, {
|
|
161
|
-
props: { navItemData: defaultNavItemData },
|
|
162
|
-
});
|
|
163
|
-
expect(wrapper.classes()).toContain("tab-navigation--left");
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("applies tab-navigation--center class when navAlign is center", async () => {
|
|
167
|
-
const wrapper = await mountSuspended(TabNavigation, {
|
|
168
|
-
props: { navItemData: defaultNavItemData, navAlign: "center" },
|
|
169
|
-
});
|
|
170
|
-
expect(wrapper.classes()).toContain("tab-navigation--center");
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("applies tab-navigation--right class when navAlign is right", async () => {
|
|
174
|
-
const wrapper = await mountSuspended(TabNavigation, {
|
|
175
|
-
props: { navItemData: defaultNavItemData, navAlign: "right" },
|
|
176
|
-
});
|
|
177
|
-
expect(wrapper.classes()).toContain("tab-navigation--right");
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// ─── styleClassPassthrough ─────────────────────────────────────────────────
|
|
181
|
-
|
|
182
|
-
it("applies styleClassPassthrough classes to the nav element", async () => {
|
|
183
|
-
const wrapper = await mountSuspended(TabNavigation, {
|
|
184
|
-
props: {
|
|
185
|
-
navItemData: defaultNavItemData,
|
|
186
|
-
styleClassPassthrough: ["custom-nav", "theme-dark"],
|
|
187
|
-
},
|
|
188
|
-
});
|
|
189
|
-
expect(wrapper.classes()).toContain("custom-nav");
|
|
190
|
-
expect(wrapper.classes()).toContain("theme-dark");
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("updates classes when styleClassPassthrough prop changes", async () => {
|
|
194
|
-
const wrapper = await mountSuspended(TabNavigation, {
|
|
195
|
-
props: {
|
|
196
|
-
navItemData: defaultNavItemData,
|
|
197
|
-
styleClassPassthrough: ["initial-class"],
|
|
198
|
-
},
|
|
199
|
-
});
|
|
200
|
-
expect(wrapper.classes()).toContain("initial-class");
|
|
201
|
-
|
|
202
|
-
await wrapper.setProps({ styleClassPassthrough: ["updated-class"] });
|
|
203
|
-
await nextTick();
|
|
204
|
-
|
|
205
|
-
expect(wrapper.classes()).toContain("updated-class");
|
|
206
|
-
expect(wrapper.classes()).not.toContain("initial-class");
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// ─── Non-collapsed state (default in JSDOM — dimensions are all 0) ─────────
|
|
210
|
-
|
|
211
|
-
it("does not show the burger button when not collapsed", async () => {
|
|
212
|
-
const wrapper = await mountSuspended(TabNavigation, {
|
|
213
|
-
props: { navItemData: defaultNavItemData },
|
|
214
|
-
});
|
|
215
|
-
expect(wrapper.find(".tab-nav-burger").exists()).toBe(false);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it("does not show the nav panel when not collapsed", async () => {
|
|
219
|
-
const wrapper = await mountSuspended(TabNavigation, {
|
|
220
|
-
props: { navItemData: defaultNavItemData },
|
|
221
|
-
});
|
|
222
|
-
expect(wrapper.find(".tab-nav-panel").exists()).toBe(false);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("does not have is-collapsed class when not collapsed", async () => {
|
|
226
|
-
const wrapper = await mountSuspended(TabNavigation, {
|
|
227
|
-
props: { navItemData: defaultNavItemData },
|
|
228
|
-
});
|
|
229
|
-
expect(wrapper.classes()).not.toContain("is-collapsed");
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// ─── Collapsed state ───────────────────────────────────────────────────────
|
|
233
|
-
// isCollapsed is driven by DOM layout measurements (scrollWidth > clientWidth).
|
|
234
|
-
// In JSDOM both values are 0 so we cannot trigger collapse via resize.
|
|
235
|
-
// Instead we force the state directly through the component's setup context.
|
|
236
|
-
|
|
237
|
-
describe("when collapsed", () => {
|
|
238
|
-
interface TabNavSetup {
|
|
239
|
-
isCollapsed: boolean;
|
|
240
|
-
isMenuOpen: boolean;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
async function mountCollapsed() {
|
|
244
|
-
const wrapper = await mountSuspended(TabNavigation, {
|
|
245
|
-
props: { navItemData: defaultNavItemData },
|
|
246
|
-
});
|
|
247
|
-
const setup = (wrapper.vm as unknown as { $: { setupState: TabNavSetup } }).$.setupState;
|
|
248
|
-
setup.isCollapsed = true;
|
|
249
|
-
await nextTick();
|
|
250
|
-
return { wrapper, setup };
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
it("applies is-collapsed class", async () => {
|
|
254
|
-
const { wrapper } = await mountCollapsed();
|
|
255
|
-
expect(wrapper.classes()).toContain("is-collapsed");
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it("hides the horizontal nav list", async () => {
|
|
259
|
-
const { wrapper } = await mountCollapsed();
|
|
260
|
-
expect(wrapper.find(".tab-nav-list").exists()).toBe(false);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it("shows the burger button", async () => {
|
|
264
|
-
const { wrapper } = await mountCollapsed();
|
|
265
|
-
expect(wrapper.find(".tab-nav-burger").exists()).toBe(true);
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it("shows the nav panel", async () => {
|
|
269
|
-
const { wrapper } = await mountCollapsed();
|
|
270
|
-
expect(wrapper.find(".tab-nav-panel").exists()).toBe(true);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it("burger button has aria-expanded false when menu is closed", async () => {
|
|
274
|
-
const { wrapper } = await mountCollapsed();
|
|
275
|
-
expect(wrapper.find(".tab-nav-burger").attributes("aria-expanded")).toBe("false");
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it("panel does not have is-open class when menu is closed", async () => {
|
|
279
|
-
const { wrapper } = await mountCollapsed();
|
|
280
|
-
expect(wrapper.find(".tab-nav-panel").classes()).not.toContain("is-open");
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it("renders panel links matching main nav items", async () => {
|
|
284
|
-
const { wrapper } = await mountCollapsed();
|
|
285
|
-
const panelLinks = wrapper.findAll(".tab-nav-panel-link");
|
|
286
|
-
expect(panelLinks.length).toBe(3);
|
|
287
|
-
expect(panelLinks[0]!.text()).toContain("Home");
|
|
288
|
-
expect(panelLinks[1]!.text()).toContain("About");
|
|
289
|
-
expect(panelLinks[2]!.text()).toContain("Contact");
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it("toggles isMenuOpen on burger click", async () => {
|
|
293
|
-
const { wrapper, setup } = await mountCollapsed();
|
|
294
|
-
expect(setup.isMenuOpen).toBe(false);
|
|
295
|
-
await wrapper.find(".tab-nav-burger").trigger("click");
|
|
296
|
-
expect(setup.isMenuOpen).toBe(true);
|
|
297
|
-
await wrapper.find(".tab-nav-burger").trigger("click");
|
|
298
|
-
expect(setup.isMenuOpen).toBe(false);
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it("applies menu-open class when menu is open", async () => {
|
|
302
|
-
const { wrapper } = await mountCollapsed();
|
|
303
|
-
await wrapper.find(".tab-nav-burger").trigger("click");
|
|
304
|
-
await nextTick();
|
|
305
|
-
expect(wrapper.classes()).toContain("menu-open");
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
it("burger has aria-expanded true when menu is open", async () => {
|
|
309
|
-
const { wrapper } = await mountCollapsed();
|
|
310
|
-
await wrapper.find(".tab-nav-burger").trigger("click");
|
|
311
|
-
await nextTick();
|
|
312
|
-
expect(wrapper.find(".tab-nav-burger").attributes("aria-expanded")).toBe("true");
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it("panel has is-open class when menu is open", async () => {
|
|
316
|
-
const { wrapper } = await mountCollapsed();
|
|
317
|
-
await wrapper.find(".tab-nav-burger").trigger("click");
|
|
318
|
-
await nextTick();
|
|
319
|
-
expect(wrapper.find(".tab-nav-panel").classes()).toContain("is-open");
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
it("panel is inert when menu is closed", async () => {
|
|
323
|
-
const { wrapper } = await mountCollapsed();
|
|
324
|
-
expect(wrapper.find(".tab-nav-panel").attributes("inert")).toBeDefined();
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it("panel is not inert when menu is open", async () => {
|
|
328
|
-
const { wrapper } = await mountCollapsed();
|
|
329
|
-
await wrapper.find(".tab-nav-burger").trigger("click");
|
|
330
|
-
await nextTick();
|
|
331
|
-
expect(wrapper.find(".tab-nav-panel").attributes("inert")).toBeUndefined();
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it("burger aria-controls points at the panel's own unique id", async () => {
|
|
335
|
-
const { wrapper } = await mountCollapsed();
|
|
336
|
-
const panelId = wrapper.find(".tab-nav-panel").attributes("id");
|
|
337
|
-
expect(panelId).toBeTruthy();
|
|
338
|
-
expect(panelId).not.toBe("tab-nav-panel");
|
|
339
|
-
expect(wrapper.find(".tab-nav-burger").attributes("aria-controls")).toBe(panelId);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it("uses openMenuLabel / closeMenuLabel for the burger", async () => {
|
|
343
|
-
const wrapper = await mountSuspended(TabNavigation, {
|
|
344
|
-
props: { navItemData: defaultNavItemData, openMenuLabel: "Menü öffnen", closeMenuLabel: "Menü schließen" },
|
|
345
|
-
});
|
|
346
|
-
const setup = (wrapper.vm as unknown as { $: { setupState: TabNavSetup } }).$.setupState;
|
|
347
|
-
setup.isCollapsed = true;
|
|
348
|
-
await nextTick();
|
|
349
|
-
expect(wrapper.find(".tab-nav-burger").text()).toContain("Menü öffnen");
|
|
350
|
-
await wrapper.find(".tab-nav-burger").trigger("click");
|
|
351
|
-
expect(wrapper.find(".tab-nav-burger").text()).toContain("Menü schließen");
|
|
352
|
-
});
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
// ─── Worst-case data ─────────────────────────────────────────────────────
|
|
356
|
-
|
|
357
|
-
it("skips items with blank text", async () => {
|
|
358
|
-
const wrapper = await mountSuspended(TabNavigation, {
|
|
359
|
-
props: {
|
|
360
|
-
navItemData: { main: [{ text: "Home", href: "/" }, { text: " ", href: "/blank" }, { text: "", href: "/empty" }] },
|
|
361
|
-
},
|
|
362
|
-
});
|
|
363
|
-
expect(wrapper.findAll(".tab-nav-link")).toHaveLength(1);
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
it("renders duplicate and missing hrefs without collapsing items", async () => {
|
|
367
|
-
const wrapper = await mountSuspended(TabNavigation, {
|
|
368
|
-
props: {
|
|
369
|
-
navItemData: { main: [{ text: "One", href: "/same" }, { text: "Two", href: "/same" }, { text: "Three" }, { text: "Four" }] },
|
|
370
|
-
},
|
|
371
|
-
});
|
|
372
|
-
expect(wrapper.findAll(".tab-nav-link")).toHaveLength(4);
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
it("renders no nav landmark when there are no items", async () => {
|
|
376
|
-
const wrapper = await mountSuspended(TabNavigation, { props: { navItemData: { main: [] } } });
|
|
377
|
-
expect(wrapper.find("nav").exists()).toBe(false);
|
|
378
|
-
});
|
|
379
|
-
});
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
-
|
|
3
|
-
exports[`TabNavigation > renders correct HTML structure 1`] = `
|
|
4
|
-
"<nav class="tab-navigation tab-navigation--left is-loaded is-animated" aria-label="Site navigation">
|
|
5
|
-
<ul class="tab-nav-list">
|
|
6
|
-
<li data-href="/" class="is-active">
|
|
7
|
-
<!-- Plain <a> for hash links — keeps Vue Router out of the smooth-scroll path --><a href="/" class="tab-nav-link" data-nav-item="">
|
|
8
|
-
<!--v-if--> Home
|
|
9
|
-
</a>
|
|
10
|
-
</li>
|
|
11
|
-
<li data-href="/about" class="">
|
|
12
|
-
<!-- Plain <a> for hash links — keeps Vue Router out of the smooth-scroll path --><a href="/about" class="tab-nav-link" data-nav-item="">
|
|
13
|
-
<!--v-if--> About
|
|
14
|
-
</a>
|
|
15
|
-
</li>
|
|
16
|
-
<li data-href="/contact" class="">
|
|
17
|
-
<!-- Plain <a> for hash links — keeps Vue Router out of the smooth-scroll path --><a href="/contact" class="tab-nav-link" data-nav-item="">
|
|
18
|
-
<!--v-if--> Contact
|
|
19
|
-
</a>
|
|
20
|
-
</li>
|
|
21
|
-
<li aria-hidden="true" role="none" class="nav-indicator-li">
|
|
22
|
-
<div class="nav__hovered"></div>
|
|
23
|
-
</li>
|
|
24
|
-
<li aria-hidden="true" role="none" class="nav-indicator-li">
|
|
25
|
-
<div class="nav__active-indicator"></div>
|
|
26
|
-
</li>
|
|
27
|
-
</ul>
|
|
28
|
-
<!--v-if-->
|
|
29
|
-
<!--teleport start-->
|
|
30
|
-
<!--teleport end-->
|
|
31
|
-
<!--v-if-->
|
|
32
|
-
</nav>"
|
|
33
|
-
`;
|