srcdev-nuxt-components 9.6.12 → 9.6.14
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 +2 -1
- package/.claude/rules/consumer/story-register-components.md +1 -1
- package/.claude/rules/consumer/vfor-keys-from-data.md +14 -0
- package/.claude/skills/component-compliance-checklist.md +24 -1
- package/.claude/skills/component-naming.md +1 -0
- package/.claude/skills/components/banner-video.md +24 -30
- package/.claude/skills/components/content-docs.md +18 -5
- package/.claude/skills/components/display-dialog.md +16 -6
- package/.claude/skills/components/display-qr-code.md +22 -4
- package/.claude/skills/components/{display-tooltip-defined.md → display-tooltip-rich.md} +40 -42
- package/.claude/skills/components/display-tooltip.md +17 -6
- package/.claude/skills/components/eyebrow-text.md +9 -5
- package/.claude/skills/components/header-block.md +13 -5
- package/.claude/skills/components/hero-text.md +13 -9
- package/.claude/skills/components/link-text.md +8 -5
- package/.claude/skills/components/pop-over.md +17 -3
- package/.claude/skills/components/skip-links.md +16 -0
- package/.claude/skills/composable-tooltips-guide.md +4 -4
- package/.claude/skills/index.md +6 -6
- package/.claude/skills/storybook-add-story.md +5 -2
- package/.claude/skills/testing-add-playwright.md +12 -7
- package/.claude/skills/testing-add-unit-test.md +8 -0
- package/.vscode/srcdev-component-banner-video.code-snippets +38 -25
- package/.vscode/srcdev-component-content-docs.code-snippets +69 -60
- package/.vscode/srcdev-component-display-dialog.code-snippets +7 -5
- package/.vscode/srcdev-component-display-qr-code.code-snippets +12 -1
- package/.vscode/srcdev-component-display-tooltip-rich.code-snippets +50 -0
- package/.vscode/srcdev-component-display-tooltip.code-snippets +31 -0
- package/.vscode/srcdev-component-hero-text.code-snippets +21 -5
- package/.vscode/srcdev-component-link-text.code-snippets +3 -3
- package/.vscode/srcdev-component-pop-over.code-snippets +28 -0
- package/.vscode/srcdev-component-skip-links.code-snippets +1 -1
- package/app/components/01.atoms/banner-video/BannerVideo.vue +51 -19
- package/app/components/01.atoms/banner-video/CONSUMER-STYLING.md +59 -40
- package/app/components/01.atoms/banner-video/stories/BannerVideo.stories.ts +88 -1
- package/app/components/01.atoms/banner-video/tests/BannerVideo.spec.ts +127 -78
- package/app/components/01.atoms/banner-video/tests/__snapshots__/BannerVideo.spec.ts.snap +12 -4
- package/app/components/01.atoms/content-wrappers/docs-pages/CONSUMER-STYLING.md +42 -14
- package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +86 -112
- package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +134 -8
- package/app/components/01.atoms/content-wrappers/docs-pages/tests/ContentDocs.spec.ts +131 -65
- package/app/components/01.atoms/display-dialog/CONSUMER-STYLING.md +28 -3
- package/app/components/01.atoms/display-dialog/DisplayDialog.vue +39 -31
- package/app/components/01.atoms/display-dialog/stories/DisplayDialog.stories.ts +92 -1
- package/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts +87 -41
- package/app/components/01.atoms/display-dialog/tests/__snapshots__/DisplayDialog.spec.ts.snap +5 -1
- package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +70 -24
- package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +24 -12
- package/app/components/01.atoms/display-tooltip/stories/DisplayTooltip.stories.ts +89 -2
- package/app/components/01.atoms/display-tooltip/tests/DisplayTooltip.spec.ts +65 -18
- package/app/components/01.atoms/display-tooltip/tests/__snapshots__/DisplayTooltip.spec.ts.snap +3 -3
- package/app/components/01.atoms/navigation/skip-links/CONSUMER-STYLING.md +65 -19
- package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +20 -14
- package/app/components/01.atoms/navigation/skip-links/stories/SkipLinks.stories.ts +89 -20
- package/app/components/01.atoms/navigation/skip-links/tests/SkipLinks.spec.ts +70 -10
- package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +42 -9
- package/app/components/01.atoms/pop-over/PopOver.vue +28 -12
- package/app/components/01.atoms/pop-over/stories/PopOver.stories.ts +114 -2
- package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +35 -0
- package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +5 -1
- package/app/components/01.atoms/qr-code/CONSUMER-STYLING.md +16 -3
- package/app/components/01.atoms/qr-code/DisplayQrCode.vue +31 -0
- package/app/components/01.atoms/qr-code/stories/DisplayQrCode.stories.ts +190 -157
- package/app/components/01.atoms/qr-code/tests/DisplayQrCode.spec.ts +58 -0
- package/app/components/01.atoms/text-blocks/eyebrow-text/CONSUMER-STYLING.md +9 -1
- package/app/components/01.atoms/text-blocks/eyebrow-text/EyebrowText.vue +5 -4
- package/app/components/01.atoms/text-blocks/eyebrow-text/playwright/eyebrow-text.playwright.ts +1 -1
- package/app/components/01.atoms/text-blocks/eyebrow-text/stories/EyebrowText.stories.ts +104 -17
- package/app/components/01.atoms/text-blocks/eyebrow-text/tests/EyebrowText.spec.ts +68 -118
- package/app/components/01.atoms/text-blocks/eyebrow-text/tests/__snapshots__/EyebrowText.spec.ts.snap +8 -8
- package/app/components/01.atoms/text-blocks/header-block/CONSUMER-STYLING.md +49 -24
- package/app/components/01.atoms/text-blocks/header-block/HeaderBlock.vue +9 -1
- package/app/components/01.atoms/text-blocks/header-block/stories/HeaderBlock.stories.ts +96 -26
- package/app/components/01.atoms/text-blocks/header-block/tests/HeaderBlock.spec.ts +64 -48
- package/app/components/01.atoms/text-blocks/header-block/tests/__snapshots__/HeaderBlock.spec.ts.snap +2 -2
- package/app/components/01.atoms/text-blocks/hero-text/CONSUMER-STYLING.md +22 -6
- package/app/components/01.atoms/text-blocks/hero-text/HeroText.vue +32 -23
- package/app/components/01.atoms/text-blocks/hero-text/playwright/hero-text.playwright.ts +1 -1
- package/app/components/01.atoms/text-blocks/hero-text/stories/HeroText.stories.ts +176 -23
- package/app/components/01.atoms/text-blocks/hero-text/tests/HeroText.spec.ts +129 -123
- package/app/components/01.atoms/text-blocks/hero-text/tests/__snapshots__/HeroText.spec.ts.snap +2 -2
- package/app/components/01.atoms/text-blocks/link-text/CONSUMER-STYLING.md +17 -0
- package/app/components/01.atoms/text-blocks/link-text/LinkText.vue +9 -3
- package/app/components/01.atoms/text-blocks/link-text/stories/LinkText.stories.ts +96 -99
- package/app/components/01.atoms/text-blocks/link-text/tests/LinkText.spec.ts +84 -93
- package/app/components/02.molecules/display-tooltip-rich/CONSUMER-STYLING.md +110 -0
- package/app/components/02.molecules/display-tooltip-rich/DisplayTooltipRich.vue +148 -0
- package/app/components/02.molecules/display-tooltip-rich/stories/DisplayTooltipRich.stories.ts +254 -0
- package/app/components/02.molecules/display-tooltip-rich/tests/DisplayTooltipRich.spec.ts +156 -0
- package/app/components/02.molecules/display-tooltip-rich/tests/__snapshots__/DisplayTooltipRich.spec.ts.snap +22 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +1 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +1 -1
- package/app/components/02.molecules/price-list/tests/__snapshots__/PriceList.spec.ts.snap +8 -8
- package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +1 -0
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +15 -15
- package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +3 -3
- package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +3 -3
- package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +3 -3
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +1 -1
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +9 -9
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +9 -9
- package/app/composables/tests/useTooltips.spec.ts +3 -3
- package/app/types/components/index.ts +1 -1
- package/package.json +1 -1
- package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +0 -29
- package/app/components/02.molecules/display-tooltip-defined/CONSUMER-STYLING.md +0 -53
- package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +0 -117
- package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts +0 -129
- package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts +0 -85
- package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +0 -17
- /package/app/types/components/{display-tooltip-defined.d.ts → display-tooltip-rich.d.ts} +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
-
import { computed } from "vue";
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
3
|
import ContentDocs from "../ContentDocs.vue";
|
|
4
|
-
import
|
|
4
|
+
import CanvasSwitcher from "../../../canvas-switcher/CanvasSwitcher.vue";
|
|
5
|
+
import type { DocsNavItem, MediaCanvas } from "~/types/components";
|
|
5
6
|
|
|
6
7
|
interface ContentDocsStoryArgs {
|
|
7
8
|
tag: "div" | "section" | "article" | "main";
|
|
@@ -9,6 +10,9 @@ interface ContentDocsStoryArgs {
|
|
|
9
10
|
docsPageNavItems: DocsNavItem[];
|
|
10
11
|
docsNavLabel: string;
|
|
11
12
|
docsPageNavLabel: string;
|
|
13
|
+
docsNavAriaLabel: string;
|
|
14
|
+
docsPageNavAriaLabel: string;
|
|
15
|
+
panelVariant: "modern" | "classic";
|
|
12
16
|
styleClassPassthrough: string[];
|
|
13
17
|
activeNavItem: string;
|
|
14
18
|
activePageNavItem: string;
|
|
@@ -50,11 +54,28 @@ export default {
|
|
|
50
54
|
docsNavLabel: {
|
|
51
55
|
control: "text",
|
|
52
56
|
description: "Heading text for the docsNav panel",
|
|
53
|
-
table: { category: "
|
|
57
|
+
table: { category: "Copy" },
|
|
54
58
|
},
|
|
55
59
|
docsPageNavLabel: {
|
|
56
60
|
control: "text",
|
|
57
61
|
description: "Heading text for the docsPageNav panel",
|
|
62
|
+
table: { category: "Copy" },
|
|
63
|
+
},
|
|
64
|
+
docsNavAriaLabel: {
|
|
65
|
+
control: "text",
|
|
66
|
+
description: "aria-label on the docsNav `<nav>` landmark. Override for localisation",
|
|
67
|
+
table: { category: "Copy" },
|
|
68
|
+
},
|
|
69
|
+
docsPageNavAriaLabel: {
|
|
70
|
+
control: "text",
|
|
71
|
+
description: "aria-label on the docsPageNav `<nav>` landmark. Override for localisation",
|
|
72
|
+
table: { category: "Copy" },
|
|
73
|
+
},
|
|
74
|
+
panelVariant: {
|
|
75
|
+
control: { type: "select" },
|
|
76
|
+
options: ["classic", "modern"],
|
|
77
|
+
description:
|
|
78
|
+
"Collapsible panel implementation. `classic` (default) works on WebKit; `modern` uses `::details-content`, which Safari drops",
|
|
58
79
|
table: { category: "Basic" },
|
|
59
80
|
},
|
|
60
81
|
styleClassPassthrough: {
|
|
@@ -64,12 +85,12 @@ export default {
|
|
|
64
85
|
},
|
|
65
86
|
activeNavItem: {
|
|
66
87
|
control: "text",
|
|
67
|
-
description: "The `to` of the currently-active docsNav item",
|
|
88
|
+
description: "The `to` of the currently-active docsNav item (`v-model:active-nav-item`)",
|
|
68
89
|
table: { category: "Model" },
|
|
69
90
|
},
|
|
70
91
|
activePageNavItem: {
|
|
71
92
|
control: "text",
|
|
72
|
-
description: "The `to` of the currently-active docsPageNav item",
|
|
93
|
+
description: "The `to` of the currently-active docsPageNav item (`v-model:active-page-nav-item`)",
|
|
73
94
|
table: { category: "Model" },
|
|
74
95
|
},
|
|
75
96
|
},
|
|
@@ -79,10 +100,40 @@ export default {
|
|
|
79
100
|
docsPageNavItems: defaultPageNavItems,
|
|
80
101
|
docsNavLabel: "Navigation",
|
|
81
102
|
docsPageNavLabel: "On this page",
|
|
103
|
+
docsNavAriaLabel: "Docs navigation",
|
|
104
|
+
docsPageNavAriaLabel: "On this page",
|
|
105
|
+
panelVariant: "classic",
|
|
82
106
|
styleClassPassthrough: [],
|
|
83
107
|
activeNavItem: defaultNavItems[0]?.to,
|
|
84
108
|
activePageNavItem: "",
|
|
85
109
|
},
|
|
110
|
+
decorators: [
|
|
111
|
+
// Breakpoints are container queries on the component's own width, so a constrained wrapper stands in for a narrower viewport.
|
|
112
|
+
(story, context) => ({
|
|
113
|
+
components: { story, CanvasSwitcher },
|
|
114
|
+
setup() {
|
|
115
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
116
|
+
return { canvasName };
|
|
117
|
+
},
|
|
118
|
+
template: `
|
|
119
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
120
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
121
|
+
</div>
|
|
122
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 1.6rem; outline: 1px dashed currentColor;">
|
|
123
|
+
<story />
|
|
124
|
+
</div>
|
|
125
|
+
`,
|
|
126
|
+
}),
|
|
127
|
+
],
|
|
128
|
+
parameters: {
|
|
129
|
+
layout: "fullscreen",
|
|
130
|
+
docs: {
|
|
131
|
+
description: {
|
|
132
|
+
component:
|
|
133
|
+
"A three-area docs layout: a site-level `docsNavItems` list, an on-this-page `docsPageNavItems` list, and the `docsContent` slot. Breakpoints follow the component's own width (container queries, mirrored in script): below 768px both navs are collapsible panels that overlay the content, from 768px the page nav is pinned open in a right-hand column, and from 1024px both are pinned open in three columns. The active link in each list is a `v-model` (`activeNavItem`, `activePageNavItem`) and gets `aria-current=\"page\"`. Headings and landmark labels are props for localisation, and `panelVariant` picks the collapsible implementation (`classic` by default, for WebKit).",
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
86
137
|
} as Meta<typeof ContentDocs>;
|
|
87
138
|
|
|
88
139
|
// ─── Stories ─────────────────────────────────────────────────────────────────
|
|
@@ -104,39 +155,45 @@ const Template: StoryFn<ContentDocsStoryArgs> = (args) => ({
|
|
|
104
155
|
<template #docsContent>
|
|
105
156
|
<h3 style="margin-top:0">Docs Content</h3>
|
|
106
157
|
<p>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
browser viewport, so page decoration elsewhere on a real page won't throw them off.
|
|
158
|
+
Switch canvas above to see the layout change between mobile, tablet and desktop. The
|
|
159
|
+
thresholds follow the component's own width, not the browser viewport.
|
|
110
160
|
</p>
|
|
111
161
|
</template>
|
|
112
162
|
</ContentDocs>
|
|
113
163
|
`,
|
|
114
164
|
});
|
|
115
165
|
|
|
166
|
+
const story = (text: string) => ({ docs: { description: { story: text } } });
|
|
167
|
+
|
|
116
168
|
export const Default = Template.bind({});
|
|
117
169
|
Default.args = {};
|
|
170
|
+
Default.parameters = story("Both navs with icons on some items, the first docs link active.");
|
|
118
171
|
|
|
119
172
|
export const NoIcons = Template.bind({});
|
|
120
173
|
NoIcons.args = {
|
|
121
174
|
docsNavItems: defaultNavItems.map(({ icon: _icon, ...item }) => item),
|
|
122
175
|
docsPageNavItems: defaultPageNavItems.map(({ icon: _icon, ...item }) => item),
|
|
123
176
|
};
|
|
177
|
+
NoIcons.parameters = story("No item has an icon: the icon column stays reserved, so labels still line up.");
|
|
124
178
|
|
|
125
179
|
export const NavOnly = Template.bind({});
|
|
126
180
|
NavOnly.args = {
|
|
127
181
|
docsPageNavItems: [],
|
|
128
182
|
};
|
|
183
|
+
NavOnly.parameters = story("An empty `docsPageNavItems` omits the page nav entirely.");
|
|
129
184
|
|
|
130
185
|
export const PageNavOnly = Template.bind({});
|
|
131
186
|
PageNavOnly.args = {
|
|
132
187
|
docsNavItems: [],
|
|
133
188
|
};
|
|
189
|
+
PageNavOnly.parameters = story("An empty `docsNavItems` omits the docs nav entirely.");
|
|
134
190
|
|
|
135
191
|
export const CustomLabels = Template.bind({});
|
|
136
192
|
CustomLabels.args = {
|
|
137
193
|
docsNavLabel: "Sections",
|
|
138
194
|
docsPageNavLabel: "Contents",
|
|
139
195
|
};
|
|
196
|
+
CustomLabels.parameters = story("Custom panel headings via `docsNavLabel` and `docsPageNavLabel`.");
|
|
140
197
|
|
|
141
198
|
export const IconAtEnd: StoryFn<ContentDocsStoryArgs> = (args) => ({
|
|
142
199
|
components: { ContentDocs },
|
|
@@ -169,3 +226,72 @@ export const IconAtEnd: StoryFn<ContentDocsStoryArgs> = (args) => ({
|
|
|
169
226
|
`,
|
|
170
227
|
});
|
|
171
228
|
IconAtEnd.args = {};
|
|
229
|
+
IconAtEnd.parameters = story(
|
|
230
|
+
"`--content-docs-nav-link-icon-order: rtl` (and the page-nav equivalent) puts each icon after its label."
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const stressNavItems: DocsNavItem[] = [
|
|
234
|
+
{ label: "Bevorzugtekontaktmethodefürrückfragenundterminvereinbarungen", to: "/docs/a", icon: "lucide:rocket" },
|
|
235
|
+
{ label: "https://example.com/a/very/long/path/that/never/breaks/naturally/at/all", to: "/docs/b" },
|
|
236
|
+
{ label: "😀 Emoji first", to: "/docs/c", icon: "lucide:smile" },
|
|
237
|
+
{ label: "<b>not bold</b>", to: "/docs/d" },
|
|
238
|
+
{ label: "طريقة الاتصال المفضلة", to: "/docs/e", icon: "lucide:globe" },
|
|
239
|
+
{ label: "", to: "/docs/f" },
|
|
240
|
+
{ label: "x", to: "/docs/g" },
|
|
241
|
+
{ label: "Duplicate destination", to: "/docs/a" },
|
|
242
|
+
{ label: "Broken icon name", to: "/docs/h", icon: "lucide:does-not-exist" },
|
|
243
|
+
...Array.from({ length: 30 }, (_, i) => ({ label: `Generated page ${i + 1}`, to: `/docs/generated-${i + 1}` })),
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
const stressPageNavItems: DocsNavItem[] = [
|
|
247
|
+
{ label: "Ein sehr langer Abschnittstitel, der in einer schmalen Spalte umbrechen muss", to: "/docs#one" },
|
|
248
|
+
{ label: "Abschnittohneleerzeichendernichtumbrechenkannundtrotzdempassenmuss", to: "/docs#two" },
|
|
249
|
+
{ label: "طريقة الاتصال", to: "/docs#three" },
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
export const StressTest: StoryFn<ContentDocsStoryArgs> = (args) => ({
|
|
253
|
+
components: { ContentDocs },
|
|
254
|
+
setup() {
|
|
255
|
+
return { args };
|
|
256
|
+
},
|
|
257
|
+
template: `
|
|
258
|
+
<ContentDocs
|
|
259
|
+
v-bind="args"
|
|
260
|
+
v-model:active-nav-item="args.activeNavItem"
|
|
261
|
+
v-model:active-page-nav-item="args.activePageNavItem"
|
|
262
|
+
>
|
|
263
|
+
<template #docsContent>
|
|
264
|
+
<h1 style="margin-top:0">Dokumentationsseitenüberschriftohneleerzeichen</h1>
|
|
265
|
+
<p>https://example.com/an/unbroken/url/in/the/content/slot/that/would/otherwise/widen/the/middle/column</p>
|
|
266
|
+
<p>Slot content is the consumer's own markup: the wide table below scrolls in its own wrapper rather than stretching the grid.</p>
|
|
267
|
+
<div style="overflow-x: auto;">
|
|
268
|
+
<table style="min-width: 80rem; border-collapse: collapse;">
|
|
269
|
+
<tr><th style="text-align:start">Prop</th><th style="text-align:start">Type</th><th style="text-align:start">Default</th><th style="text-align:start">Description</th></tr>
|
|
270
|
+
<tr><td>docsNavItems</td><td>DocsNavItem[]</td><td>[]</td><td>Items rendered in the docs nav panel</td></tr>
|
|
271
|
+
</table>
|
|
272
|
+
</div>
|
|
273
|
+
</template>
|
|
274
|
+
</ContentDocs>
|
|
275
|
+
`,
|
|
276
|
+
});
|
|
277
|
+
StressTest.storyName = "Stress Test (Worst-Case Data)";
|
|
278
|
+
StressTest.args = {
|
|
279
|
+
docsNavItems: stressNavItems,
|
|
280
|
+
docsPageNavItems: stressPageNavItems,
|
|
281
|
+
docsNavLabel: "Dokumentationsnavigationüberschriftohneleerzeichen 😀",
|
|
282
|
+
docsPageNavLabel: "<i>Auf dieser Seite</i>",
|
|
283
|
+
docsNavAriaLabel: "Dokumentationsnavigation",
|
|
284
|
+
docsPageNavAriaLabel: "Auf dieser Seite",
|
|
285
|
+
activeNavItem: "/docs/a",
|
|
286
|
+
activePageNavItem: "/docs#two",
|
|
287
|
+
};
|
|
288
|
+
StressTest.parameters = {
|
|
289
|
+
initialCanvas: "mobileCanvas",
|
|
290
|
+
docs: {
|
|
291
|
+
story: { inline: false, height: "900px" },
|
|
292
|
+
description: {
|
|
293
|
+
story:
|
|
294
|
+
"39 docs links (unbroken German, a long URL, emoji first, HTML-like and right-to-left labels, an empty label, a one-character label, a duplicate `to`, an unknown icon name) and three page links, with an unbroken heading. Check at every canvas width: unbroken text wraps inside its link and never widens a column, right-to-left labels read and align right-to-left, markup renders as text, both links sharing `/docs/a` show as active, and the unbroken heading and URL in the content slot wrap instead of widening the middle column at tablet and desktop. The empty label renders a thin link with no accessible name: the component can't invent one, so filter empty labels out of your data. Link labels have no line-clamp token on purpose: they name their destinations.",
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { nextTick } from "vue";
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { defineComponent, h, nextTick, ref } from "vue";
|
|
3
3
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
4
4
|
import ContentDocs from "../ContentDocs.vue";
|
|
5
5
|
|
|
@@ -37,67 +37,75 @@ const navItems = [
|
|
|
37
37
|
const pageNavItems = [{ label: "Overview", to: "/one#overview" }];
|
|
38
38
|
|
|
39
39
|
describe("ContentDocs", () => {
|
|
40
|
+
let wrapper: Awaited<ReturnType<typeof mountSuspended>> | undefined;
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
wrapper?.unmount();
|
|
44
|
+
wrapper = undefined;
|
|
45
|
+
vi.restoreAllMocks();
|
|
46
|
+
});
|
|
47
|
+
|
|
40
48
|
it("mounts without error", async () => {
|
|
41
|
-
|
|
49
|
+
wrapper = await mountSuspended(ContentDocs);
|
|
42
50
|
expect(wrapper.vm).toBeTruthy();
|
|
43
51
|
});
|
|
44
52
|
|
|
45
53
|
// ─── docsNav / docsPageNav visibility ──────────────────────────────────────
|
|
46
54
|
|
|
47
|
-
it("does not render .docs-nav when docsNavItems is empty", async () => {
|
|
48
|
-
|
|
49
|
-
expect(wrapper.find(".docs-nav").exists()).toBe(false);
|
|
55
|
+
it("does not render .content-docs-nav when docsNavItems is empty", async () => {
|
|
56
|
+
wrapper = await mountSuspended(ContentDocs);
|
|
57
|
+
expect(wrapper.find(".content-docs-nav").exists()).toBe(false);
|
|
50
58
|
});
|
|
51
59
|
|
|
52
|
-
it("renders .docs-nav with a link per item when docsNavItems is provided", async () => {
|
|
53
|
-
|
|
54
|
-
const links = wrapper.find(".docs-nav").findAll("a");
|
|
60
|
+
it("renders .content-docs-nav with a link per item when docsNavItems is provided", async () => {
|
|
61
|
+
wrapper = await mountSuspended(ContentDocs, { props: { docsNavItems: navItems } });
|
|
62
|
+
const links = wrapper.find(".content-docs-nav").findAll("a");
|
|
55
63
|
expect(links).toHaveLength(2);
|
|
56
64
|
expect(links[0]?.text()).toBe("One");
|
|
57
65
|
expect(links[0]?.attributes("href")).toBe("/one");
|
|
58
66
|
});
|
|
59
67
|
|
|
60
|
-
it("does not render .docs-page-nav when docsPageNavItems is empty", async () => {
|
|
61
|
-
|
|
62
|
-
expect(wrapper.find(".docs-page-nav").exists()).toBe(false);
|
|
68
|
+
it("does not render .content-docs-page-nav when docsPageNavItems is empty", async () => {
|
|
69
|
+
wrapper = await mountSuspended(ContentDocs);
|
|
70
|
+
expect(wrapper.find(".content-docs-page-nav").exists()).toBe(false);
|
|
63
71
|
});
|
|
64
72
|
|
|
65
|
-
it("renders .docs-page-nav with a link per item when docsPageNavItems is provided", async () => {
|
|
66
|
-
|
|
67
|
-
const links = wrapper.find(".docs-page-nav").findAll("a");
|
|
73
|
+
it("renders .content-docs-page-nav with a link per item when docsPageNavItems is provided", async () => {
|
|
74
|
+
wrapper = await mountSuspended(ContentDocs, { props: { docsPageNavItems: pageNavItems } });
|
|
75
|
+
const links = wrapper.find(".content-docs-page-nav").findAll("a");
|
|
68
76
|
expect(links).toHaveLength(1);
|
|
69
77
|
expect(links[0]?.text()).toBe("Overview");
|
|
70
78
|
});
|
|
71
79
|
|
|
72
80
|
it("renders the docsContent slot", async () => {
|
|
73
|
-
|
|
81
|
+
wrapper = await mountSuspended(ContentDocs, {
|
|
74
82
|
slots: { docsContent: '<p data-testid="body">Body</p>' },
|
|
75
83
|
});
|
|
76
84
|
expect(wrapper.find('[data-testid="body"]').text()).toBe("Body");
|
|
77
85
|
});
|
|
78
86
|
|
|
79
87
|
it("places docsContent before docsNav and docsPageNav in DOM order, so a consumer's h1 is reachable before either nav panel's h3 heading (visual position is unaffected — grid-template-areas places all three, not source order)", async () => {
|
|
80
|
-
|
|
88
|
+
wrapper = await mountSuspended(ContentDocs, {
|
|
81
89
|
props: { docsNavItems: navItems, docsPageNavItems: pageNavItems },
|
|
82
90
|
slots: { docsContent: "<h1>Title</h1>" },
|
|
83
91
|
});
|
|
84
92
|
const inner = wrapper.find(".content-docs-inner").element;
|
|
85
93
|
const children = [...inner.children].map((el) => el.className);
|
|
86
|
-
expect(children).toEqual(["docs-content", "docs-nav", "docs-page-nav"]);
|
|
94
|
+
expect(children).toEqual(["content-docs-content", "content-docs-nav", "content-docs-page-nav"]);
|
|
87
95
|
});
|
|
88
96
|
|
|
89
97
|
// ─── labels ─────────────────────────────────────────────────────────────
|
|
90
98
|
|
|
91
99
|
it("defaults docsNavLabel to 'Navigation' and docsPageNavLabel to 'On this page'", async () => {
|
|
92
|
-
|
|
100
|
+
wrapper = await mountSuspended(ContentDocs, {
|
|
93
101
|
props: { docsNavItems: navItems, docsPageNavItems: pageNavItems },
|
|
94
102
|
});
|
|
95
|
-
expect(wrapper.find(".docs-nav-heading").text()).toBe("Navigation");
|
|
96
|
-
expect(wrapper.find(".docs-page-nav-heading").text()).toBe("On this page");
|
|
103
|
+
expect(wrapper.find(".content-docs-nav-heading").text()).toBe("Navigation");
|
|
104
|
+
expect(wrapper.find(".content-docs-page-nav-heading").text()).toBe("On this page");
|
|
97
105
|
});
|
|
98
106
|
|
|
99
107
|
it("uses custom docsNavLabel / docsPageNavLabel when provided", async () => {
|
|
100
|
-
|
|
108
|
+
wrapper = await mountSuspended(ContentDocs, {
|
|
101
109
|
props: {
|
|
102
110
|
docsNavItems: navItems,
|
|
103
111
|
docsPageNavItems: pageNavItems,
|
|
@@ -105,111 +113,111 @@ describe("ContentDocs", () => {
|
|
|
105
113
|
docsPageNavLabel: "Contents",
|
|
106
114
|
},
|
|
107
115
|
});
|
|
108
|
-
expect(wrapper.find(".docs-nav-heading").text()).toBe("Sections");
|
|
109
|
-
expect(wrapper.find(".docs-page-nav-heading").text()).toBe("Contents");
|
|
116
|
+
expect(wrapper.find(".content-docs-nav-heading").text()).toBe("Sections");
|
|
117
|
+
expect(wrapper.find(".content-docs-page-nav-heading").text()).toBe("Contents");
|
|
110
118
|
});
|
|
111
119
|
|
|
112
120
|
// ─── icons ──────────────────────────────────────────────────────────────
|
|
113
121
|
|
|
114
122
|
it("renders an icon on a docsNav item when item.icon is set", async () => {
|
|
115
|
-
|
|
123
|
+
wrapper = await mountSuspended(ContentDocs, {
|
|
116
124
|
props: { docsNavItems: [{ label: "One", to: "/one", icon: "lucide:home" }] },
|
|
117
125
|
});
|
|
118
|
-
expect(wrapper.find(".docs-nav-link-icon").exists()).toBe(true);
|
|
126
|
+
expect(wrapper.find(".content-docs-nav-link-icon").exists()).toBe(true);
|
|
119
127
|
});
|
|
120
128
|
|
|
121
129
|
it("does not render an icon on a docsNav item when item.icon is omitted", async () => {
|
|
122
|
-
|
|
123
|
-
expect(wrapper.find(".docs-nav-link-icon").exists()).toBe(false);
|
|
130
|
+
wrapper = await mountSuspended(ContentDocs, { props: { docsNavItems: navItems } });
|
|
131
|
+
expect(wrapper.find(".content-docs-nav-link-icon").exists()).toBe(false);
|
|
124
132
|
});
|
|
125
133
|
|
|
126
134
|
it("renders an icon on a docsPageNav item when item.icon is set", async () => {
|
|
127
|
-
|
|
135
|
+
wrapper = await mountSuspended(ContentDocs, {
|
|
128
136
|
props: { docsPageNavItems: [{ label: "Overview", to: "/one#overview", icon: "lucide:list" }] },
|
|
129
137
|
});
|
|
130
|
-
expect(wrapper.find(".docs-page-nav-link-icon").exists()).toBe(true);
|
|
138
|
+
expect(wrapper.find(".content-docs-page-nav-link-icon").exists()).toBe(true);
|
|
131
139
|
});
|
|
132
140
|
|
|
133
141
|
// ─── active item ────────────────────────────────────────────────────────
|
|
134
142
|
|
|
135
|
-
it("
|
|
136
|
-
|
|
143
|
+
it("sets aria-current='page' only on the link matching activeNavItem", async () => {
|
|
144
|
+
wrapper = await mountSuspended(ContentDocs, {
|
|
137
145
|
props: { docsNavItems: navItems, activeNavItem: "/two" },
|
|
138
146
|
});
|
|
139
|
-
const links = wrapper.find(".docs-nav").findAll("a");
|
|
140
|
-
expect(links[0]?.
|
|
141
|
-
expect(links[1]?.classes()).toContain("is-active");
|
|
147
|
+
const links = wrapper.find(".content-docs-nav").findAll("a");
|
|
148
|
+
expect(links[0]?.attributes("aria-current")).toBeUndefined();
|
|
142
149
|
expect(links[1]?.attributes("aria-current")).toBe("page");
|
|
150
|
+
expect(links[1]?.classes()).not.toContain("is-active");
|
|
143
151
|
});
|
|
144
152
|
|
|
145
153
|
it("emits update:activeNavItem when a docsNav link is clicked", async () => {
|
|
146
|
-
|
|
147
|
-
await wrapper.find(".docs-nav").findAll("a")[1]?.trigger("click");
|
|
154
|
+
wrapper = await mountSuspended(ContentDocs, { props: { docsNavItems: navItems } });
|
|
155
|
+
await wrapper.find(".content-docs-nav").findAll("a")[1]?.trigger("click");
|
|
148
156
|
expect(wrapper.emitted("update:activeNavItem")?.[0]).toEqual(["/two"]);
|
|
149
157
|
});
|
|
150
158
|
|
|
151
159
|
it("emits update:activePageNavItem when a docsPageNav link is clicked", async () => {
|
|
152
|
-
|
|
153
|
-
await wrapper.find(".docs-page-nav").find("a").trigger("click");
|
|
160
|
+
wrapper = await mountSuspended(ContentDocs, { props: { docsPageNavItems: pageNavItems } });
|
|
161
|
+
await wrapper.find(".content-docs-page-nav").find("a").trigger("click");
|
|
154
162
|
expect(wrapper.emitted("update:activePageNavItem")?.[0]).toEqual(["/one#overview"]);
|
|
155
163
|
});
|
|
156
164
|
|
|
157
165
|
// ─── closing on link click (mobile overlay behaviour) ─────────────────────
|
|
158
166
|
|
|
159
167
|
it("closes the docsNav panel when a link inside it is clicked", async () => {
|
|
160
|
-
|
|
168
|
+
wrapper = await mountSuspended(ContentDocs, { props: { docsNavItems: navItems } });
|
|
161
169
|
|
|
162
|
-
await wrapper.find(".docs-nav summary").trigger("click");
|
|
163
|
-
expect(wrapper.find(".docs-nav details").attributes("open")).toBeDefined();
|
|
170
|
+
await wrapper.find(".content-docs-nav summary").trigger("click");
|
|
171
|
+
expect(wrapper.find(".content-docs-nav details").attributes("open")).toBeDefined();
|
|
164
172
|
|
|
165
|
-
await wrapper.find(".docs-nav").findAll("a")[0]?.trigger("click");
|
|
166
|
-
expect(wrapper.find(".docs-nav details").attributes("open")).toBeUndefined();
|
|
173
|
+
await wrapper.find(".content-docs-nav").findAll("a")[0]?.trigger("click");
|
|
174
|
+
expect(wrapper.find(".content-docs-nav details").attributes("open")).toBeUndefined();
|
|
167
175
|
});
|
|
168
176
|
|
|
169
177
|
it("closes the docsPageNav panel when a link inside it is clicked", async () => {
|
|
170
|
-
|
|
178
|
+
wrapper = await mountSuspended(ContentDocs, { props: { docsPageNavItems: pageNavItems } });
|
|
171
179
|
|
|
172
|
-
await wrapper.find(".docs-page-nav summary").trigger("click");
|
|
173
|
-
expect(wrapper.find(".docs-page-nav details").attributes("open")).toBeDefined();
|
|
180
|
+
await wrapper.find(".content-docs-page-nav summary").trigger("click");
|
|
181
|
+
expect(wrapper.find(".content-docs-page-nav details").attributes("open")).toBeDefined();
|
|
174
182
|
|
|
175
|
-
await wrapper.find(".docs-page-nav").find("a").trigger("click");
|
|
176
|
-
expect(wrapper.find(".docs-page-nav details").attributes("open")).toBeUndefined();
|
|
183
|
+
await wrapper.find(".content-docs-page-nav").find("a").trigger("click");
|
|
184
|
+
expect(wrapper.find(".content-docs-page-nav details").attributes("open")).toBeUndefined();
|
|
177
185
|
});
|
|
178
186
|
|
|
179
187
|
// ─── container-width-driven forceOpened ────────────────────────────────────
|
|
180
188
|
|
|
181
189
|
describe("breakpoint-driven forceOpened", () => {
|
|
182
190
|
it("neither panel is forceOpened below 768px (mobile)", async () => {
|
|
183
|
-
|
|
191
|
+
wrapper = await mountSuspended(ContentDocs, {
|
|
184
192
|
props: { docsNavItems: navItems, docsPageNavItems: pageNavItems },
|
|
185
193
|
});
|
|
186
194
|
triggerResize(wrapper.find(".content-docs").element as HTMLElement, 500);
|
|
187
195
|
await nextTick();
|
|
188
196
|
|
|
189
|
-
expect(wrapper.find(".docs-nav .icon-wrapper").exists()).toBe(true);
|
|
190
|
-
expect(wrapper.find(".docs-page-nav .icon-wrapper").exists()).toBe(true);
|
|
197
|
+
expect(wrapper.find(".content-docs-nav .icon-wrapper").exists()).toBe(true);
|
|
198
|
+
expect(wrapper.find(".content-docs-page-nav .icon-wrapper").exists()).toBe(true);
|
|
191
199
|
});
|
|
192
200
|
|
|
193
201
|
it("only docsPageNav is forceOpened between 768px and 1023px (tablet)", async () => {
|
|
194
|
-
|
|
202
|
+
wrapper = await mountSuspended(ContentDocs, {
|
|
195
203
|
props: { docsNavItems: navItems, docsPageNavItems: pageNavItems },
|
|
196
204
|
});
|
|
197
205
|
triggerResize(wrapper.find(".content-docs").element as HTMLElement, 900);
|
|
198
206
|
await nextTick();
|
|
199
207
|
|
|
200
|
-
expect(wrapper.find(".docs-nav .icon-wrapper").classes()).not.toContain("icon-wrapper--hidden");
|
|
201
|
-
expect(wrapper.find(".docs-page-nav .icon-wrapper").classes()).toContain("icon-wrapper--hidden");
|
|
208
|
+
expect(wrapper.find(".content-docs-nav .icon-wrapper").classes()).not.toContain("icon-wrapper--hidden");
|
|
209
|
+
expect(wrapper.find(".content-docs-page-nav .icon-wrapper").classes()).toContain("icon-wrapper--hidden");
|
|
202
210
|
});
|
|
203
211
|
|
|
204
212
|
it("both panels are forceOpened at 1024px and above (desktop)", async () => {
|
|
205
|
-
|
|
213
|
+
wrapper = await mountSuspended(ContentDocs, {
|
|
206
214
|
props: { docsNavItems: navItems, docsPageNavItems: pageNavItems },
|
|
207
215
|
});
|
|
208
216
|
triggerResize(wrapper.find(".content-docs").element as HTMLElement, 1200);
|
|
209
217
|
await nextTick();
|
|
210
218
|
|
|
211
|
-
expect(wrapper.find(".docs-nav .icon-wrapper").classes()).toContain("icon-wrapper--hidden");
|
|
212
|
-
expect(wrapper.find(".docs-page-nav .icon-wrapper").classes()).toContain("icon-wrapper--hidden");
|
|
219
|
+
expect(wrapper.find(".content-docs-nav .icon-wrapper").classes()).toContain("icon-wrapper--hidden");
|
|
220
|
+
expect(wrapper.find(".content-docs-page-nav .icon-wrapper").classes()).toContain("icon-wrapper--hidden");
|
|
213
221
|
});
|
|
214
222
|
});
|
|
215
223
|
|
|
@@ -217,34 +225,92 @@ describe("ContentDocs", () => {
|
|
|
217
225
|
|
|
218
226
|
describe("panel name grouping", () => {
|
|
219
227
|
it("shares one details name between docsNav and docsPageNav on mobile", async () => {
|
|
220
|
-
|
|
228
|
+
wrapper = await mountSuspended(ContentDocs, {
|
|
221
229
|
props: { docsNavItems: navItems, docsPageNavItems: pageNavItems },
|
|
222
230
|
});
|
|
223
231
|
triggerResize(wrapper.find(".content-docs").element as HTMLElement, 500);
|
|
224
232
|
await nextTick();
|
|
225
233
|
|
|
226
|
-
const navName = wrapper.find(".docs-nav details").attributes("name");
|
|
227
|
-
const pageNavName = wrapper.find(".docs-page-nav details").attributes("name");
|
|
234
|
+
const navName = wrapper.find(".content-docs-nav details").attributes("name");
|
|
235
|
+
const pageNavName = wrapper.find(".content-docs-page-nav details").attributes("name");
|
|
228
236
|
expect(navName).toBe(pageNavName);
|
|
229
237
|
});
|
|
230
238
|
|
|
231
239
|
it("uses distinct details names on desktop, where both must stay open simultaneously", async () => {
|
|
232
|
-
|
|
240
|
+
wrapper = await mountSuspended(ContentDocs, {
|
|
233
241
|
props: { docsNavItems: navItems, docsPageNavItems: pageNavItems },
|
|
234
242
|
});
|
|
235
243
|
triggerResize(wrapper.find(".content-docs").element as HTMLElement, 1200);
|
|
236
244
|
await nextTick();
|
|
237
245
|
|
|
238
|
-
const navName = wrapper.find(".docs-nav details").attributes("name");
|
|
239
|
-
const pageNavName = wrapper.find(".docs-page-nav details").attributes("name");
|
|
246
|
+
const navName = wrapper.find(".content-docs-nav details").attributes("name");
|
|
247
|
+
const pageNavName = wrapper.find(".content-docs-page-nav details").attributes("name");
|
|
240
248
|
expect(navName).not.toBe(pageNavName);
|
|
241
249
|
});
|
|
242
250
|
});
|
|
243
251
|
|
|
252
|
+
// ─── worst-case data and markup ─────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
it("gives both styled lists role=\"list\" so Safari/VoiceOver still announces them", async () => {
|
|
255
|
+
wrapper = await mountSuspended(ContentDocs, {
|
|
256
|
+
props: { docsNavItems: navItems, docsPageNavItems: pageNavItems },
|
|
257
|
+
});
|
|
258
|
+
const lists = wrapper.findAll("ul");
|
|
259
|
+
expect(lists).toHaveLength(2);
|
|
260
|
+
for (const list of lists) expect(list.attributes("role")).toBe("list");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("renders and reorders items that share the same `to`", async () => {
|
|
264
|
+
wrapper = await mountSuspended(ContentDocs, {
|
|
265
|
+
props: {
|
|
266
|
+
docsNavItems: [
|
|
267
|
+
{ label: "First", to: "/same" },
|
|
268
|
+
{ label: "Second", to: "/same" },
|
|
269
|
+
],
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
const mounted = wrapper;
|
|
273
|
+
const labels = () => mounted.findAll(".content-docs-nav-link-label").map((el: { text: () => string }) => el.text());
|
|
274
|
+
expect(labels()).toEqual(["First", "Second"]);
|
|
275
|
+
await mounted.setProps({
|
|
276
|
+
docsNavItems: [
|
|
277
|
+
{ label: "Second", to: "/same" },
|
|
278
|
+
{ label: "First", to: "/same" },
|
|
279
|
+
{ label: "Third", to: "/same" },
|
|
280
|
+
],
|
|
281
|
+
});
|
|
282
|
+
expect(labels()).toEqual(["Second", "First", "Third"]);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("renders HTML-like labels as text", async () => {
|
|
286
|
+
wrapper = await mountSuspended(ContentDocs, {
|
|
287
|
+
props: { docsNavItems: [{ label: "<b>not bold</b>", to: "/x" }], docsNavLabel: "<i>x</i>" },
|
|
288
|
+
});
|
|
289
|
+
expect(wrapper.find(".content-docs-nav-link-label b").exists()).toBe(false);
|
|
290
|
+
expect(wrapper.find(".content-docs-nav-link-label").text()).toBe("<b>not bold</b>");
|
|
291
|
+
expect(wrapper.find(".content-docs-nav-heading i").exists()).toBe(false);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("adds and removes the content area when the docsContent slot is toggled after mount", async () => {
|
|
295
|
+
const show = ref(false);
|
|
296
|
+
const Host = defineComponent({
|
|
297
|
+
setup: () => () =>
|
|
298
|
+
h(ContentDocs, null, show.value ? { docsContent: () => h("p", { class: "body" }, "Body") } : {}),
|
|
299
|
+
});
|
|
300
|
+
wrapper = await mountSuspended(Host);
|
|
301
|
+
expect(wrapper.find(".content-docs-content").exists()).toBe(false);
|
|
302
|
+
show.value = true;
|
|
303
|
+
await nextTick();
|
|
304
|
+
expect(wrapper.find(".content-docs-content .body").exists()).toBe(true);
|
|
305
|
+
show.value = false;
|
|
306
|
+
await nextTick();
|
|
307
|
+
expect(wrapper.find(".content-docs-content").exists()).toBe(false);
|
|
308
|
+
});
|
|
309
|
+
|
|
244
310
|
// ─── styleClassPassthrough ────────────────────────────────────────────────
|
|
245
311
|
|
|
246
312
|
it("applies styleClassPassthrough classes to the root element", async () => {
|
|
247
|
-
|
|
313
|
+
wrapper = await mountSuspended(ContentDocs, {
|
|
248
314
|
props: { styleClassPassthrough: ["custom-class"] },
|
|
249
315
|
});
|
|
250
316
|
expect(wrapper.find(".content-docs").classes()).toContain("custom-class");
|
|
@@ -6,6 +6,9 @@ All `--display-dialog-*` tokens are the stable override surface. Because dialogs
|
|
|
6
6
|
elements — not inline components — the recommended approach is to set tokens once in a **global CSS
|
|
7
7
|
file** rather than per-instance via `styleClassPassthrough`.
|
|
8
8
|
|
|
9
|
+
Private tokens (`--_transition-duration`, `--_inner-border-radius`, `--_inner-border`,
|
|
10
|
+
`--_inner-outline`) are not public API: set the public token each one reads instead.
|
|
11
|
+
|
|
9
12
|
### Overlay
|
|
10
13
|
|
|
11
14
|
| Token | Default | Controls |
|
|
@@ -36,6 +39,8 @@ file** rather than per-instance via `styleClassPassthrough`.
|
|
|
36
39
|
| Token | Default | Controls |
|
|
37
40
|
|---|---|---|
|
|
38
41
|
| `--display-dialog-header-padding` | `1.2rem` | Header area padding |
|
|
42
|
+
| `--display-dialog-header-gap` | `1.2rem` | Space between the title and the close button |
|
|
43
|
+
| `--display-dialog-header-accent-color` | `var(--theme-accent)` | Bottom border and close-icon colour when `theme` is set |
|
|
39
44
|
| `--display-dialog-header-button-margin` | `0` | Close button margin |
|
|
40
45
|
| `--display-dialog-header-button-padding` | `0.4rem` | Close button padding |
|
|
41
46
|
| `--display-dialog-header-button-border` | `0.1rem solid transparent` | Close button border (resting) |
|
|
@@ -46,6 +51,14 @@ file** rather than per-instance via `styleClassPassthrough`.
|
|
|
46
51
|
| `--display-dialog-header-button-icon-color` | `var(--colour-text-default)` | Close button icon colour |
|
|
47
52
|
| `--display-dialog-header-button-icon-size` | `2.4rem` | Close button icon size |
|
|
48
53
|
|
|
54
|
+
The close button is an icon-only `InputButton` (`variant="inline"`), so it renders as
|
|
55
|
+
`.display-dialog-close.input-button`; the `--display-dialog-header-button-*` tokens style it.
|
|
56
|
+
|
|
57
|
+
> Changed 2026-10-18: the close button was a raw `<button>` and is now an `InputButton`. Its class
|
|
58
|
+
> (`.display-dialog-close`) and tokens are unchanged, so existing overrides still match. A set `theme` now tints the
|
|
59
|
+
> close icon as documented: the tint rule used to lose to the resting icon colour and never applied.
|
|
60
|
+
> `--display-dialog-header-gap` and `--display-dialog-header-accent-color` are new.
|
|
61
|
+
|
|
49
62
|
### Content & footer
|
|
50
63
|
|
|
51
64
|
| Token | Default | Controls |
|
|
@@ -64,7 +77,7 @@ file** rather than per-instance via `styleClassPassthrough`.
|
|
|
64
77
|
| `[justify-dialog="start|center|end"]` | `.display-dialog` | Horizontal placement of the panel |
|
|
65
78
|
| `[align-dialog="start|center|end"]` | `.display-dialog` | Vertical placement of the panel |
|
|
66
79
|
| `.dialog`, `.modal`, `.confirm`, `.alert`, `.fullscreen` | `.display-dialog-inner` | The resolved `variant` |
|
|
67
|
-
| `[data-theme]` | `.display-dialog-header` | A `theme` is set: adds a
|
|
80
|
+
| `[data-theme]` | `.display-dialog-header` | A `theme` is set: adds a bottom border and close-icon tint in `--display-dialog-header-accent-color` |
|
|
68
81
|
| `.allow-content-scroll` | `.display-dialog-content` | `allow-content-scroll` is on |
|
|
69
82
|
|
|
70
83
|
Inner classes: `.display-dialog-inner` (panel), `.display-dialog-header`,
|
|
@@ -76,16 +89,28 @@ Inner classes: `.display-dialog-inner` (panel), `.display-dialog-header`,
|
|
|
76
89
|
> consumer's own unlayered `.header`/`.footer` CSS silently restyled it. Update any overrides
|
|
77
90
|
> written against the old names.
|
|
78
91
|
|
|
92
|
+
## Sizing
|
|
93
|
+
|
|
94
|
+
The panel never grows past the overlay: `max-inline-size` and `max-block-size` are `100%`, the
|
|
95
|
+
rows are `auto minmax(0, 1fr) auto`, and the header, content and footer each keep their own row
|
|
96
|
+
(so with no content the footer doesn't stretch into the middle). The title column and content area
|
|
97
|
+
wrap long unbroken text (`overflow-wrap: anywhere`), and the footer wraps its buttons onto further
|
|
98
|
+
rows. Content taller than the panel is clipped, or scrolls with `allow-content-scroll`; the header
|
|
99
|
+
and footer stay visible either way.
|
|
100
|
+
|
|
101
|
+
> Changed 2026-10-18: `modal` used to grow with its content past the viewport, a long title pushed
|
|
102
|
+
> the close button out of the panel, and footer buttons never wrapped.
|
|
103
|
+
|
|
79
104
|
---
|
|
80
105
|
|
|
81
106
|
## Global theming
|
|
82
107
|
|
|
83
108
|
Create `assets/styles/setup/07.components/display-dialog.css` in the consuming app and set tokens
|
|
84
|
-
on `:
|
|
109
|
+
on `:where(html)`. This applies to every `DisplayDialog` across the site.
|
|
85
110
|
|
|
86
111
|
```css
|
|
87
112
|
/* assets/styles/setup/07.components/display-dialog.css */
|
|
88
|
-
:
|
|
113
|
+
:where(html) {
|
|
89
114
|
--display-dialog-backdrop-background: rgba(0, 0, 0, 0.6);
|
|
90
115
|
--display-dialog-inner-border-radius: 1.2rem;
|
|
91
116
|
--display-dialog-inner-background: var(--brand-surface);
|