srcdev-nuxt-components 9.6.19 → 9.6.20
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/rules/consumer/ids-from-useid.md +3 -2
- package/.claude/skills/component-compliance-checklist.md +5 -0
- package/.claude/skills/components/auto-grid.md +3 -1
- package/.claude/skills/components/capture-qr-code.md +9 -5
- package/.claude/skills/components/carousel-flip.md +41 -30
- package/.claude/skills/components/column-flow-grid.md +2 -0
- package/.claude/skills/components/dashboard-quad-grid.md +7 -2
- package/.claude/skills/components/dashboard-stats-grid.md +4 -0
- package/.claude/skills/components/decode-qr-code.md +7 -2
- package/.claude/skills/components/deep-expanding-menu-classic.md +31 -30
- package/.claude/skills/components/deep-expanding-menu.md +22 -21
- package/.claude/skills/components/grid-stack.md +2 -0
- package/.claude/skills/components/layout-grid-by-cols.md +2 -0
- package/.claude/skills/components/masonry-grid.md +9 -1
- package/.claude/skills/components/page-row.md +3 -0
- package/.claude/skills/components/samaritan-prompt.md +24 -8
- package/.claude/skills/components/site-header.md +16 -2
- package/.claude/skills/index.md +12 -12
- package/.claude/skills/storybook-add-story.md +25 -0
- package/.claude/skills/testing-add-unit-test.md +26 -0
- package/.vscode/srcdev-component-capture-decode-qr-code.code-snippets +11 -2
- package/.vscode/srcdev-component-carousel-flip.code-snippets +22 -4
- package/.vscode/srcdev-component-deep-expanding-menu-classic.code-snippets +32 -2
- package/.vscode/srcdev-component-deep-expanding-menu.code-snippets +31 -1
- package/.vscode/srcdev-component-samaritan-prompt.code-snippets +5 -4
- package/.vscode/srcdev-component-site-header.code-snippets +13 -1
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +12 -2
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue +19 -17
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts +42 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/tests/DashboardQuadGrid.spec.ts +31 -44
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +13 -1
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +40 -31
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/stories/DashboardStatsGrid.stories.ts +47 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/tests/DashboardStatsGrid.spec.ts +51 -53
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/CONSUMER-STYLING.md +3 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/LayoutGridByCols.vue +4 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/stories/LayoutGridByCols.stories.ts +47 -2
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/LayoutGridByCols.spec.ts +36 -23
- package/app/components/01.atoms/grids/auto-grid/AutoGrid.vue +7 -3
- package/app/components/01.atoms/grids/auto-grid/CONSUMER-STYLING.md +1 -1
- package/app/components/01.atoms/grids/auto-grid/stories/AutoGrid.stories.ts +46 -1
- package/app/components/01.atoms/grids/auto-grid/tests/AutoGrid.spec.ts +40 -27
- package/app/components/01.atoms/grids/auto-grid/tests/__snapshots__/AutoGrid.spec.ts.snap +1 -1
- package/app/components/01.atoms/grids/column-flow-grid/CONSUMER-STYLING.md +9 -0
- package/app/components/01.atoms/grids/column-flow-grid/ColumnFlowGrid.vue +3 -2
- package/app/components/01.atoms/grids/column-flow-grid/stories/ColumnFlowGrid.stories.ts +46 -0
- package/app/components/01.atoms/grids/column-flow-grid/tests/ColumnFlowGrid.spec.ts +24 -11
- package/app/components/01.atoms/grids/grid-stack/CONSUMER-STYLING.md +3 -0
- package/app/components/01.atoms/grids/grid-stack/GridStack.vue +2 -0
- package/app/components/01.atoms/grids/grid-stack/stories/GridStack.stories.ts +41 -0
- package/app/components/01.atoms/grids/grid-stack/tests/GridStack.spec.ts +26 -13
- package/app/components/01.atoms/grids/masonry-grid/CONSUMER-STYLING.md +15 -0
- package/app/components/01.atoms/grids/masonry-grid/MasonryGrid.vue +51 -31
- package/app/components/01.atoms/grids/masonry-grid/stories/MasonryGrid.stories.ts +48 -0
- package/app/components/01.atoms/grids/masonry-grid/tests/MasonryGrid.spec.ts +84 -29
- package/app/components/01.atoms/page-row/CONSUMER-STYLING.md +6 -1
- package/app/components/01.atoms/page-row/PageRow.vue +16 -15
- package/app/components/01.atoms/page-row/stories/PageRow.stories.ts +23 -0
- package/app/components/01.atoms/page-row/tests/PageRow.spec.ts +6 -5
- package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +63 -44
- package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +74 -37
- package/app/components/02.molecules/navigation/deep-expanding-menu/stories/DeepExpandingMenu.stories.ts +139 -19
- package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts +90 -27
- package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +8 -8
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +58 -26
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +113 -57
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/stories/DeepExpandingMenuClassic.stories.ts +139 -19
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/DeepExpandingMenuClassic.spec.ts +140 -22
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/__snapshots__/DeepExpandingMenuClassic.spec.ts.snap +9 -9
- package/app/components/02.molecules/qr-code/CONSUMER-STYLING.md +22 -7
- package/app/components/02.molecules/qr-code/CaptureQrCode.vue +63 -71
- package/app/components/02.molecules/qr-code/DecodeQrCode.vue +47 -10
- package/app/components/02.molecules/qr-code/stories/CaptureQrCode.stories.ts +114 -26
- package/app/components/02.molecules/qr-code/stories/DecodeQrCode.stories.ts +115 -24
- package/app/components/02.molecules/qr-code/tests/CaptureQrCode.spec.ts +124 -51
- package/app/components/02.molecules/qr-code/tests/DecodeQrCode.spec.ts +74 -25
- package/app/components/02.molecules/samaritan-prompt/CONSUMER-STYLING.md +22 -10
- package/app/components/02.molecules/samaritan-prompt/SamaritanPrompt.vue +30 -12
- package/app/components/02.molecules/samaritan-prompt/SamaritanPromptMixed.vue +34 -10
- package/app/components/02.molecules/samaritan-prompt/stories/SamaritanPrompt.stories.ts +55 -4
- package/app/components/02.molecules/samaritan-prompt/stories/SamaritanPromptMixed.stories.ts +67 -7
- package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPrompt.spec.ts +113 -29
- package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPromptMixed.spec.ts +92 -45
- package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +82 -29
- package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +176 -113
- package/app/components/03.organisms/image-galleries/carousel-flip/stories/CarouselFlip.stories.ts +153 -23
- package/app/components/03.organisms/image-galleries/carousel-flip/tests/CarouselFlip.spec.ts +119 -63
- package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +8 -8
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +4 -4
- package/app/components/03.organisms/site-header/CONSUMER-STYLING.md +41 -23
- package/app/components/03.organisms/site-header/SiteHeader.vue +16 -21
- package/app/components/03.organisms/site-header/stories/SiteHeader.stories.ts +138 -0
- package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +53 -26
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +2 -2
- package/app/types/components/qr-code.d.ts +12 -0
- package/app/utils/graphemes.ts +8 -0
- package/package.json +1 -1
- package/app/components/01.atoms/animations/entry/.nuxt/app.config.mjs +0 -21
- package/app/components/01.atoms/animations/entry/.nuxt/components.d.ts +0 -66
- package/app/components/01.atoms/animations/entry/.nuxt/imports.d.ts +0 -30
- package/app/components/01.atoms/animations/entry/.nuxt/manifest/meta/test.json +0 -1
- package/app/components/01.atoms/animations/entry/.nuxt/types/app.config.d.ts +0 -35
- package/app/components/01.atoms/animations/entry/.nuxt/types/build.d.ts +0 -23
- package/app/components/01.atoms/animations/entry/.nuxt/types/builder-env.d.ts +0 -1
- package/app/components/01.atoms/animations/entry/.nuxt/types/components.d.ts +0 -71
- package/app/components/01.atoms/animations/entry/.nuxt/types/imports.d.ts +0 -386
- package/app/components/01.atoms/animations/entry/.nuxt/types/middleware.d.ts +0 -11
- package/app/components/01.atoms/animations/entry/.nuxt/types/modules.d.ts +0 -79
- package/app/components/01.atoms/animations/entry/.nuxt/types/nitro-layouts.d.ts +0 -17
- package/app/components/01.atoms/animations/entry/.nuxt/types/nitro-nuxt.d.ts +0 -64
- package/app/components/01.atoms/animations/entry/.nuxt/types/plugins.d.ts +0 -29
- package/app/components/01.atoms/animations/entry/.nuxt/types/runtime-config.d.ts +0 -32
- package/app/components/01.atoms/animations/entry/.nuxt/types/shared-imports.d.ts +0 -10
- package/app/components/01.atoms/animations/entry/.nuxt/types/vue-shim.d.ts +0 -0
- package/app/components/01.atoms/animations/entry/.nuxtrc +0 -1
- package/app/components/01.atoms/animations/entry/node_modules/.cache/nuxt/chrome-workspace.json +0 -1
- package/app/components/01.atoms/animations/entry/node_modules/.cache/vite/client/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/app/components/03.organisms/responsive-header/tests/.nuxt/app.config.mjs +0 -21
- package/app/components/03.organisms/responsive-header/tests/.nuxt/components.d.ts +0 -66
- package/app/components/03.organisms/responsive-header/tests/.nuxt/imports.d.ts +0 -30
- package/app/components/03.organisms/responsive-header/tests/.nuxt/manifest/meta/test.json +0 -1
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/app.config.d.ts +0 -35
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/build.d.ts +0 -23
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/builder-env.d.ts +0 -1
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/components.d.ts +0 -71
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/imports.d.ts +0 -386
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/middleware.d.ts +0 -11
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/modules.d.ts +0 -79
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-layouts.d.ts +0 -17
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-nuxt.d.ts +0 -64
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/plugins.d.ts +0 -29
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/runtime-config.d.ts +0 -32
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/shared-imports.d.ts +0 -10
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/vue-shim.d.ts +0 -0
- package/app/components/03.organisms/responsive-header/tests/node_modules/.cache/nuxt/chrome-workspace.json +0 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
1
2
|
import DeepExpandingMenuClassic from "../DeepExpandingMenuClassic.vue";
|
|
3
|
+
import CanvasSwitcher from "../../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
2
4
|
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
-
import type { ResponsiveHeaderNavItem } from "~/types/components";
|
|
5
|
+
import type { MediaCanvas, ResponsiveHeaderNavItem } from "~/types/components";
|
|
4
6
|
|
|
5
7
|
const navLinks: ResponsiveHeaderNavItem[] = [
|
|
6
8
|
{ name: "Home", path: "/" },
|
|
@@ -16,63 +18,181 @@ const navLinks: ResponsiveHeaderNavItem[] = [
|
|
|
16
18
|
{ name: "Contact", path: "/contact" },
|
|
17
19
|
];
|
|
18
20
|
|
|
19
|
-
const
|
|
21
|
+
const stressNavLinks: ResponsiveHeaderNavItem[] = [
|
|
22
|
+
{ name: "H", path: "/" },
|
|
23
|
+
{
|
|
24
|
+
name: "Dienstleistungsangebotsübersichtsseite",
|
|
25
|
+
childLinksTitle:
|
|
26
|
+
"Unsere vollständige Übersicht aller Dienstleistungen, Behandlungen und Pflegeangebote für Haut und Haar in Northtown",
|
|
27
|
+
childLinks: [
|
|
28
|
+
{ name: "Haarschnitt", path: "/services/haircuts" },
|
|
29
|
+
{ name: "https://example.com/a/very/long/unbroken/path/that/never/wraps/on/its/own", path: "/long-url" },
|
|
30
|
+
{ name: "🎨 Colouring", path: "/services/colouring" },
|
|
31
|
+
{ name: "Styling", path: "/services/styling-1" },
|
|
32
|
+
{ name: "Styling", path: "/services/styling-2" },
|
|
33
|
+
{ name: "<b>Not bold</b>", path: "/services/html" },
|
|
34
|
+
{ name: "قص الشعر", path: "/services/rtl" },
|
|
35
|
+
...Array.from({ length: 24 }, (_, i) => ({ name: `Treatment ${i + 1}`, path: `/services/treatment-${i + 1}` })),
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
{ name: "Gruppe ohne Titel", childLinks: [{ name: "Only link", path: "/only" }] },
|
|
39
|
+
{ name: "Empty group", childLinksTitle: "Nothing here yet", childLinks: [] },
|
|
40
|
+
{ name: "Supercalifragilisticexpialidociousandthensomemoretext", path: "/unbroken" },
|
|
41
|
+
{ name: "🙂", path: "/emoji" },
|
|
42
|
+
{ name: "Contact", path: "/contact" },
|
|
43
|
+
{ name: "Contact", path: "/contact" },
|
|
44
|
+
{
|
|
45
|
+
name: "About us and our story",
|
|
46
|
+
childLinksTitle: "About Alex Morgan",
|
|
47
|
+
childLinks: [
|
|
48
|
+
{ name: "Team", path: "/about/team" },
|
|
49
|
+
{ name: "History", path: "/about/history" },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
type LineClamp = "none" | "1" | "2" | "3";
|
|
55
|
+
|
|
56
|
+
// Story-only arg: drives a CSS token, not a DeepExpandingMenuClassic prop. See the argTypes entry.
|
|
57
|
+
type StoryArgs = InstanceType<typeof DeepExpandingMenuClassic>["$props"] & {
|
|
58
|
+
headingLineClamp: LineClamp;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const meta: Meta<StoryArgs> = {
|
|
20
62
|
title: "Molecules/DeepExpandingMenuClassic",
|
|
21
63
|
component: DeepExpandingMenuClassic,
|
|
22
64
|
argTypes: {
|
|
65
|
+
headingLineClamp: {
|
|
66
|
+
control: "select",
|
|
67
|
+
options: ["none", "1", "2", "3"] satisfies LineClamp[],
|
|
68
|
+
description:
|
|
69
|
+
"**Story control, not a prop.** Sets the `--deep-expanding-menu-classic-panel-heading-line-clamp` CSS token on a wrapper so you can try it here. " +
|
|
70
|
+
"To use it in an app, set the token in your own CSS, e.g. `.site-nav { --deep-expanding-menu-classic-panel-heading-line-clamp: 1; }`. " +
|
|
71
|
+
"`1` is single-line ellipsis, `none` (the default) shows everything.",
|
|
72
|
+
table: { category: "CSS tokens (story only, set in your CSS)", defaultValue: { summary: "none" } },
|
|
73
|
+
},
|
|
23
74
|
tag: {
|
|
24
75
|
control: { type: "select" },
|
|
25
|
-
options: ["div", "section", "nav"
|
|
76
|
+
options: ["div", "section", "nav"],
|
|
26
77
|
description: "Root element tag",
|
|
78
|
+
table: { category: "Basic", defaultValue: { summary: "nav" } },
|
|
79
|
+
},
|
|
80
|
+
ariaLabel: {
|
|
81
|
+
control: "text",
|
|
82
|
+
description: "Accessible name for the root. Set it when the page has more than one `<nav>`",
|
|
27
83
|
table: { category: "Basic" },
|
|
28
84
|
},
|
|
29
|
-
navLinks: {
|
|
85
|
+
navLinks: {
|
|
86
|
+
control: "object",
|
|
87
|
+
description: "Top-level items: `path` renders a link, `childLinks` renders a toggle and panel",
|
|
88
|
+
table: { category: "Content" },
|
|
89
|
+
},
|
|
30
90
|
styleClassPassthrough: { table: { disable: true } },
|
|
31
91
|
},
|
|
32
92
|
args: {
|
|
33
93
|
tag: "nav",
|
|
34
94
|
navLinks,
|
|
95
|
+
headingLineClamp: "none",
|
|
35
96
|
},
|
|
97
|
+
decorators: [
|
|
98
|
+
(story, context) => ({
|
|
99
|
+
components: { story, CanvasSwitcher },
|
|
100
|
+
setup() {
|
|
101
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
102
|
+
return { canvasName };
|
|
103
|
+
},
|
|
104
|
+
template: `
|
|
105
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
106
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
107
|
+
</div>
|
|
108
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
109
|
+
<story />
|
|
110
|
+
</div>
|
|
111
|
+
`,
|
|
112
|
+
}),
|
|
113
|
+
],
|
|
36
114
|
parameters: {
|
|
115
|
+
layout: "fullscreen",
|
|
37
116
|
docs: {
|
|
38
117
|
description: {
|
|
39
118
|
component:
|
|
40
|
-
"`<details>`/`<summary>`-based
|
|
119
|
+
"`<details>`/`<summary>`-based version of `DeepExpandingMenu`: each item is a direct link (`path`) or a native `<details>` group showing its child links (`childLinks`, with an optional `childLinksTitle` heading). " +
|
|
120
|
+
"Groups in one menu are exclusive; outside click, Escape and following a child link close the open group. No Popover API or anchor positioning involved. Props: `navLinks`, `tag`, `ariaLabel`. " +
|
|
121
|
+
"Prefer `DeepExpandingMenu` unless you need this simpler fallback.",
|
|
41
122
|
},
|
|
42
123
|
},
|
|
43
124
|
},
|
|
44
125
|
};
|
|
45
126
|
|
|
46
127
|
export default meta;
|
|
47
|
-
type Story = StoryObj<
|
|
128
|
+
type Story = StoryObj<StoryArgs>;
|
|
129
|
+
|
|
130
|
+
function useStorySetup(args: StoryArgs) {
|
|
131
|
+
const componentArgs = computed(() => {
|
|
132
|
+
const { headingLineClamp: _headingLineClamp, ...rest } = args;
|
|
133
|
+
return rest;
|
|
134
|
+
});
|
|
135
|
+
const tokenStyles = computed(() => ({
|
|
136
|
+
"--deep-expanding-menu-classic-panel-heading-line-clamp": args.headingLineClamp,
|
|
137
|
+
}));
|
|
138
|
+
return { componentArgs, tokenStyles };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const render = (args: StoryArgs) => ({
|
|
142
|
+
components: { DeepExpandingMenuClassic },
|
|
143
|
+
setup() {
|
|
144
|
+
return useStorySetup(args);
|
|
145
|
+
},
|
|
146
|
+
template: `<div :style="tokenStyles"><DeepExpandingMenuClassic v-bind="componentArgs" /></div>`,
|
|
147
|
+
});
|
|
48
148
|
|
|
49
149
|
export const Default: Story = {
|
|
50
|
-
render
|
|
51
|
-
components: { DeepExpandingMenuClassic },
|
|
52
|
-
setup() {
|
|
53
|
-
return { args };
|
|
54
|
-
},
|
|
55
|
-
template: `<DeepExpandingMenuClassic v-bind="args" style="padding: 4rem;" />`,
|
|
56
|
-
}),
|
|
150
|
+
render,
|
|
57
151
|
};
|
|
58
152
|
|
|
59
153
|
export const CustomColours: Story = {
|
|
154
|
+
name: "Custom Colours",
|
|
60
155
|
render: (args) => ({
|
|
61
156
|
components: { DeepExpandingMenuClassic },
|
|
62
157
|
setup() {
|
|
63
|
-
return
|
|
158
|
+
return useStorySetup(args);
|
|
64
159
|
},
|
|
65
160
|
template: `
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
style="
|
|
69
|
-
|
|
161
|
+
<div
|
|
162
|
+
:style="tokenStyles"
|
|
163
|
+
style="--deep-expanding-menu-classic-panel-background-colour: #1a1a1a; --deep-expanding-menu-classic-panel-border-colour: transparent; --deep-expanding-menu-classic-group-link-colour: white; --deep-expanding-menu-classic-group-link-border-colour-hover: white; --deep-expanding-menu-classic-panel-heading-colour: #aaaaaa;"
|
|
164
|
+
>
|
|
165
|
+
<DeepExpandingMenuClassic v-bind="componentArgs" />
|
|
166
|
+
</div>
|
|
70
167
|
`,
|
|
71
168
|
}),
|
|
72
169
|
parameters: {
|
|
73
170
|
docs: {
|
|
74
171
|
description: {
|
|
75
|
-
story: "Overriding the public
|
|
172
|
+
story: "Overriding the public `--deep-expanding-menu-classic-*` tokens on a wrapper element.",
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export const StressTest: Story = {
|
|
179
|
+
name: "Stress Test (Worst-Case Data)",
|
|
180
|
+
args: {
|
|
181
|
+
navLinks: stressNavLinks,
|
|
182
|
+
},
|
|
183
|
+
render,
|
|
184
|
+
parameters: {
|
|
185
|
+
initialCanvas: "mobileCanvas",
|
|
186
|
+
docs: {
|
|
187
|
+
story: { inline: false, height: "720px" },
|
|
188
|
+
description: {
|
|
189
|
+
story:
|
|
190
|
+
"Hostile nav data at every `CanvasSwitcher` width. Check that: the top-level row wraps instead of overflowing; " +
|
|
191
|
+
"the long German label and the unbroken word break inside the canvas; the caret stays beside a wrapped toggle label; " +
|
|
192
|
+
"the first panel's long heading wraps (try the story-only `headingLineClamp` CSS token control), its unbroken URL link stays inside the panel, " +
|
|
193
|
+
"`<b>Not bold</b>` renders as text, the right-to-left link reads correctly, and its 31 links stay inside the panel; " +
|
|
194
|
+
"duplicate `Styling` and `Contact` items both render; a group without a title shows its list only; " +
|
|
195
|
+
"an empty group opens with its heading and no list; panels are never wider than the menu itself (they open from their toggle, so one near the right edge can still overrun it: `DeepExpandingMenu` flips instead).",
|
|
76
196
|
},
|
|
77
197
|
},
|
|
78
198
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
2
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import { defineComponent, h, nextTick } from "vue";
|
|
3
4
|
import DeepExpandingMenuClassic from "../DeepExpandingMenuClassic.vue";
|
|
4
5
|
import type { ResponsiveHeaderNavItem } from "~/types/components";
|
|
5
6
|
|
|
@@ -16,67 +17,184 @@ const defaultNavLinks: ResponsiveHeaderNavItem[] = [
|
|
|
16
17
|
{ name: "Contact", path: "/contact" },
|
|
17
18
|
];
|
|
18
19
|
|
|
20
|
+
const twoGroups: ResponsiveHeaderNavItem[] = [
|
|
21
|
+
{ name: "Services", childLinksTitle: "Our services", childLinks: [{ name: "Haircuts", path: "/haircuts" }] },
|
|
22
|
+
{ name: "About", childLinksTitle: "About us", childLinks: [{ name: "Team", path: "/team" }] },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const mounted: Awaited<ReturnType<typeof mountSuspended>>[] = [];
|
|
26
|
+
|
|
27
|
+
const mountMenu = async (options: { props?: Record<string, unknown>; attachTo?: HTMLElement } = {}) => {
|
|
28
|
+
const wrapper = await mountSuspended(DeepExpandingMenuClassic, options);
|
|
29
|
+
mounted.push(wrapper);
|
|
30
|
+
return wrapper;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
mounted.splice(0).forEach((wrapper) => wrapper.unmount());
|
|
35
|
+
vi.restoreAllMocks();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const groups = (wrapper: Awaited<ReturnType<typeof mountMenu>>) =>
|
|
39
|
+
wrapper.findAll("details").map((d) => d.element as HTMLDetailsElement);
|
|
40
|
+
|
|
19
41
|
describe("DeepExpandingMenuClassic", () => {
|
|
20
42
|
it("mounts without error", async () => {
|
|
21
|
-
const wrapper = await
|
|
43
|
+
const wrapper = await mountMenu({ props: { navLinks: defaultNavLinks } });
|
|
22
44
|
expect(wrapper.vm).toBeTruthy();
|
|
23
45
|
});
|
|
24
46
|
|
|
25
47
|
it("renders correct HTML structure", async () => {
|
|
26
|
-
const wrapper = await
|
|
48
|
+
const wrapper = await mountMenu({ props: { navLinks: defaultNavLinks } });
|
|
27
49
|
expect(wrapper.html()).toMatchSnapshot();
|
|
28
50
|
});
|
|
29
51
|
|
|
30
52
|
it("renders a <nav> by default", async () => {
|
|
31
|
-
const wrapper = await
|
|
53
|
+
const wrapper = await mountMenu({ props: { navLinks: defaultNavLinks } });
|
|
32
54
|
expect(wrapper.element.tagName.toLowerCase()).toBe("nav");
|
|
33
55
|
});
|
|
34
56
|
|
|
35
57
|
it("renders the given tag", async () => {
|
|
36
|
-
const wrapper = await
|
|
37
|
-
props: { navLinks: defaultNavLinks, tag: "div" },
|
|
38
|
-
});
|
|
58
|
+
const wrapper = await mountMenu({ props: { navLinks: defaultNavLinks, tag: "div" } });
|
|
39
59
|
expect(wrapper.element.tagName.toLowerCase()).toBe("div");
|
|
40
60
|
});
|
|
41
61
|
|
|
62
|
+
it("labels the root when ariaLabel is set, and leaves it unlabelled otherwise", async () => {
|
|
63
|
+
const unlabelled = await mountMenu({ props: { navLinks: defaultNavLinks } });
|
|
64
|
+
expect(unlabelled.attributes("aria-label")).toBeUndefined();
|
|
65
|
+
|
|
66
|
+
const labelled = await mountMenu({ props: { navLinks: defaultNavLinks, ariaLabel: "Main" } });
|
|
67
|
+
expect(labelled.attributes("aria-label")).toBe("Main");
|
|
68
|
+
});
|
|
69
|
+
|
|
42
70
|
it("renders a direct link for items with a path", async () => {
|
|
43
|
-
const wrapper = await
|
|
44
|
-
const links = wrapper.findAll(".
|
|
71
|
+
const wrapper = await mountMenu({ props: { navLinks: defaultNavLinks } });
|
|
72
|
+
const links = wrapper.findAll(".deep-expanding-menu-classic-link");
|
|
45
73
|
expect(links.map((l) => l.text())).toEqual(["Home", "Contact"]);
|
|
46
74
|
});
|
|
47
75
|
|
|
48
76
|
it("renders a <details> group for items with childLinks", async () => {
|
|
49
|
-
const wrapper = await
|
|
50
|
-
expect(wrapper.find("details.
|
|
51
|
-
expect(wrapper.find(".
|
|
77
|
+
const wrapper = await mountMenu({ props: { navLinks: defaultNavLinks } });
|
|
78
|
+
expect(wrapper.find("details.deep-expanding-menu-classic-group").exists()).toBe(true);
|
|
79
|
+
expect(wrapper.find(".deep-expanding-menu-classic-group-toggle").text()).toContain("Services");
|
|
52
80
|
});
|
|
53
81
|
|
|
54
82
|
it("renders each child link inside the group panel", async () => {
|
|
55
|
-
const wrapper = await
|
|
56
|
-
const childLinks = wrapper.findAll(".
|
|
83
|
+
const wrapper = await mountMenu({ props: { navLinks: defaultNavLinks } });
|
|
84
|
+
const childLinks = wrapper.findAll(".deep-expanding-menu-classic-group-link");
|
|
57
85
|
expect(childLinks.map((l) => l.text())).toEqual(["Haircuts", "Colouring"]);
|
|
58
86
|
});
|
|
59
87
|
|
|
60
88
|
it("renders the childLinksTitle heading", async () => {
|
|
61
|
-
const wrapper = await
|
|
62
|
-
expect(wrapper.find(".
|
|
89
|
+
const wrapper = await mountMenu({ props: { navLinks: defaultNavLinks } });
|
|
90
|
+
expect(wrapper.find(".deep-expanding-menu-classic-group-heading").text()).toBe("Our services");
|
|
63
91
|
});
|
|
64
92
|
|
|
65
|
-
it("
|
|
66
|
-
const
|
|
67
|
-
|
|
93
|
+
it("omits the heading when childLinksTitle is missing or empty", async () => {
|
|
94
|
+
const navLinks: ResponsiveHeaderNavItem[] = [
|
|
95
|
+
{ name: "A", childLinks: [{ name: "A1", path: "/a1" }] },
|
|
96
|
+
{ name: "B", childLinksTitle: "", childLinks: [{ name: "B1", path: "/b1" }] },
|
|
97
|
+
];
|
|
98
|
+
const wrapper = await mountMenu({ props: { navLinks } });
|
|
99
|
+
expect(wrapper.find(".deep-expanding-menu-classic-group-heading").exists()).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("omits the list when a group has no child links", async () => {
|
|
103
|
+
const navLinks: ResponsiveHeaderNavItem[] = [{ name: "Empty", childLinksTitle: "Empty", childLinks: [] }];
|
|
104
|
+
const wrapper = await mountMenu({ props: { navLinks } });
|
|
105
|
+
expect(wrapper.find(".deep-expanding-menu-classic-group-list").exists()).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("gives the child link list a list role and hides the caret from assistive technology", async () => {
|
|
109
|
+
const wrapper = await mountMenu({ props: { navLinks: defaultNavLinks } });
|
|
110
|
+
expect(wrapper.find(".deep-expanding-menu-classic-group-list").attributes("role")).toBe("list");
|
|
111
|
+
expect(wrapper.find(".deep-expanding-menu-classic-group-icon").attributes("aria-hidden")).toBe("true");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("keeps child links with duplicate names in step when the list is reordered", async () => {
|
|
115
|
+
const childLinks = [
|
|
116
|
+
{ name: "Same", path: "/one" },
|
|
117
|
+
{ name: "Same", path: "/two" },
|
|
118
|
+
{ name: "Other", path: "/three" },
|
|
119
|
+
];
|
|
120
|
+
const wrapper = await mountMenu({ props: { navLinks: [{ name: "Group", childLinks }] } });
|
|
121
|
+
await wrapper.setProps({ navLinks: [{ name: "Group", childLinks: [childLinks[2]!, childLinks[0]!] }] });
|
|
122
|
+
const links = wrapper.findAll(".deep-expanding-menu-classic-group-link");
|
|
123
|
+
expect(links.map((l) => l.attributes("href"))).toEqual(["/three", "/one"]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("shares a details name within one menu but not across menus", async () => {
|
|
127
|
+
const Host = defineComponent({
|
|
128
|
+
setup: () => () => [
|
|
129
|
+
h(DeepExpandingMenuClassic, { navLinks: twoGroups }),
|
|
130
|
+
h(DeepExpandingMenuClassic, { navLinks: twoGroups }),
|
|
131
|
+
],
|
|
68
132
|
});
|
|
133
|
+
const wrapper = await mountSuspended(Host);
|
|
134
|
+
mounted.push(wrapper);
|
|
135
|
+
const names = wrapper.findAll("details").map((d) => d.attributes("name"));
|
|
136
|
+
expect(names[0]).toBe(names[1]);
|
|
137
|
+
expect(names[2]).toBe(names[3]);
|
|
138
|
+
expect(names[0]).not.toBe(names[2]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("applies styleClassPassthrough classes", async () => {
|
|
142
|
+
const wrapper = await mountMenu({ props: { navLinks: defaultNavLinks, styleClassPassthrough: ["custom-class"] } });
|
|
69
143
|
expect(wrapper.classes()).toContain("custom-class");
|
|
70
144
|
});
|
|
71
145
|
|
|
72
146
|
it("resets classes when styleClassPassthrough prop changes", async () => {
|
|
73
|
-
const wrapper = await
|
|
74
|
-
props: { navLinks: defaultNavLinks, styleClassPassthrough: ["initial-class"] },
|
|
75
|
-
});
|
|
147
|
+
const wrapper = await mountMenu({ props: { navLinks: defaultNavLinks, styleClassPassthrough: ["initial-class"] } });
|
|
76
148
|
expect(wrapper.classes()).toContain("initial-class");
|
|
77
149
|
|
|
78
150
|
await wrapper.setProps({ styleClassPassthrough: ["updated-class"] });
|
|
79
151
|
expect(wrapper.classes()).not.toContain("initial-class");
|
|
80
152
|
expect(wrapper.classes()).toContain("updated-class");
|
|
81
153
|
});
|
|
154
|
+
|
|
155
|
+
describe("closing", () => {
|
|
156
|
+
it("closes an open group when something else in the menu is clicked", async () => {
|
|
157
|
+
const wrapper = await mountMenu({ props: { navLinks: twoGroups } });
|
|
158
|
+
const [first] = groups(wrapper);
|
|
159
|
+
first!.open = true;
|
|
160
|
+
|
|
161
|
+
await wrapper.findAll("summary")[1]!.trigger("click");
|
|
162
|
+
expect(first!.open).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("keeps a group open when its own panel is clicked, and closes it when a child link is followed", async () => {
|
|
166
|
+
const wrapper = await mountMenu({ props: { navLinks: twoGroups } });
|
|
167
|
+
const [first] = groups(wrapper);
|
|
168
|
+
first!.open = true;
|
|
169
|
+
|
|
170
|
+
await wrapper.find(".deep-expanding-menu-classic-group-heading").trigger("click");
|
|
171
|
+
expect(first!.open).toBe(true);
|
|
172
|
+
|
|
173
|
+
await wrapper.find(".deep-expanding-menu-classic-group-link").trigger("click");
|
|
174
|
+
expect(first!.open).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("closes the group on Escape and returns focus to its toggle", async () => {
|
|
178
|
+
const wrapper = await mountMenu({ props: { navLinks: twoGroups }, attachTo: document.body });
|
|
179
|
+
const [first] = groups(wrapper);
|
|
180
|
+
first!.open = true;
|
|
181
|
+
|
|
182
|
+
const link = wrapper.find(".deep-expanding-menu-classic-group-link");
|
|
183
|
+
await link.trigger("keydown", { key: "Escape" });
|
|
184
|
+
expect(first!.open).toBe(false);
|
|
185
|
+
expect(document.activeElement).toBe(wrapper.findAll("summary")[0]!.element);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("closes open groups on a click outside the menu", async () => {
|
|
189
|
+
const wrapper = await mountMenu({ props: { navLinks: twoGroups }, attachTo: document.body });
|
|
190
|
+
const [first] = groups(wrapper);
|
|
191
|
+
first!.open = true;
|
|
192
|
+
vi.advanceTimersByTime(1);
|
|
193
|
+
|
|
194
|
+
document.body.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true }));
|
|
195
|
+
document.body.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
196
|
+
await nextTick();
|
|
197
|
+
expect(first!.open).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
82
200
|
});
|
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
exports[`DeepExpandingMenuClassic > renders correct HTML structure 1`] = `
|
|
4
4
|
"<nav class="deep-expanding-menu-classic">
|
|
5
|
-
<div class="inner"><a href="/" class="
|
|
6
|
-
<details class="
|
|
7
|
-
<summary class="
|
|
8
|
-
<div class="
|
|
9
|
-
<h4 class="page-heading-4 mb-6">Our services</h4>
|
|
10
|
-
<ul class="
|
|
11
|
-
<li class="
|
|
12
|
-
<li class="
|
|
5
|
+
<div class="deep-expanding-menu-classic-inner"><a href="/" class="deep-expanding-menu-classic-link">Home</a>
|
|
6
|
+
<details class="deep-expanding-menu-classic-group" name="deep-expanding-menu-classic-v-0-0">
|
|
7
|
+
<summary class="deep-expanding-menu-classic-group-toggle"><span class="deep-expanding-menu-classic-group-label">Services</span><span class="iconify i-bi:caret-down-fill deep-expanding-menu-classic-group-icon" aria-hidden="true"></span></summary>
|
|
8
|
+
<div class="deep-expanding-menu-classic-group-panel">
|
|
9
|
+
<h4 class="deep-expanding-menu-classic-group-heading page-heading-4 mb-6">Our services</h4>
|
|
10
|
+
<ul class="deep-expanding-menu-classic-group-list" role="list">
|
|
11
|
+
<li class="deep-expanding-menu-classic-group-item"><a href="/services/haircuts" class="deep-expanding-menu-classic-group-link">Haircuts</a></li>
|
|
12
|
+
<li class="deep-expanding-menu-classic-group-item"><a href="/services/colouring" class="deep-expanding-menu-classic-group-link">Colouring</a></li>
|
|
13
13
|
</ul>
|
|
14
14
|
</div>
|
|
15
|
-
</details><a href="/contact" class="
|
|
15
|
+
</details><a href="/contact" class="deep-expanding-menu-classic-link">Contact</a>
|
|
16
16
|
</div>
|
|
17
17
|
</nav>"
|
|
18
18
|
`;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|---|---|---|
|
|
9
9
|
| `--capture-qr-code-aspect-ratio` | `1 / 1` | Root aspect ratio (the camera frame) |
|
|
10
10
|
| `--capture-qr-code-error-gap` | `1.2rem` | Gap between the error message and the reset button |
|
|
11
|
+
| `--capture-qr-code-results-indent` | `2rem` | Inline-start padding of the results list (bullet indent) |
|
|
11
12
|
|
|
12
13
|
The reset button is `InputButton` (`variant="secondary"`), styled by its own tokens.
|
|
13
14
|
|
|
@@ -20,13 +21,19 @@ The reset button is `InputButton` (`variant="secondary"`), styled by its own tok
|
|
|
20
21
|
| `--decode-qr-code-dropzone-min-height` | `3rem` | Drop zone minimum height |
|
|
21
22
|
| `--decode-qr-code-dropzone-padding` | `1.2rem` | Drop zone padding |
|
|
22
23
|
| `--decode-qr-code-dropzone-border-radius` | `0.5rem` | Drop zone corner rounding |
|
|
23
|
-
| `--decode-qr-code-dropzone-border-width` | `
|
|
24
|
-
| `--decode-qr-code-dropzone-border-colour` | `
|
|
24
|
+
| `--decode-qr-code-dropzone-border-width` | `0.2rem` | Drop zone dashed border width |
|
|
25
|
+
| `--decode-qr-code-dropzone-border-colour` | `var(--theme-border)` | Drop zone border colour |
|
|
25
26
|
| `--decode-qr-code-dropzone-border-colour-active` | the border colour | Border colour while an image is dragged over |
|
|
26
|
-
| `--decode-qr-code-dropzone-background-active` | `
|
|
27
|
+
| `--decode-qr-code-dropzone-background-active` | `var(--theme-surface-subtle)` | Background while an image is dragged over |
|
|
28
|
+
| `--decode-qr-code-results-indent` | `2rem` | Inline-start padding of the results list (bullet indent) |
|
|
27
29
|
|
|
28
|
-
Private (not public API): `--
|
|
30
|
+
Private (not public API): `--_decode-qr-code-dropzone-border-colour`.
|
|
29
31
|
|
|
32
|
+
> Changed 2026-10-21: the drop zone border defaults to `var(--theme-border)` (was `gray`), its
|
|
33
|
+
> width to `0.2rem` (was `2px`), and the active background to `var(--theme-surface-subtle)` (was
|
|
34
|
+
> `transparent`, so dragging gave no feedback beyond the border). The private token was renamed
|
|
35
|
+
> from `--_dropzone-border-colour`.
|
|
36
|
+
>
|
|
30
37
|
> Changed 2026-09-28: all of these were hardcoded or missing. DecodeQrCode's root is no longer
|
|
31
38
|
> forced to `aspect-ratio: 1 / 1`.
|
|
32
39
|
|
|
@@ -38,14 +45,22 @@ Private (not public API): `--_dropzone-border-colour`.
|
|
|
38
45
|
|---|---|---|
|
|
39
46
|
| `.capture-qr-code-stopped` | Capture | Camera stopped (tab hidden, route leave), no error |
|
|
40
47
|
| `.capture-qr-code-error` (`role="alert"`) | Capture | Camera error; replaceable via the `#error` slot |
|
|
41
|
-
| `.decode-qr-code-dropzone
|
|
48
|
+
| `.decode-qr-code-dropzone[data-dropping]` | Decode | An image is dragged over the drop zone |
|
|
49
|
+
| `.decode-qr-code-status` | Decode | No QR code found (`noResultLabel`), or with `[data-error]` the file couldn't be read (`readErrorLabel`) |
|
|
42
50
|
|
|
43
|
-
Inner classes: Capture `.capture-qr-code-camera`, `.capture-qr-code-results
|
|
51
|
+
Inner classes: Capture `.capture-qr-code-camera`, `.capture-qr-code-results`,
|
|
52
|
+
`.capture-qr-code-results-list`, `.capture-qr-code-result`, `.capture-qr-code-error-message`; Decode
|
|
44
53
|
`.decode-qr-code-upload` (label), `.decode-qr-code-upload-label`, `.decode-qr-code-capture` (file
|
|
45
|
-
input), `.decode-qr-code-dropzone`, `.decode-qr-code-dropzone-label`, `.decode-qr-code-results
|
|
54
|
+
input), `.decode-qr-code-dropzone`, `.decode-qr-code-dropzone-label`, `.decode-qr-code-results`,
|
|
55
|
+
`.decode-qr-code-results-list`, `.decode-qr-code-result`. Results, error messages and labels wrap
|
|
56
|
+
(`overflow-wrap: anywhere`), so a long decoded URL stays inside the component.
|
|
46
57
|
Both results wrappers are polite live regions and always rendered; the `<ul>` inside appears once a
|
|
47
58
|
code is decoded.
|
|
48
59
|
|
|
60
|
+
> Changed 2026-10-21: `.decode-qr-code-dropzone.is-dropping` is now `[data-dropping]` (a bare state
|
|
61
|
+
> class could collide with a consumer's own `.is-dropping`), and each result `<li>` lost its inner
|
|
62
|
+
> `<span>` and gained a class.
|
|
63
|
+
>
|
|
49
64
|
> Changed 2026-09-28: renamed from `.capture-qr-stream` (root), `.camera-stopped`, `.camera-error`,
|
|
50
65
|
> `.scanned-results`, `.qr-code-capture` and `.qr-code-dropzone`.
|
|
51
66
|
|