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
|
@@ -8,42 +8,129 @@ const meta: Meta<typeof EyebrowText> = {
|
|
|
8
8
|
tag: {
|
|
9
9
|
control: { type: "select" },
|
|
10
10
|
options: ["p", "div", "span"],
|
|
11
|
-
description: "HTML tag for the text element",
|
|
12
|
-
|
|
11
|
+
description: "HTML tag for the text element. Use `span` inside a line of text.",
|
|
12
|
+
table: { category: "Basic" },
|
|
13
13
|
},
|
|
14
14
|
fontSize: {
|
|
15
15
|
control: { type: "select" },
|
|
16
16
|
options: ["large", "medium", "small"],
|
|
17
|
-
description: "
|
|
18
|
-
|
|
17
|
+
description: "Size step, set as `data-font-size` and sized by `--eyebrow-text-large/medium/small`.",
|
|
18
|
+
table: { category: "Basic" },
|
|
19
19
|
},
|
|
20
20
|
textContent: {
|
|
21
|
-
control: "
|
|
22
|
-
description: "
|
|
23
|
-
|
|
21
|
+
control: { type: "text" },
|
|
22
|
+
description: "The label text. Pass it in normal case: the component uppercases it.",
|
|
23
|
+
table: { category: "Content" },
|
|
24
24
|
},
|
|
25
|
-
styleClassPassthrough: {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
styleClassPassthrough: { table: { disable: true } },
|
|
26
|
+
},
|
|
27
|
+
args: {
|
|
28
|
+
tag: "div",
|
|
29
|
+
fontSize: "medium",
|
|
30
|
+
textContent: "Designing Artistry at Home",
|
|
31
|
+
},
|
|
32
|
+
parameters: {
|
|
33
|
+
docs: {
|
|
34
|
+
description: {
|
|
35
|
+
component:
|
|
36
|
+
"A short uppercase label above a heading, filled with the `--eyebrow-text-bg-img` gradient. `textContent` is the label, `fontSize` picks one of three size steps and `tag` the element. Rendered inside `ServicesCard`, `ServiceSummary` and `ServiceDetail`; `ProfileSection` restyles one placed in its slot.",
|
|
37
|
+
},
|
|
29
38
|
},
|
|
30
39
|
},
|
|
31
40
|
};
|
|
32
41
|
|
|
33
42
|
export default meta;
|
|
34
43
|
type Story = StoryObj<typeof EyebrowText>;
|
|
44
|
+
|
|
45
|
+
// Links in the canvas open in the Storybook manager (target="_top"), not inside the preview iframe.
|
|
46
|
+
const storyNoteStyle = "margin: 0 0 1.6rem; font-size: 1.4rem;";
|
|
47
|
+
const parentNote = `
|
|
48
|
+
<p style="${storyNoteStyle}">
|
|
49
|
+
In use: the subtitle above the title in
|
|
50
|
+
<a href="/?path=/story/organisms-services-services-card--config-overrides" target="_top">ServicesCard</a>
|
|
51
|
+
(size and tag set through <code>eyebrowConfig</code>),
|
|
52
|
+
<a href="/?path=/story/organisms-services-service-summary--default" target="_top">ServiceSummary</a> and
|
|
53
|
+
<a href="/?path=/story/organisms-services-service-detail--default" target="_top">ServiceDetail</a>.
|
|
54
|
+
</p>
|
|
55
|
+
`;
|
|
56
|
+
const parentDocs =
|
|
57
|
+
"In use: the subtitle in [ServicesCard](?path=/story/organisms-services-services-card--config-overrides) (sized through `eyebrowConfig`), [ServiceSummary](?path=/story/organisms-services-service-summary--default) and [ServiceDetail](?path=/story/organisms-services-service-detail--default).";
|
|
58
|
+
|
|
35
59
|
export const Default: Story = {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
60
|
+
render: (args) => ({
|
|
61
|
+
components: { EyebrowText },
|
|
62
|
+
setup() {
|
|
63
|
+
return { args };
|
|
64
|
+
},
|
|
65
|
+
template: `<div>${parentNote}<EyebrowText v-bind="args" /></div>`,
|
|
66
|
+
}),
|
|
67
|
+
parameters: {
|
|
68
|
+
docs: {
|
|
69
|
+
description: {
|
|
70
|
+
story: `Default \`div\` at the \`medium\` size. Use the Controls panel to change the tag, size and text. ${parentDocs}`,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
41
73
|
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const Sizes: Story = {
|
|
42
77
|
render: (args) => ({
|
|
43
78
|
components: { EyebrowText },
|
|
44
79
|
setup() {
|
|
45
80
|
return { args };
|
|
46
81
|
},
|
|
47
|
-
template:
|
|
82
|
+
template: `
|
|
83
|
+
${parentNote}
|
|
84
|
+
<div style="display: grid; gap: 1.2rem;">
|
|
85
|
+
<EyebrowText v-bind="args" font-size="large" />
|
|
86
|
+
<EyebrowText v-bind="args" font-size="medium" />
|
|
87
|
+
<EyebrowText v-bind="args" font-size="small" />
|
|
88
|
+
</div>
|
|
89
|
+
`,
|
|
48
90
|
}),
|
|
91
|
+
parameters: {
|
|
92
|
+
docs: {
|
|
93
|
+
description: {
|
|
94
|
+
story: `The three \`fontSize\` steps, largest first. ${parentDocs}`,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const StressTest: Story = {
|
|
101
|
+
name: "Stress Test (Worst-Case Data)",
|
|
102
|
+
render: () => ({
|
|
103
|
+
components: { EyebrowText },
|
|
104
|
+
template: `
|
|
105
|
+
<div style="display: grid; gap: 2.4rem; padding: 2.4rem; font-size: 1.4rem;">
|
|
106
|
+
<p style="margin: 0;">Long German copy and an unbroken string in an 18rem column.</p>
|
|
107
|
+
<div style="display: grid; gap: 1.2rem; inline-size: 18rem; outline: 0.1rem dashed currentColor;">
|
|
108
|
+
<EyebrowText text-content="Kostenlose Lieferung für alle Bestellungen über einem Mindestbestellwert" font-size="large" />
|
|
109
|
+
<EyebrowText text-content="Kostenloselieferungfürallebestellungenübereinemmindestbestellwert" />
|
|
110
|
+
<EyebrowText text-content="https://example.invalid/a/very/long/unbroken/url/with/no/break/opportunity" font-size="small" />
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<p style="margin: 0;">Emoji first, HTML-like text (must show as text), right-to-left text, a single character and an empty string (renders nothing visible).</p>
|
|
114
|
+
<div style="display: grid; gap: 1.2rem;">
|
|
115
|
+
<EyebrowText text-content="😀 Emoji at the start" />
|
|
116
|
+
<EyebrowText text-content="<b>not bold</b>" />
|
|
117
|
+
<EyebrowText text-content="التوصيل المجاني" dir="rtl" />
|
|
118
|
+
<EyebrowText text-content="a" />
|
|
119
|
+
<EyebrowText text-content="" />
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<p style="margin: 0;">Inline <code>span</code> inside a sentence: the italic overhang of the last letter should not be cut off.</p>
|
|
123
|
+
<p style="margin: 0;">Before <EyebrowText tag="span" text-content="Inline eyebrow WIDTH" font-size="large" /> after.</p>
|
|
124
|
+
</div>
|
|
125
|
+
`,
|
|
126
|
+
}),
|
|
127
|
+
parameters: {
|
|
128
|
+
controls: { disable: true },
|
|
129
|
+
docs: {
|
|
130
|
+
description: {
|
|
131
|
+
story:
|
|
132
|
+
"Worst-case text. Check: long copy and unbroken strings wrap inside the dashed column; emoji and right-to-left text keep the gradient fill; HTML-like text shows its tags as text; an empty string adds no visible box; the inline span sits on the line and its last italic letter is fully painted.",
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
49
136
|
};
|
|
@@ -1,52 +1,55 @@
|
|
|
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 EyebrowText from "../EyebrowText.vue";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
type EyebrowProps = InstanceType<typeof EyebrowText>["$props"];
|
|
6
|
+
|
|
7
|
+
const mounted: Awaited<ReturnType<typeof mountSuspended>>[] = [];
|
|
8
|
+
|
|
9
|
+
const mountEyebrow = async (props: EyebrowProps) => {
|
|
10
|
+
const wrapper = await mountSuspended(EyebrowText, { props });
|
|
11
|
+
mounted.push(wrapper);
|
|
12
|
+
return wrapper;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
mounted.splice(0).forEach((wrapper) => wrapper.unmount());
|
|
17
|
+
vi.restoreAllMocks();
|
|
18
|
+
});
|
|
7
19
|
|
|
20
|
+
describe("EyebrowText", () => {
|
|
8
21
|
it("mounts without error", async () => {
|
|
9
|
-
const wrapper = await
|
|
10
|
-
props: { textContent: "Hello World" },
|
|
11
|
-
});
|
|
22
|
+
const wrapper = await mountEyebrow({ textContent: "Hello World" });
|
|
12
23
|
expect(wrapper.vm).toBeTruthy();
|
|
13
24
|
});
|
|
14
25
|
|
|
15
26
|
// ─── Snapshots ───────────────────────────────────────────────────────────
|
|
16
27
|
|
|
17
28
|
it("renders correct HTML structure with default props", async () => {
|
|
18
|
-
const wrapper = await
|
|
19
|
-
props: { textContent: "Featured Collection" },
|
|
20
|
-
});
|
|
29
|
+
const wrapper = await mountEyebrow({ textContent: "Featured Collection" });
|
|
21
30
|
expect(wrapper.html()).toMatchSnapshot();
|
|
22
31
|
});
|
|
23
32
|
|
|
24
33
|
it("renders correct HTML structure with all props set", async () => {
|
|
25
|
-
const wrapper = await
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
styleClassPassthrough: ["custom-class"],
|
|
31
|
-
},
|
|
34
|
+
const wrapper = await mountEyebrow({
|
|
35
|
+
tag: "p",
|
|
36
|
+
fontSize: "large",
|
|
37
|
+
textContent: "New Arrivals",
|
|
38
|
+
styleClassPassthrough: ["custom-class"],
|
|
32
39
|
});
|
|
33
40
|
expect(wrapper.html()).toMatchSnapshot();
|
|
34
41
|
});
|
|
35
42
|
|
|
36
43
|
it("renders correct HTML structure for each tag variant", async () => {
|
|
37
44
|
for (const tag of ["p", "div", "span"] as const) {
|
|
38
|
-
const wrapper = await
|
|
39
|
-
props: { tag, textContent: "Snapshot text" },
|
|
40
|
-
});
|
|
45
|
+
const wrapper = await mountEyebrow({ tag, textContent: "Snapshot text" });
|
|
41
46
|
expect(wrapper.html()).toMatchSnapshot();
|
|
42
47
|
}
|
|
43
48
|
});
|
|
44
49
|
|
|
45
50
|
it("renders correct HTML structure for each fontSize variant", async () => {
|
|
46
51
|
for (const fontSize of ["large", "medium", "small"] as const) {
|
|
47
|
-
const wrapper = await
|
|
48
|
-
props: { fontSize, textContent: "Snapshot text" },
|
|
49
|
-
});
|
|
52
|
+
const wrapper = await mountEyebrow({ fontSize, textContent: "Snapshot text" });
|
|
50
53
|
expect(wrapper.html()).toMatchSnapshot();
|
|
51
54
|
}
|
|
52
55
|
});
|
|
@@ -54,154 +57,101 @@ describe("EyebrowText", () => {
|
|
|
54
57
|
// ─── Tag rendering ───────────────────────────────────────────────────────
|
|
55
58
|
|
|
56
59
|
it("renders as <div> by default", async () => {
|
|
57
|
-
const wrapper = await
|
|
58
|
-
props: { textContent: "Default tag" },
|
|
59
|
-
});
|
|
60
|
+
const wrapper = await mountEyebrow({ textContent: "Default tag" });
|
|
60
61
|
expect(wrapper.element.tagName).toBe("DIV");
|
|
61
62
|
});
|
|
62
63
|
|
|
63
|
-
it(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
it("renders as <span> when tag='span'", async () => {
|
|
71
|
-
const wrapper = await mountSuspended(EyebrowText, {
|
|
72
|
-
props: { tag: "span", textContent: "Span tag" },
|
|
73
|
-
});
|
|
74
|
-
expect(wrapper.element.tagName).toBe("SPAN");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("renders as <div> when tag='div'", async () => {
|
|
78
|
-
const wrapper = await mountSuspended(EyebrowText, {
|
|
79
|
-
props: { tag: "div", textContent: "Div tag" },
|
|
80
|
-
});
|
|
81
|
-
expect(wrapper.element.tagName).toBe("DIV");
|
|
64
|
+
it.each([
|
|
65
|
+
["p", "P"],
|
|
66
|
+
["span", "SPAN"],
|
|
67
|
+
["div", "DIV"],
|
|
68
|
+
] as const)("renders as <%s> when tag='%s'", async (tag, tagName) => {
|
|
69
|
+
const wrapper = await mountEyebrow({ tag, textContent: "Tag" });
|
|
70
|
+
expect(wrapper.element.tagName).toBe(tagName);
|
|
82
71
|
});
|
|
83
72
|
|
|
84
|
-
// ─── Base class ──────────────────────────────────────────────────────────
|
|
85
|
-
|
|
86
73
|
it("always has the eyebrow-text class", async () => {
|
|
87
|
-
const wrapper = await
|
|
88
|
-
props: { textContent: "Base class check" },
|
|
89
|
-
});
|
|
74
|
+
const wrapper = await mountEyebrow({ textContent: "Base class check" });
|
|
90
75
|
expect(wrapper.classes()).toContain("eyebrow-text");
|
|
91
76
|
});
|
|
92
77
|
|
|
93
|
-
// ─── fontSize
|
|
78
|
+
// ─── fontSize ────────────────────────────────────────────────────────────
|
|
94
79
|
|
|
95
|
-
it("
|
|
96
|
-
const wrapper = await
|
|
97
|
-
|
|
98
|
-
});
|
|
99
|
-
expect(wrapper.classes()).toContain("medium");
|
|
80
|
+
it("sets data-font-size='medium' by default", async () => {
|
|
81
|
+
const wrapper = await mountEyebrow({ textContent: "Default fontSize" });
|
|
82
|
+
expect(wrapper.attributes("data-font-size")).toBe("medium");
|
|
100
83
|
});
|
|
101
84
|
|
|
102
|
-
it("
|
|
103
|
-
const wrapper = await
|
|
104
|
-
|
|
105
|
-
});
|
|
106
|
-
expect(wrapper.classes()).toContain("large");
|
|
85
|
+
it.each(["large", "medium", "small"] as const)("sets data-font-size='%s'", async (fontSize) => {
|
|
86
|
+
const wrapper = await mountEyebrow({ fontSize, textContent: "Sized" });
|
|
87
|
+
expect(wrapper.attributes("data-font-size")).toBe(fontSize);
|
|
107
88
|
});
|
|
108
89
|
|
|
109
|
-
it("
|
|
110
|
-
const wrapper = await
|
|
111
|
-
|
|
112
|
-
});
|
|
113
|
-
expect(wrapper.classes()).toContain("small");
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("applies only one fontSize class at a time", async () => {
|
|
117
|
-
const fontSizes = ["large", "medium", "small"] as const;
|
|
118
|
-
for (const fontSize of fontSizes) {
|
|
119
|
-
const wrapper = await mountSuspended(EyebrowText, {
|
|
120
|
-
props: { fontSize, textContent: "One class only" },
|
|
121
|
-
});
|
|
122
|
-
const otherSizes = fontSizes.filter((s) => s !== fontSize);
|
|
123
|
-
otherSizes.forEach((size) => {
|
|
124
|
-
expect(wrapper.classes()).not.toContain(size);
|
|
125
|
-
});
|
|
126
|
-
}
|
|
90
|
+
it("does not put the size on the element as a bare class", async () => {
|
|
91
|
+
const wrapper = await mountEyebrow({ fontSize: "small", textContent: "No bare class" });
|
|
92
|
+
expect(wrapper.classes()).toEqual(["eyebrow-text"]);
|
|
127
93
|
});
|
|
128
94
|
|
|
129
95
|
// ─── textContent ─────────────────────────────────────────────────────────
|
|
130
96
|
|
|
131
97
|
it("renders the textContent string", async () => {
|
|
132
|
-
const wrapper = await
|
|
133
|
-
props: { textContent: "New Arrivals" },
|
|
134
|
-
});
|
|
98
|
+
const wrapper = await mountEyebrow({ textContent: "New Arrivals" });
|
|
135
99
|
expect(wrapper.text()).toBe("New Arrivals");
|
|
136
100
|
});
|
|
137
101
|
|
|
138
102
|
it("renders an empty string without error", async () => {
|
|
139
|
-
const wrapper = await
|
|
140
|
-
props: { textContent: "" },
|
|
141
|
-
});
|
|
103
|
+
const wrapper = await mountEyebrow({ textContent: "" });
|
|
142
104
|
expect(wrapper.text()).toBe("");
|
|
143
105
|
});
|
|
144
106
|
|
|
145
107
|
it("renders textContent with special characters correctly", async () => {
|
|
146
|
-
const wrapper = await
|
|
147
|
-
props: { textContent: "Édition Limitée & Co." },
|
|
148
|
-
});
|
|
108
|
+
const wrapper = await mountEyebrow({ textContent: "Édition Limitée & Co." });
|
|
149
109
|
expect(wrapper.text()).toBe("Édition Limitée & Co.");
|
|
150
110
|
});
|
|
151
111
|
|
|
112
|
+
it("renders HTML-like textContent as text", async () => {
|
|
113
|
+
const wrapper = await mountEyebrow({ textContent: "<b>not bold</b>" });
|
|
114
|
+
expect(wrapper.find("b").exists()).toBe(false);
|
|
115
|
+
expect(wrapper.text()).toBe("<b>not bold</b>");
|
|
116
|
+
});
|
|
117
|
+
|
|
152
118
|
// ─── styleClassPassthrough ───────────────────────────────────────────────
|
|
153
119
|
|
|
154
120
|
it("applies a styleClassPassthrough array", async () => {
|
|
155
|
-
const wrapper = await
|
|
156
|
-
props: {
|
|
157
|
-
textContent: "Styled",
|
|
158
|
-
styleClassPassthrough: ["class-a", "class-b"],
|
|
159
|
-
},
|
|
160
|
-
});
|
|
121
|
+
const wrapper = await mountEyebrow({ textContent: "Styled", styleClassPassthrough: ["class-a", "class-b"] });
|
|
161
122
|
expect(wrapper.classes()).toContain("class-a");
|
|
162
123
|
expect(wrapper.classes()).toContain("class-b");
|
|
163
124
|
});
|
|
164
125
|
|
|
165
126
|
it("applies a single styleClassPassthrough string", async () => {
|
|
166
|
-
const wrapper = await
|
|
167
|
-
props: { textContent: "Styled", styleClassPassthrough: "solo-class" },
|
|
168
|
-
});
|
|
127
|
+
const wrapper = await mountEyebrow({ textContent: "Styled", styleClassPassthrough: "solo-class" });
|
|
169
128
|
expect(wrapper.classes()).toContain("solo-class");
|
|
170
129
|
});
|
|
171
130
|
|
|
172
|
-
it("renders cleanly with no styleClassPassthrough prop", async () => {
|
|
173
|
-
const wrapper = await mountSuspended(EyebrowText, {
|
|
174
|
-
props: { textContent: "No passthrough" },
|
|
175
|
-
});
|
|
176
|
-
expect(wrapper.classes()).toContain("eyebrow-text");
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// ─── Combined props ───────────────────────────────────────────────────────
|
|
180
|
-
|
|
181
131
|
it("renders correctly with all props combined", async () => {
|
|
182
|
-
const wrapper = await
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
styleClassPassthrough: ["promo-label"],
|
|
188
|
-
},
|
|
132
|
+
const wrapper = await mountEyebrow({
|
|
133
|
+
tag: "span",
|
|
134
|
+
fontSize: "small",
|
|
135
|
+
textContent: "Limited Edition",
|
|
136
|
+
styleClassPassthrough: ["promo-label"],
|
|
189
137
|
});
|
|
190
138
|
expect(wrapper.element.tagName).toBe("SPAN");
|
|
191
139
|
expect(wrapper.classes()).toContain("eyebrow-text");
|
|
192
|
-
|
|
140
|
+
expect(wrapper.attributes("data-font-size")).toBe("small");
|
|
193
141
|
expect(wrapper.classes()).toContain("promo-label");
|
|
194
142
|
expect(wrapper.text()).toBe("Limited Edition");
|
|
195
143
|
});
|
|
196
|
-
});
|
|
197
144
|
|
|
198
|
-
describe("EyebrowText styleClassPassthrough reactivity", () => {
|
|
199
145
|
it("updates classes when styleClassPassthrough changes after mount", async () => {
|
|
200
|
-
const wrapper = await
|
|
201
|
-
props: { textContent: "Eyebrow", styleClassPassthrough: ["original"] },
|
|
202
|
-
});
|
|
146
|
+
const wrapper = await mountEyebrow({ textContent: "Eyebrow", styleClassPassthrough: ["original"] });
|
|
203
147
|
await wrapper.setProps({ styleClassPassthrough: ["updated"] });
|
|
204
148
|
expect(wrapper.classes()).not.toContain("original");
|
|
205
149
|
expect(wrapper.classes()).toContain("updated");
|
|
206
150
|
});
|
|
151
|
+
|
|
152
|
+
it("updates data-font-size when fontSize changes after mount", async () => {
|
|
153
|
+
const wrapper = await mountEyebrow({ textContent: "Eyebrow", fontSize: "small" });
|
|
154
|
+
await wrapper.setProps({ fontSize: "large" });
|
|
155
|
+
expect(wrapper.attributes("data-font-size")).toBe("large");
|
|
156
|
+
});
|
|
207
157
|
});
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
|
-
exports[`EyebrowText > renders correct HTML structure for each fontSize variant 1`] = `"<div class="eyebrow-text large">Snapshot text</div>"`;
|
|
3
|
+
exports[`EyebrowText > renders correct HTML structure for each fontSize variant 1`] = `"<div class="eyebrow-text" data-font-size="large">Snapshot text</div>"`;
|
|
4
4
|
|
|
5
|
-
exports[`EyebrowText > renders correct HTML structure for each fontSize variant 2`] = `"<div class="eyebrow-text medium">Snapshot text</div>"`;
|
|
5
|
+
exports[`EyebrowText > renders correct HTML structure for each fontSize variant 2`] = `"<div class="eyebrow-text" data-font-size="medium">Snapshot text</div>"`;
|
|
6
6
|
|
|
7
|
-
exports[`EyebrowText > renders correct HTML structure for each fontSize variant 3`] = `"<div class="eyebrow-text small">Snapshot text</div>"`;
|
|
7
|
+
exports[`EyebrowText > renders correct HTML structure for each fontSize variant 3`] = `"<div class="eyebrow-text" data-font-size="small">Snapshot text</div>"`;
|
|
8
8
|
|
|
9
|
-
exports[`EyebrowText > renders correct HTML structure for each tag variant 1`] = `"<p class="eyebrow-text medium">Snapshot text</p>"`;
|
|
9
|
+
exports[`EyebrowText > renders correct HTML structure for each tag variant 1`] = `"<p class="eyebrow-text" data-font-size="medium">Snapshot text</p>"`;
|
|
10
10
|
|
|
11
|
-
exports[`EyebrowText > renders correct HTML structure for each tag variant 2`] = `"<div class="eyebrow-text medium">Snapshot text</div>"`;
|
|
11
|
+
exports[`EyebrowText > renders correct HTML structure for each tag variant 2`] = `"<div class="eyebrow-text" data-font-size="medium">Snapshot text</div>"`;
|
|
12
12
|
|
|
13
|
-
exports[`EyebrowText > renders correct HTML structure for each tag variant 3`] = `"<span class="eyebrow-text medium">Snapshot text</span>"`;
|
|
13
|
+
exports[`EyebrowText > renders correct HTML structure for each tag variant 3`] = `"<span class="eyebrow-text" data-font-size="medium">Snapshot text</span>"`;
|
|
14
14
|
|
|
15
|
-
exports[`EyebrowText > renders correct HTML structure with all props set 1`] = `"<p class="eyebrow-text custom-class large">New Arrivals</p>"`;
|
|
15
|
+
exports[`EyebrowText > renders correct HTML structure with all props set 1`] = `"<p class="eyebrow-text custom-class" data-font-size="large">New Arrivals</p>"`;
|
|
16
16
|
|
|
17
|
-
exports[`EyebrowText > renders correct HTML structure with default props 1`] = `"<div class="eyebrow-text medium">Featured Collection</div>"`;
|
|
17
|
+
exports[`EyebrowText > renders correct HTML structure with default props 1`] = `"<div class="eyebrow-text" data-font-size="medium">Featured Collection</div>"`;
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
# HeaderBlock — Consumer Styling Guide
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Public token API
|
|
4
4
|
|
|
5
|
-
`HeaderBlock` has no
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
`HeaderBlock` has no tokens of its own, public or private (`--_`). Its own style block only sets
|
|
6
|
+
`overflow-wrap: anywhere` on `.header-block`, so an unbroken word or URL wraps instead of
|
|
7
|
+
overflowing its column.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
.
|
|
12
|
-
/* … down to .page-heading-6 { font-size: var(--step-3); } */
|
|
13
|
-
```
|
|
9
|
+
Visual size comes from one of six global `.page-heading-1`–`.page-heading-6` utility classes (the
|
|
10
|
+
`classLevel` prop), defined in
|
|
11
|
+
`app/assets/styles/setup/05.typography/02.utility-classes/_font-classes-page-heading.css`:
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
| Class | `font-size` |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `.page-heading-1` | `var(--step-8)` |
|
|
16
|
+
| `.page-heading-2` | `var(--step-7)` |
|
|
17
|
+
| `.page-heading-3` | `var(--step-6)` |
|
|
18
|
+
| `.page-heading-4` | `var(--step-5)` |
|
|
19
|
+
| `.page-heading-5` | `var(--step-4)` |
|
|
20
|
+
| `.page-heading-6` | `var(--step-3)` |
|
|
18
21
|
|
|
19
|
-
All six
|
|
20
|
-
browser's
|
|
21
|
-
`classLevel
|
|
22
|
-
own CSS rather than expecting a level to come with baked-in margin:
|
|
22
|
+
All six also set `font-weight: 600`, `line-height: 1`, and reset `margin-block` and `padding-block`
|
|
23
|
+
to `0rem` instead of relying on the browser's per-level heading margins, which would fight
|
|
24
|
+
`classLevel` and `tagLevel` being independent. Add spacing in your own CSS:
|
|
23
25
|
|
|
24
26
|
```css
|
|
25
27
|
.my-page .page-heading-1 {
|
|
@@ -29,13 +31,27 @@ own CSS rather than expecting a level to come with baked-in margin:
|
|
|
29
31
|
|
|
30
32
|
---
|
|
31
33
|
|
|
34
|
+
## State hooks
|
|
35
|
+
|
|
36
|
+
No state attributes. Classes on the root `<h1>`–`<h6>`:
|
|
37
|
+
|
|
38
|
+
| Class | Meaning |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `.header-block` | Always present |
|
|
41
|
+
| `.page-heading-{1-6}` | The `classLevel` prop (out-of-range values fall back to `1`) |
|
|
42
|
+
|
|
43
|
+
> Changed 2026-10-19: `.header-block` is new. Before, the root carried only the
|
|
44
|
+
> `.page-heading-*` class and passthrough classes.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
32
48
|
## Local overrides
|
|
33
49
|
|
|
34
|
-
There are no public tokens, so override
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
There are no public tokens, so override with direct properties, scoped to an element you own (a
|
|
51
|
+
page or section class, or a class added with `:style-class-passthrough`). Keep the block
|
|
52
|
+
**unlayered** (no `@layer` wrapper) so it beats the library's `@layer components`. If your own file
|
|
53
|
+
uses `<style scoped>`, selectors that reach inside the component need `:deep()`. Patterns and
|
|
54
|
+
examples: `.claude/skills/component-local-style-override.md`.
|
|
39
55
|
|
|
40
56
|
**Caveat:** size comes from the global `--step-*` type-scale tokens behind `.page-heading-*`.
|
|
41
57
|
Overriding a `--step-*` token on your own element changes every `page-heading-*` inside it, not just
|
|
@@ -43,9 +59,6 @@ this component's.
|
|
|
43
59
|
|
|
44
60
|
### One instance
|
|
45
61
|
|
|
46
|
-
Use `styleClassPassthrough` to add your own class and override `font-size`/`font-weight`/etc. for
|
|
47
|
-
a single `HeaderBlock` instance without touching the shared `.page-heading-*` utility classes:
|
|
48
|
-
|
|
49
62
|
```vue
|
|
50
63
|
<HeaderBlock :tag-level="2" :class-level="1" style-class-passthrough="hero-title">Big idea</HeaderBlock>
|
|
51
64
|
```
|
|
@@ -56,3 +69,15 @@ a single `HeaderBlock` instance without touching the shared `.page-heading-*` ut
|
|
|
56
69
|
}
|
|
57
70
|
```
|
|
58
71
|
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Class passthrough
|
|
75
|
+
|
|
76
|
+
`style-class-passthrough` adds classes to the root heading element. Reactive after mount.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Notes
|
|
81
|
+
|
|
82
|
+
- Text arrives through the default slot, so there's no line-clamp token: clamp it in your own
|
|
83
|
+
markup if needed.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<component :is="tag" :id :class="[classLevelClass, elementClasses]">
|
|
2
|
+
<component :is="tag" :id class="header-block" :class="[classLevelClass, elementClasses]">
|
|
3
3
|
<slot></slot>
|
|
4
4
|
</component>
|
|
5
5
|
</template>
|
|
@@ -48,3 +48,11 @@ watch(
|
|
|
48
48
|
}
|
|
49
49
|
);
|
|
50
50
|
</script>
|
|
51
|
+
|
|
52
|
+
<style lang="css">
|
|
53
|
+
@layer components {
|
|
54
|
+
.header-block {
|
|
55
|
+
overflow-wrap: anywhere;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
</style>
|