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
|
@@ -7,134 +7,131 @@ const meta: Meta<typeof LinkText> = {
|
|
|
7
7
|
argTypes: {
|
|
8
8
|
to: {
|
|
9
9
|
control: "text",
|
|
10
|
-
description: "Link destination (internal path or external URL)",
|
|
10
|
+
description: "Link destination (internal path or external URL).",
|
|
11
|
+
table: { category: "Basic" },
|
|
11
12
|
},
|
|
12
13
|
linkText: {
|
|
13
14
|
control: "text",
|
|
14
|
-
description: "Visible link label",
|
|
15
|
+
description: "Visible link label. It should name the destination, so it's never clamped.",
|
|
16
|
+
table: { category: "Content" },
|
|
15
17
|
},
|
|
16
18
|
external: {
|
|
17
19
|
control: "boolean",
|
|
18
|
-
description: "Force external link handling",
|
|
19
|
-
|
|
20
|
+
description: "Force external link handling on an internal-looking path.",
|
|
21
|
+
table: { category: "Basic" },
|
|
20
22
|
},
|
|
21
23
|
target: {
|
|
22
24
|
control: { type: "select" },
|
|
23
25
|
options: [undefined, "_blank", "_self"],
|
|
24
|
-
description: "Link target
|
|
25
|
-
|
|
26
|
-
styleClassPassthrough: {
|
|
27
|
-
control: "object",
|
|
28
|
-
description: "Additional classes",
|
|
29
|
-
defaultValue: [],
|
|
26
|
+
description: "Link `target`. NuxtLink adds `rel=\"noopener noreferrer\"` for an external `_blank`.",
|
|
27
|
+
table: { category: "Basic" },
|
|
30
28
|
},
|
|
29
|
+
styleClassPassthrough: { table: { disable: true } },
|
|
31
30
|
},
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export default meta;
|
|
35
|
-
type Story = StoryObj<typeof LinkText>;
|
|
36
|
-
|
|
37
|
-
export const Default: Story = {
|
|
38
31
|
args: {
|
|
39
32
|
to: "/",
|
|
40
33
|
linkText: "Read More",
|
|
41
34
|
external: false,
|
|
42
|
-
styleClassPassthrough: [],
|
|
43
35
|
},
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
36
|
+
parameters: {
|
|
37
|
+
docs: {
|
|
38
|
+
description: {
|
|
39
|
+
component:
|
|
40
|
+
"A `NuxtLink` styled as a standalone text link. `to` and `linkText` are required; `left` and `right` slots take icons, which stay centred on the label. Colours, underline and gap come from `--link-text-*` tokens.",
|
|
41
|
+
},
|
|
48
42
|
},
|
|
49
|
-
template: `<LinkText v-bind="args" />`,
|
|
50
|
-
}),
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export const WithLeftIcon: Story = {
|
|
54
|
-
args: {
|
|
55
|
-
to: "/",
|
|
56
|
-
linkText: "Go Back",
|
|
57
|
-
styleClassPassthrough: [],
|
|
58
43
|
},
|
|
59
|
-
render: (args) => ({
|
|
60
|
-
components: { LinkText },
|
|
61
|
-
setup() {
|
|
62
|
-
return { args };
|
|
63
|
-
},
|
|
64
|
-
template: `
|
|
65
|
-
<LinkText v-bind="args">
|
|
66
|
-
<template #left>
|
|
67
|
-
<Icon name="lucide:arrow-left" />
|
|
68
|
-
</template>
|
|
69
|
-
</LinkText>
|
|
70
|
-
`,
|
|
71
|
-
}),
|
|
72
44
|
};
|
|
73
45
|
|
|
74
|
-
export
|
|
75
|
-
|
|
76
|
-
to: "/",
|
|
77
|
-
linkText: "Learn More",
|
|
78
|
-
styleClassPassthrough: [],
|
|
79
|
-
},
|
|
80
|
-
render: (args) => ({
|
|
81
|
-
components: { LinkText },
|
|
82
|
-
setup() {
|
|
83
|
-
return { args };
|
|
84
|
-
},
|
|
85
|
-
template: `
|
|
86
|
-
<LinkText v-bind="args">
|
|
87
|
-
<template #right>
|
|
88
|
-
<Icon name="lucide:arrow-right" />
|
|
89
|
-
</template>
|
|
90
|
-
</LinkText>
|
|
91
|
-
`,
|
|
92
|
-
}),
|
|
93
|
-
};
|
|
46
|
+
export default meta;
|
|
47
|
+
type Story = StoryObj<typeof LinkText>;
|
|
94
48
|
|
|
95
|
-
|
|
96
|
-
args
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
49
|
+
const withIcons = (left: string | null, right: string | null) => `
|
|
50
|
+
<LinkText v-bind="args">
|
|
51
|
+
${left ? `<template #left><Icon name="${left}" /></template>` : ""}
|
|
52
|
+
${right ? `<template #right><Icon name="${right}" /></template>` : ""}
|
|
53
|
+
</LinkText>
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const story = (template: string, description: string, args: Story["args"] = {}): Story => ({
|
|
57
|
+
args,
|
|
58
|
+
render: (storyArgs) => ({
|
|
102
59
|
components: { LinkText },
|
|
103
60
|
setup() {
|
|
104
|
-
return { args };
|
|
61
|
+
return { args: storyArgs };
|
|
105
62
|
},
|
|
106
|
-
template
|
|
107
|
-
<LinkText v-bind="args">
|
|
108
|
-
<template #left>
|
|
109
|
-
<Icon name="lucide:sparkles" />
|
|
110
|
-
</template>
|
|
111
|
-
<template #right>
|
|
112
|
-
<Icon name="lucide:arrow-right" />
|
|
113
|
-
</template>
|
|
114
|
-
</LinkText>
|
|
115
|
-
`,
|
|
63
|
+
template,
|
|
116
64
|
}),
|
|
117
|
-
}
|
|
65
|
+
parameters: { docs: { description: { story: description } } },
|
|
66
|
+
});
|
|
118
67
|
|
|
119
|
-
export const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
68
|
+
export const Default = story(withIcons(null, null), "A plain link. Use the Controls panel to change the destination, label and target.");
|
|
69
|
+
|
|
70
|
+
export const WithLeftIcon = story(withIcons("lucide:arrow-left", null), "A `left` slot icon for a back or previous link.", {
|
|
71
|
+
linkText: "Go Back",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export const WithRightIcon = story(withIcons(null, "lucide:arrow-right"), "A `right` slot icon for a forward or more link.", {
|
|
75
|
+
linkText: "Learn More",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export const WithBothIcons = story(withIcons("lucide:sparkles", "lucide:arrow-right"), "Both slots. Left icon, label, then right icon.", {
|
|
79
|
+
linkText: "Explore",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const ExternalLink = story(
|
|
83
|
+
withIcons(null, "lucide:external-link"),
|
|
84
|
+
"An external link opening in a new tab, with an icon that says so.",
|
|
85
|
+
{ to: "https://nuxt.com", linkText: "Nuxt Docs", external: true, target: "_blank" }
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
export const StressTest: Story = {
|
|
89
|
+
name: "Stress Test (Worst-Case Data)",
|
|
90
|
+
render: () => ({
|
|
128
91
|
components: { LinkText },
|
|
129
|
-
setup() {
|
|
130
|
-
return { args };
|
|
131
|
-
},
|
|
132
92
|
template: `
|
|
133
|
-
<
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
93
|
+
<div style="display: grid; gap: 2.4rem; padding: 2.4rem; font-size: 1.6rem;">
|
|
94
|
+
<p style="margin: 0; font-size: 1.4rem;">Long German label, an unbroken label and a long URL as the label, each with icons, in an 18rem column.</p>
|
|
95
|
+
<div style="display: grid; gap: 1.6rem; justify-items: start; inline-size: 18rem; outline: 0.1rem dashed currentColor;">
|
|
96
|
+
<LinkText to="/" link-text="Alle Lieferbedingungen und Rückgaberichtlinien ansehen">
|
|
97
|
+
<template #right><Icon name="lucide:arrow-right" /></template>
|
|
98
|
+
</LinkText>
|
|
99
|
+
<LinkText to="/" link-text="Lieferbedingungenundrückgaberichtlinienansehen">
|
|
100
|
+
<template #left><Icon name="lucide:arrow-left" /></template>
|
|
101
|
+
<template #right><Icon name="lucide:arrow-right" /></template>
|
|
102
|
+
</LinkText>
|
|
103
|
+
<LinkText to="https://example.invalid/a/very/long/unbroken/url" link-text="https://example.invalid/a/very/long/unbroken/url" external target="_blank">
|
|
104
|
+
<template #right><Icon name="lucide:external-link" /></template>
|
|
105
|
+
</LinkText>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<p style="margin: 0; font-size: 1.4rem;">Emoji first, HTML-like text (shown as text), right-to-left with a right icon, and a single character.</p>
|
|
109
|
+
<div style="display: flex; flex-wrap: wrap; gap: 2.4rem; align-items: center;">
|
|
110
|
+
<LinkText to="/" link-text="😀 Emoji first" />
|
|
111
|
+
<LinkText to="/" link-text="<b>not bold</b>" />
|
|
112
|
+
<LinkText to="/" link-text="اقرأ المزيد" dir="rtl">
|
|
113
|
+
<template #right><Icon name="lucide:arrow-left" /></template>
|
|
114
|
+
</LinkText>
|
|
115
|
+
<LinkText to="/" link-text="A" />
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<p style="margin: 0; font-size: 1.4rem;">Icon-only link: empty <code>link-text</code>, named with <code>aria-label</code>. Tab to it to check the focus ring.</p>
|
|
119
|
+
<LinkText to="/" link-text="" aria-label="Back to home">
|
|
120
|
+
<template #left><Icon name="lucide:house" /></template>
|
|
121
|
+
</LinkText>
|
|
122
|
+
|
|
123
|
+
<p style="margin: 0; font-size: 1.4rem;">Inside a sentence: the link sits on the text line.</p>
|
|
124
|
+
<p style="margin: 0;">Our full price list is on the <LinkText to="/" link-text="services page" /> and updated monthly.</p>
|
|
125
|
+
</div>
|
|
138
126
|
`,
|
|
139
127
|
}),
|
|
128
|
+
parameters: {
|
|
129
|
+
controls: { disable: true },
|
|
130
|
+
docs: {
|
|
131
|
+
description: {
|
|
132
|
+
story:
|
|
133
|
+
"Worst-case labels. Check: long and unbroken labels and URLs wrap inside the dashed 18rem column with their icons centred on the wrapped text; emoji and HTML-like text render as text; right-to-left puts the right slot on the inline end; an icon-only link keeps a visible focus ring; an inline link sits on the line of text.",
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
140
137
|
};
|
|
@@ -1,177 +1,168 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
2
2
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
3
|
import LinkText from "../LinkText.vue";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
type LinkTextProps = InstanceType<typeof LinkText>["$props"];
|
|
6
|
+
|
|
7
|
+
const mounted: Awaited<ReturnType<typeof mountSuspended>>[] = [];
|
|
8
|
+
|
|
9
|
+
const mountLink = async (props: Partial<LinkTextProps> = {}, slots: Record<string, string> = {}) => {
|
|
10
|
+
const wrapper = await mountSuspended(LinkText, {
|
|
11
|
+
props: { to: "/about", linkText: "About Us", ...props } as LinkTextProps,
|
|
12
|
+
slots,
|
|
13
|
+
});
|
|
14
|
+
mounted.push(wrapper);
|
|
15
|
+
return wrapper;
|
|
16
|
+
};
|
|
7
17
|
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
mounted.splice(0).forEach((wrapper) => wrapper.unmount());
|
|
20
|
+
vi.restoreAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("LinkText", () => {
|
|
8
24
|
it("mounts without error", async () => {
|
|
9
|
-
const wrapper = await
|
|
10
|
-
props: { to: "/about", linkText: "About Us" },
|
|
11
|
-
});
|
|
25
|
+
const wrapper = await mountLink();
|
|
12
26
|
expect(wrapper.vm).toBeTruthy();
|
|
13
27
|
});
|
|
14
28
|
|
|
15
29
|
// ─── Snapshots ───────────────────────────────────────────────────────────
|
|
16
30
|
|
|
17
31
|
it("renders correct HTML structure with default props", async () => {
|
|
18
|
-
const wrapper = await
|
|
19
|
-
props: { to: "/about", linkText: "About Us" },
|
|
20
|
-
});
|
|
32
|
+
const wrapper = await mountLink();
|
|
21
33
|
expect(wrapper.html()).toMatchSnapshot();
|
|
22
34
|
});
|
|
23
35
|
|
|
24
36
|
it("renders correct HTML structure with left slot", async () => {
|
|
25
|
-
const wrapper = await
|
|
26
|
-
props: { to: "/about", linkText: "About Us" },
|
|
27
|
-
slots: { left: "<svg data-testid='icon-left' />" },
|
|
28
|
-
});
|
|
37
|
+
const wrapper = await mountLink({}, { left: "<svg data-testid='icon-left' />" });
|
|
29
38
|
expect(wrapper.html()).toMatchSnapshot();
|
|
30
39
|
});
|
|
31
40
|
|
|
32
41
|
it("renders correct HTML structure with right slot", async () => {
|
|
33
|
-
const wrapper = await
|
|
34
|
-
props: { to: "/about", linkText: "About Us" },
|
|
35
|
-
slots: { right: "<svg data-testid='icon-right' />" },
|
|
36
|
-
});
|
|
42
|
+
const wrapper = await mountLink({}, { right: "<svg data-testid='icon-right' />" });
|
|
37
43
|
expect(wrapper.html()).toMatchSnapshot();
|
|
38
44
|
});
|
|
39
45
|
|
|
40
46
|
it("renders correct HTML structure with both slots", async () => {
|
|
41
|
-
const wrapper = await
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
right: "<svg data-testid='icon-right' />",
|
|
46
|
-
},
|
|
47
|
-
});
|
|
47
|
+
const wrapper = await mountLink(
|
|
48
|
+
{},
|
|
49
|
+
{ left: "<svg data-testid='icon-left' />", right: "<svg data-testid='icon-right' />" }
|
|
50
|
+
);
|
|
48
51
|
expect(wrapper.html()).toMatchSnapshot();
|
|
49
52
|
});
|
|
50
53
|
|
|
51
54
|
it("renders correct HTML structure with all props set", async () => {
|
|
52
|
-
const wrapper = await
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
styleClassPassthrough: ["custom-class"],
|
|
59
|
-
},
|
|
55
|
+
const wrapper = await mountLink({
|
|
56
|
+
to: "https://example.com",
|
|
57
|
+
linkText: "Visit Us",
|
|
58
|
+
external: true,
|
|
59
|
+
target: "_blank",
|
|
60
|
+
styleClassPassthrough: ["custom-class"],
|
|
60
61
|
});
|
|
61
62
|
expect(wrapper.html()).toMatchSnapshot();
|
|
62
63
|
});
|
|
63
64
|
|
|
64
|
-
// ───
|
|
65
|
+
// ─── Root and label ──────────────────────────────────────────────────────
|
|
65
66
|
|
|
66
67
|
it("always has the link-text class", async () => {
|
|
67
|
-
const wrapper = await
|
|
68
|
-
props: { to: "/", linkText: "Home" },
|
|
69
|
-
});
|
|
68
|
+
const wrapper = await mountLink({ to: "/", linkText: "Home" });
|
|
70
69
|
expect(wrapper.classes()).toContain("link-text");
|
|
71
70
|
});
|
|
72
71
|
|
|
73
|
-
// ─── linkText ────────────────────────────────────────────────────────────
|
|
74
|
-
|
|
75
72
|
it("renders the linkText in a label span", async () => {
|
|
76
|
-
const wrapper = await
|
|
77
|
-
props: { to: "/", linkText: "Read More" },
|
|
78
|
-
});
|
|
73
|
+
const wrapper = await mountLink({ linkText: "Read More" });
|
|
79
74
|
expect(wrapper.find(".link-text__label").text()).toBe("Read More");
|
|
80
75
|
});
|
|
81
76
|
|
|
82
77
|
it("renders linkText with special characters", async () => {
|
|
83
|
-
const wrapper = await
|
|
84
|
-
props: { to: "/", linkText: "Édition & Co." },
|
|
85
|
-
});
|
|
78
|
+
const wrapper = await mountLink({ linkText: "Édition & Co." });
|
|
86
79
|
expect(wrapper.find(".link-text__label").text()).toBe("Édition & Co.");
|
|
87
80
|
});
|
|
88
81
|
|
|
82
|
+
it("renders HTML-like linkText as text", async () => {
|
|
83
|
+
const wrapper = await mountLink({ linkText: "<b>not bold</b>" });
|
|
84
|
+
expect(wrapper.find("b").exists()).toBe(false);
|
|
85
|
+
expect(wrapper.find(".link-text__label").text()).toBe("<b>not bold</b>");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("updates the label when linkText changes after mount", async () => {
|
|
89
|
+
const wrapper = await mountLink({ linkText: "Before" });
|
|
90
|
+
await wrapper.setProps({ linkText: "After" });
|
|
91
|
+
expect(wrapper.find(".link-text__label").text()).toBe("After");
|
|
92
|
+
});
|
|
93
|
+
|
|
89
94
|
// ─── Slots ───────────────────────────────────────────────────────────────
|
|
90
95
|
|
|
91
96
|
it("renders no icon spans when no slots provided", async () => {
|
|
92
|
-
const wrapper = await
|
|
93
|
-
props: { to: "/", linkText: "No Icons" },
|
|
94
|
-
});
|
|
97
|
+
const wrapper = await mountLink();
|
|
95
98
|
expect(wrapper.find(".link-text__icon--left").exists()).toBe(false);
|
|
96
99
|
expect(wrapper.find(".link-text__icon--right").exists()).toBe(false);
|
|
97
100
|
});
|
|
98
101
|
|
|
99
102
|
it("renders left icon span when left slot is provided", async () => {
|
|
100
|
-
const wrapper = await
|
|
101
|
-
props: { to: "/", linkText: "With Left Icon" },
|
|
102
|
-
slots: { left: "<svg />" },
|
|
103
|
-
});
|
|
103
|
+
const wrapper = await mountLink({}, { left: "<svg />" });
|
|
104
104
|
expect(wrapper.find(".link-text__icon--left").exists()).toBe(true);
|
|
105
105
|
expect(wrapper.find(".link-text__icon--right").exists()).toBe(false);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
it("renders right icon span when right slot is provided", async () => {
|
|
109
|
-
const wrapper = await
|
|
110
|
-
props: { to: "/", linkText: "With Right Icon" },
|
|
111
|
-
slots: { right: "<svg />" },
|
|
112
|
-
});
|
|
109
|
+
const wrapper = await mountLink({}, { right: "<svg />" });
|
|
113
110
|
expect(wrapper.find(".link-text__icon--left").exists()).toBe(false);
|
|
114
111
|
expect(wrapper.find(".link-text__icon--right").exists()).toBe(true);
|
|
115
112
|
});
|
|
116
113
|
|
|
117
|
-
it("renders
|
|
118
|
-
const wrapper = await
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
114
|
+
it("renders left icon before label and right icon after label", async () => {
|
|
115
|
+
const wrapper = await mountLink({}, { left: "<span class='l' />", right: "<span class='r' />" });
|
|
116
|
+
const children = Array.from((wrapper.element as HTMLElement).children);
|
|
117
|
+
expect(children.map((child) => child.classList[1] ?? child.classList[0])).toEqual([
|
|
118
|
+
"link-text__icon--left",
|
|
119
|
+
"link-text__label",
|
|
120
|
+
"link-text__icon--right",
|
|
121
|
+
]);
|
|
124
122
|
});
|
|
125
123
|
|
|
126
|
-
// ───
|
|
124
|
+
// ─── Link attributes ─────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
it("renders the to prop as href", async () => {
|
|
127
|
+
const wrapper = await mountLink({ to: "/services" });
|
|
128
|
+
expect(wrapper.attributes("href")).toBe("/services");
|
|
129
|
+
});
|
|
127
130
|
|
|
128
131
|
it("passes target prop to NuxtLink", async () => {
|
|
132
|
+
const wrapper = await mountLink({ to: "https://example.com", target: "_blank" });
|
|
133
|
+
expect(wrapper.attributes("target")).toBe("_blank");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("adds rel noopener to an external link opening in a new tab", async () => {
|
|
137
|
+
const wrapper = await mountLink({ to: "https://example.com", external: true, target: "_blank" });
|
|
138
|
+
expect(wrapper.attributes("rel")).toContain("noopener");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("forwards an aria-label to the link for an icon-only link", async () => {
|
|
129
142
|
const wrapper = await mountSuspended(LinkText, {
|
|
130
|
-
props: { to: "
|
|
143
|
+
props: { to: "/", linkText: "" },
|
|
144
|
+
attrs: { "aria-label": "Home" },
|
|
145
|
+
slots: { left: "<svg />" },
|
|
131
146
|
});
|
|
132
|
-
|
|
147
|
+
mounted.push(wrapper);
|
|
148
|
+
expect(wrapper.attributes("aria-label")).toBe("Home");
|
|
133
149
|
});
|
|
134
150
|
|
|
135
151
|
// ─── styleClassPassthrough ───────────────────────────────────────────────
|
|
136
152
|
|
|
137
153
|
it("applies a styleClassPassthrough array", async () => {
|
|
138
|
-
const wrapper = await
|
|
139
|
-
props: {
|
|
140
|
-
to: "/",
|
|
141
|
-
linkText: "Styled",
|
|
142
|
-
styleClassPassthrough: ["class-a", "class-b"],
|
|
143
|
-
},
|
|
144
|
-
});
|
|
154
|
+
const wrapper = await mountLink({ styleClassPassthrough: ["class-a", "class-b"] });
|
|
145
155
|
expect(wrapper.classes()).toContain("class-a");
|
|
146
156
|
expect(wrapper.classes()).toContain("class-b");
|
|
147
157
|
});
|
|
148
158
|
|
|
149
159
|
it("applies a single styleClassPassthrough string", async () => {
|
|
150
|
-
const wrapper = await
|
|
151
|
-
props: { to: "/", linkText: "Styled", styleClassPassthrough: "solo-class" },
|
|
152
|
-
});
|
|
160
|
+
const wrapper = await mountLink({ styleClassPassthrough: "solo-class" });
|
|
153
161
|
expect(wrapper.classes()).toContain("solo-class");
|
|
154
162
|
});
|
|
155
163
|
|
|
156
|
-
// ─── DOM order ───────────────────────────────────────────────────────────
|
|
157
|
-
|
|
158
|
-
it("renders left icon before label and right icon after label", async () => {
|
|
159
|
-
const wrapper = await mountSuspended(LinkText, {
|
|
160
|
-
props: { to: "/", linkText: "Ordered" },
|
|
161
|
-
slots: { left: "<span class='l' />", right: "<span class='r' />" },
|
|
162
|
-
});
|
|
163
|
-
const children = wrapper.element.children;
|
|
164
|
-
expect(children[0].classList.contains("link-text__icon--left")).toBe(true);
|
|
165
|
-
expect(children[1].classList.contains("link-text__label")).toBe(true);
|
|
166
|
-
expect(children[2].classList.contains("link-text__icon--right")).toBe(true);
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
describe("LinkText styleClassPassthrough reactivity", () => {
|
|
171
164
|
it("updates classes when styleClassPassthrough changes after mount", async () => {
|
|
172
|
-
const wrapper = await
|
|
173
|
-
props: { to: "/about", linkText: "About", styleClassPassthrough: ["original"] },
|
|
174
|
-
});
|
|
165
|
+
const wrapper = await mountLink({ styleClassPassthrough: ["original"] });
|
|
175
166
|
await wrapper.setProps({ styleClassPassthrough: ["updated"] });
|
|
176
167
|
expect(wrapper.classes()).not.toContain("original");
|
|
177
168
|
expect(wrapper.classes()).toContain("updated");
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# DisplayTooltipRich — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
> Renamed 2026-10-18 from `DisplayTooltipDefined`. Every `--display-tooltip-defined-*` token is now
|
|
4
|
+
> `--display-tooltip-rich-*` (same suffixes and defaults), and the `.display-tooltip-defined*`
|
|
5
|
+
> classes are now `.display-tooltip-rich*`. Find and replace `display-tooltip-defined` with
|
|
6
|
+
> `display-tooltip-rich` in your CSS.
|
|
7
|
+
|
|
8
|
+
## Public token API
|
|
9
|
+
|
|
10
|
+
No private `--_` tokens of its own. This component renders `DisplayTooltip`, so every
|
|
11
|
+
`--display-tooltip-*` token (trigger, popover panel, close button) applies too: see
|
|
12
|
+
`DisplayTooltip`'s CONSUMER-STYLING.md.
|
|
13
|
+
|
|
14
|
+
### Layout
|
|
15
|
+
|
|
16
|
+
| Token | Default | Controls |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| `--display-tooltip-rich-content-gap` | `1.2rem` | Gap between title/body/action/close-button |
|
|
19
|
+
| `--display-tooltip-rich-close-button-margin-block` | `1.2rem 0` | Space above the close button, on top of the gap |
|
|
20
|
+
|
|
21
|
+
### Title (`contentText.tooltipTitle`)
|
|
22
|
+
|
|
23
|
+
| Token | Default | Controls |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `--display-tooltip-rich-title-colour` | `inherit` | Text colour |
|
|
26
|
+
| `--display-tooltip-rich-title-margin-block` | `0 0` | Block margin |
|
|
27
|
+
| `--display-tooltip-rich-title-font-size` | `1.6rem` | Font size |
|
|
28
|
+
| `--display-tooltip-rich-title-font-weight` | `700` | Font weight |
|
|
29
|
+
| `--display-tooltip-rich-title-line-height` | `120%` | Line height |
|
|
30
|
+
| `--display-tooltip-rich-title-letter-spacing` | `normal` | Letter spacing |
|
|
31
|
+
| `--display-tooltip-rich-title-line-clamp` | `none` | Line cap with ellipsis (`1` is single-line ellipsis) |
|
|
32
|
+
|
|
33
|
+
### Body (`contentText.tooltipContent`)
|
|
34
|
+
|
|
35
|
+
| Token | Default | Controls |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `--display-tooltip-rich-body-colour` | `inherit` | Text colour |
|
|
38
|
+
| `--display-tooltip-rich-body-margin-block` | `0 0` | Block margin |
|
|
39
|
+
| `--display-tooltip-rich-body-font-size` | `1.3rem` | Font size |
|
|
40
|
+
| `--display-tooltip-rich-body-font-weight` | `inherit` | Font weight |
|
|
41
|
+
| `--display-tooltip-rich-body-line-height` | `140%` | Line height |
|
|
42
|
+
| `--display-tooltip-rich-body-letter-spacing` | `normal` | Letter spacing |
|
|
43
|
+
| `--display-tooltip-rich-body-line-clamp` | `none` | Line cap with ellipsis |
|
|
44
|
+
|
|
45
|
+
### Action (`contentText.tooltipAction`)
|
|
46
|
+
|
|
47
|
+
| Token | Default | Controls |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `--display-tooltip-rich-action-colour` | `inherit` | Text colour |
|
|
50
|
+
| `--display-tooltip-rich-action-margin-block` | `0 0` | Block margin |
|
|
51
|
+
| `--display-tooltip-rich-action-font-size` | `1.4rem` | Font size |
|
|
52
|
+
| `--display-tooltip-rich-action-font-weight` | `700` | Font weight |
|
|
53
|
+
| `--display-tooltip-rich-action-line-height` | `130%` | Line height |
|
|
54
|
+
| `--display-tooltip-rich-action-letter-spacing` | `normal` | Letter spacing |
|
|
55
|
+
| `--display-tooltip-rich-action-line-clamp` | `none` | Line cap with ellipsis |
|
|
56
|
+
|
|
57
|
+
The three `*-colour` tokens default to `inherit`, which resolves to the panel's own
|
|
58
|
+
`--display-tooltip-popover-text-colour`: override that for a uniform colour change, or one of these
|
|
59
|
+
three for a single field. Clamped text stays in the DOM, so screen readers still read all of it;
|
|
60
|
+
the panel has nowhere else to show the rest, which is why the clamps default to `none`.
|
|
61
|
+
|
|
62
|
+
> Changed 2026-10-18: the three `*-line-clamp` tokens are new. Long unbroken text in every field
|
|
63
|
+
> now wraps (`overflow-wrap: anywhere`).
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## State hooks
|
|
68
|
+
|
|
69
|
+
Inner classes: `.display-tooltip-rich` (on the root, next to `.display-tooltip`),
|
|
70
|
+
`.display-tooltip-rich-content`, `.display-tooltip-rich-title`,
|
|
71
|
+
`.display-tooltip-rich-body`, `.display-tooltip-rich-action`, and the close button
|
|
72
|
+
`.display-tooltip-close-button.input-button`. The panel's open and placement hooks are
|
|
73
|
+
`DisplayTooltip`'s.
|
|
74
|
+
|
|
75
|
+
> Changed 2026-10-18: `.popover-content-defined`, `.tooltip-title`, `.tooltip-body` and
|
|
76
|
+
> `.tooltip-action` are now `.display-tooltip-rich-content`, `-title`, `-body` and `-action`
|
|
77
|
+
> (the old names were generic enough for a consumer's own CSS to match them). The undefined
|
|
78
|
+
> `subtitle-sm`, `body-sm` and `input-value` classes on the fields were removed. The close button is
|
|
79
|
+
> now an `InputButton` (`variant="inline"`) that keeps the `.display-tooltip-close-button` class and
|
|
80
|
+
> its tokens, and its right alignment and top margin now apply: `DisplayTooltip`'s close-button
|
|
81
|
+
> reset used to override them, so the button stretched the full panel width.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Global theming
|
|
86
|
+
|
|
87
|
+
```css
|
|
88
|
+
:where(html) {
|
|
89
|
+
--display-tooltip-rich-title-colour: var(--brand-accent);
|
|
90
|
+
--display-tooltip-rich-title-font-size: 1.8rem;
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Local overrides
|
|
97
|
+
|
|
98
|
+
Set the tokens above on an element you own (a page or section class, or a class added with
|
|
99
|
+
`:style-class-passthrough`): they inherit down into the component. Keep the block **unlayered** (no
|
|
100
|
+
`@layer` wrapper) so it beats the library's `@layer components`. If your own file uses
|
|
101
|
+
`<style scoped>`, tokens set on your element still work, but selectors that reach inside the component need
|
|
102
|
+
`:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Class passthrough
|
|
107
|
+
|
|
108
|
+
`style-class-passthrough` is forwarded to `DisplayTooltip`'s root, alongside the
|
|
109
|
+
`display-tooltip-rich` class this component always adds, so tokens set on a passthrough class
|
|
110
|
+
reach the trigger, the panel and every field.
|