srcdev-nuxt-components 9.6.13 → 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/story-register-components.md +1 -1
- package/.claude/skills/component-compliance-checklist.md +5 -0
- package/.claude/skills/components/display-qr-code.md +22 -4
- 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/storybook-add-story.md +5 -2
- package/.claude/skills/testing-add-playwright.md +12 -7
- package/.vscode/srcdev-component-display-qr-code.code-snippets +12 -1
- 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/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/price-list/tests/__snapshots__/PriceList.spec.ts.snap +8 -8
- 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/package.json +1 -1
|
@@ -1,214 +1,247 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
import DisplayQrCode from "../DisplayQrCode.vue";
|
|
3
4
|
import type { QrCodeVariant } from "../../../../types/components";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
type SvgVariant = QrCodeVariant["pixel"];
|
|
7
|
+
|
|
8
|
+
interface StoryArgs {
|
|
9
|
+
qrValue: string;
|
|
10
|
+
size: string;
|
|
11
|
+
radius: number;
|
|
12
|
+
blackColor: string;
|
|
13
|
+
whiteColor: string;
|
|
14
|
+
ariaLabel: string;
|
|
15
|
+
variantInner: SvgVariant;
|
|
16
|
+
variantMarker: SvgVariant;
|
|
17
|
+
variantPixel: SvgVariant;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const variantOptions: SvgVariant[] = ["default", "circle", "rounded", "dots", "pixelated"];
|
|
21
|
+
|
|
22
|
+
const variantArgType = (part: string) => ({
|
|
23
|
+
control: { type: "select" as const },
|
|
24
|
+
options: variantOptions,
|
|
25
|
+
description: `Story-only: sets \`variant.${part}\` on the \`variant\` prop.`,
|
|
26
|
+
table: { category: "Variant (story-only)" },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const meta: Meta<StoryArgs> = {
|
|
6
30
|
title: "Atoms/QR Code/DisplayQrCode",
|
|
7
|
-
component:
|
|
31
|
+
component: DisplayQrCode,
|
|
8
32
|
argTypes: {
|
|
9
33
|
qrValue: {
|
|
10
34
|
control: { type: "text" },
|
|
11
|
-
description: "The value
|
|
35
|
+
description: "The value to encode. Over about 2.9KB it can't be encoded, and the `fallback` slot renders instead.",
|
|
12
36
|
table: { category: "Content" },
|
|
13
37
|
},
|
|
14
38
|
size: {
|
|
15
39
|
control: { type: "text" },
|
|
16
|
-
description: "
|
|
40
|
+
description: "Width of the code as any CSS length; `--display-qr-code-size` overrides it. Never wider than its container.",
|
|
17
41
|
table: { category: "Appearance" },
|
|
18
42
|
},
|
|
19
43
|
radius: {
|
|
20
|
-
control: { type: "range", min: 0, max:
|
|
21
|
-
description: "
|
|
44
|
+
control: { type: "range", min: 0, max: 1, step: 0.05 },
|
|
45
|
+
description: "Corner rounding for the rounded, circle and dots shapes, from 0 to 1 (the library clamps anything outside).",
|
|
22
46
|
table: { category: "Appearance" },
|
|
23
47
|
},
|
|
24
48
|
blackColor: {
|
|
25
49
|
control: { type: "color" },
|
|
26
|
-
description: "
|
|
50
|
+
description: "Colour of the dark modules. Defaults to `currentColor`.",
|
|
27
51
|
table: { category: "Appearance" },
|
|
28
52
|
},
|
|
29
53
|
whiteColor: {
|
|
30
54
|
control: { type: "color" },
|
|
31
|
-
description: "
|
|
55
|
+
description: "Colour of the light modules. Defaults to `transparent`.",
|
|
32
56
|
table: { category: "Appearance" },
|
|
33
57
|
},
|
|
34
58
|
ariaLabel: {
|
|
35
59
|
control: { type: "text" },
|
|
36
|
-
description: "Accessible name for the QR image (override
|
|
60
|
+
description: "Accessible name for the QR image (override with a more specific or translated label)",
|
|
37
61
|
table: { category: "Accessibility" },
|
|
38
62
|
},
|
|
39
|
-
|
|
40
|
-
|
|
63
|
+
variantInner: variantArgType("inner"),
|
|
64
|
+
variantMarker: variantArgType("marker"),
|
|
65
|
+
variantPixel: variantArgType("pixel"),
|
|
41
66
|
},
|
|
42
67
|
args: {
|
|
43
|
-
qrValue: "https://
|
|
68
|
+
qrValue: "https://example.com",
|
|
44
69
|
size: "256px",
|
|
45
70
|
radius: 0,
|
|
46
71
|
blackColor: "#000000",
|
|
47
72
|
whiteColor: "transparent",
|
|
48
73
|
ariaLabel: "QR code",
|
|
49
|
-
|
|
74
|
+
variantInner: "default",
|
|
75
|
+
variantMarker: "default",
|
|
76
|
+
variantPixel: "default",
|
|
77
|
+
},
|
|
78
|
+
parameters: {
|
|
79
|
+
docs: {
|
|
80
|
+
description: {
|
|
81
|
+
component:
|
|
82
|
+
"Renders a value as an SVG QR code with `role=\"img\"` and an `ariaLabel`. `size`, `radius`, `blackColor`, `whiteColor` and `variant` (separate shapes for the inner markers, outer markers and pixels) style it. A value too long to encode renders the `fallback` slot and emits `error` instead of breaking the page.",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
50
85
|
},
|
|
51
86
|
};
|
|
52
87
|
|
|
53
88
|
export default meta;
|
|
89
|
+
type Story = StoryObj<StoryArgs>;
|
|
90
|
+
|
|
91
|
+
const storySetup = (args: StoryArgs) => {
|
|
92
|
+
const componentArgs = computed(() => {
|
|
93
|
+
const { variantInner, variantMarker, variantPixel, ...rest } = args;
|
|
94
|
+
return { ...rest, variant: { inner: variantInner, marker: variantMarker, pixel: variantPixel } };
|
|
95
|
+
});
|
|
96
|
+
return { componentArgs };
|
|
97
|
+
};
|
|
54
98
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
radius: number;
|
|
59
|
-
blackColor: string;
|
|
60
|
-
whiteColor: string;
|
|
61
|
-
ariaLabel: string;
|
|
62
|
-
styleClassPassthrough: string[];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const DisplayTemplate: StoryFn<QrStoryArgs> = (args, { parameters }) => ({
|
|
66
|
-
components: { DisplayQrCodeComponent },
|
|
67
|
-
setup() {
|
|
68
|
-
const variant: QrCodeVariant = parameters?.variant || {
|
|
69
|
-
inner: "default",
|
|
70
|
-
marker: "default",
|
|
71
|
-
pixel: "default",
|
|
72
|
-
};
|
|
73
|
-
return { args, variant };
|
|
74
|
-
},
|
|
99
|
+
const render: Story["render"] = (args) => ({
|
|
100
|
+
components: { DisplayQrCode },
|
|
101
|
+
setup: () => storySetup(args),
|
|
75
102
|
template: `
|
|
76
|
-
<div style="padding:
|
|
77
|
-
<
|
|
78
|
-
:qr-value="args.qrValue"
|
|
79
|
-
:variant="variant"
|
|
80
|
-
:radius="args.radius"
|
|
81
|
-
:black-color="args.blackColor"
|
|
82
|
-
:white-color="args.whiteColor"
|
|
83
|
-
:size="args.size"
|
|
84
|
-
:aria-label="args.ariaLabel"
|
|
85
|
-
:style-class-passthrough="args.styleClassPassthrough"
|
|
86
|
-
/>
|
|
103
|
+
<div style="padding: 4rem; display: flex; align-items: center; justify-content: center; background: #f5f5f5; border-radius: 0.8rem;">
|
|
104
|
+
<DisplayQrCode v-bind="componentArgs" />
|
|
87
105
|
</div>
|
|
88
106
|
`,
|
|
89
107
|
});
|
|
90
108
|
|
|
91
|
-
|
|
109
|
+
const story = (description: string, args: Partial<StoryArgs> = {}): Story => ({
|
|
110
|
+
render,
|
|
111
|
+
args,
|
|
112
|
+
parameters: { docs: { description: { story: description } } },
|
|
113
|
+
});
|
|
92
114
|
|
|
93
|
-
export const
|
|
94
|
-
Website.args = { qrValue: "https://nuxt.com", radius: 8, blackColor: "#000000" };
|
|
95
|
-
Website.parameters = { variant: { inner: "circle", marker: "rounded", pixel: "dots" } };
|
|
115
|
+
export const Default = story("Default styling. Use the Controls panel to try every prop, including the three variant shapes.");
|
|
96
116
|
|
|
97
|
-
export const
|
|
98
|
-
|
|
99
|
-
|
|
117
|
+
export const Website = story("A URL with circle inner markers, rounded outer markers and dot pixels.", {
|
|
118
|
+
qrValue: "https://nuxt.com",
|
|
119
|
+
radius: 0.5,
|
|
120
|
+
variantInner: "circle",
|
|
121
|
+
variantMarker: "rounded",
|
|
122
|
+
variantPixel: "dots",
|
|
123
|
+
});
|
|
100
124
|
|
|
101
|
-
export const VariantCircle =
|
|
102
|
-
|
|
103
|
-
|
|
125
|
+
export const VariantCircle = story("Every part in the circle shape.", {
|
|
126
|
+
qrValue: "Circle variant style",
|
|
127
|
+
radius: 0.5,
|
|
128
|
+
blackColor: "#1e40af",
|
|
129
|
+
variantInner: "circle",
|
|
130
|
+
variantMarker: "circle",
|
|
131
|
+
variantPixel: "circle",
|
|
132
|
+
});
|
|
104
133
|
|
|
105
|
-
export const VariantRounded =
|
|
106
|
-
|
|
107
|
-
|
|
134
|
+
export const VariantRounded = story("Every part in the rounded shape, at full radius.", {
|
|
135
|
+
qrValue: "Rounded variant style",
|
|
136
|
+
radius: 1,
|
|
137
|
+
blackColor: "#059669",
|
|
138
|
+
variantInner: "rounded",
|
|
139
|
+
variantMarker: "rounded",
|
|
140
|
+
variantPixel: "rounded",
|
|
141
|
+
});
|
|
108
142
|
|
|
109
|
-
export const VariantDots =
|
|
110
|
-
|
|
111
|
-
|
|
143
|
+
export const VariantDots = story("Every part in the dots shape.", {
|
|
144
|
+
qrValue: "Dots variant style",
|
|
145
|
+
radius: 0.5,
|
|
146
|
+
blackColor: "#dc2626",
|
|
147
|
+
variantInner: "dots",
|
|
148
|
+
variantMarker: "dots",
|
|
149
|
+
variantPixel: "dots",
|
|
150
|
+
});
|
|
112
151
|
|
|
113
|
-
export const
|
|
114
|
-
|
|
115
|
-
|
|
152
|
+
export const VariantPixelated = story("Every part in the pixelated shape.", {
|
|
153
|
+
qrValue: "Pixelated variant style",
|
|
154
|
+
blackColor: "#7c3aed",
|
|
155
|
+
variantInner: "pixelated",
|
|
156
|
+
variantMarker: "pixelated",
|
|
157
|
+
variantPixel: "pixelated",
|
|
158
|
+
});
|
|
116
159
|
|
|
117
|
-
export const WiFiConnection =
|
|
118
|
-
|
|
119
|
-
|
|
160
|
+
export const WiFiConnection = story("A Wi-Fi join string, which phone cameras offer to connect to.", {
|
|
161
|
+
qrValue: "WIFI:T:WPA;S:NorthtownGuest;P:example-password;H:false;",
|
|
162
|
+
blackColor: "#1e40af",
|
|
163
|
+
radius: 0.25,
|
|
164
|
+
variantInner: "rounded",
|
|
165
|
+
variantMarker: "circle",
|
|
166
|
+
});
|
|
120
167
|
|
|
121
|
-
export const VCardContact =
|
|
122
|
-
|
|
123
|
-
|
|
168
|
+
export const VCardContact = story("A vCard contact. Longer values produce a denser code.", {
|
|
169
|
+
qrValue:
|
|
170
|
+
"BEGIN:VCARD\nVERSION:3.0\nFN:Alex Morgan\nORG:Northtown Studio\nTEL:+441234567890\nEMAIL:alex@example.com\nURL:https://example.com\nEND:VCARD",
|
|
124
171
|
blackColor: "#059669",
|
|
125
|
-
radius:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
SmallSize
|
|
132
|
-
|
|
133
|
-
export const LargeSize =
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
{
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
template: `
|
|
163
|
-
<div style="padding: 40px;">
|
|
164
|
-
<div style="display: grid; grid-template-columns: 1fr 300px; gap: 40px; align-items: start;">
|
|
165
|
-
<div style="display: flex; align-items: center; justify-content: center; min-height: 400px; background: #f8fafc; border-radius: 8px; padding: 40px;">
|
|
166
|
-
<DisplayQrCodeComponent :qrValue="qrValue" :variant="variant" :radius="radius" :blackColor="blackColor" :whiteColor="whiteColor" :size="size" />
|
|
172
|
+
radius: 0.75,
|
|
173
|
+
variantInner: "dots",
|
|
174
|
+
variantMarker: "rounded",
|
|
175
|
+
variantPixel: "circle",
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
export const SmallSize = story("`size` at 128px.", { size: "128px" });
|
|
179
|
+
|
|
180
|
+
export const LargeSize = story("`size` at 400px. In a narrower container the code shrinks to fit.", {
|
|
181
|
+
size: "400px",
|
|
182
|
+
radius: 0.5,
|
|
183
|
+
blackColor: "#0ea5e9",
|
|
184
|
+
variantInner: "circle",
|
|
185
|
+
variantMarker: "rounded",
|
|
186
|
+
variantPixel: "dots",
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
export const HighContrast = story("Black on an opaque white background, the most reliable combination for scanning.", {
|
|
190
|
+
whiteColor: "#ffffff",
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
export const StressTest: Story = {
|
|
194
|
+
name: "Stress Test (Worst-Case Data)",
|
|
195
|
+
render: () => ({
|
|
196
|
+
components: { DisplayQrCode },
|
|
197
|
+
setup() {
|
|
198
|
+
const nearCapacity = "x".repeat(2900);
|
|
199
|
+
const overCapacity = "https://example.com/?q=" + "x".repeat(4000);
|
|
200
|
+
return { nearCapacity, overCapacity };
|
|
201
|
+
},
|
|
202
|
+
template: `
|
|
203
|
+
<div style="display: grid; gap: 2.4rem; padding: 2.4rem; font-size: 1.4rem;">
|
|
204
|
+
<p style="margin: 0;">An empty value, a single character, and emoji with right-to-left text: each still renders a code.</p>
|
|
205
|
+
<div style="display: flex; flex-wrap: wrap; gap: 2.4rem;">
|
|
206
|
+
<DisplayQrCode qr-value="" size="12rem"></DisplayQrCode>
|
|
207
|
+
<DisplayQrCode qr-value="a" size="12rem"></DisplayQrCode>
|
|
208
|
+
<DisplayQrCode qr-value="😀 التوصيل المجاني" size="12rem"></DisplayQrCode>
|
|
167
209
|
</div>
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
</div>
|
|
181
|
-
<div style="margin-bottom: 20px;">
|
|
182
|
-
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #374151;">Style Variants:</label>
|
|
183
|
-
<div style="display: grid; gap: 8px;">
|
|
184
|
-
<div><label style="font-size: 12px; color: #6b7280;">Inner:</label><select v-model="variant.inner" style="width: 100%; padding: 4px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 12px;"><option v-for="option in variantOptions" :key="option" :value="option">{{ option }}</option></select></div>
|
|
185
|
-
<div><label style="font-size: 12px; color: #6b7280;">Marker:</label><select v-model="variant.marker" style="width: 100%; padding: 4px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 12px;"><option v-for="option in variantOptions" :key="option" :value="option">{{ option }}</option></select></div>
|
|
186
|
-
<div><label style="font-size: 12px; color: #6b7280;">Pixel:</label><select v-model="variant.pixel" style="width: 100%; padding: 4px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 12px;"><option v-for="option in variantOptions" :key="option" :value="option">{{ option }}</option></select></div>
|
|
187
|
-
</div>
|
|
188
|
-
</div>
|
|
189
|
-
<div style="margin-bottom: 20px;">
|
|
190
|
-
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #374151;">Appearance:</label>
|
|
191
|
-
<div style="display: grid; gap: 12px;">
|
|
192
|
-
<div><label style="font-size: 12px; color: #6b7280;">Size: {{ sizeValue }}px</label><input v-model.number="sizeValue" type="range" min="128" max="400" step="16" style="width: 100%;" /></div>
|
|
193
|
-
<div><label style="font-size: 12px; color: #6b7280;">Radius: {{ radius }}px</label><input v-model.number="radius" type="range" min="0" max="20" step="1" style="width: 100%;" /></div>
|
|
194
|
-
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
|
|
195
|
-
<div><label style="font-size: 12px; color: #6b7280;">Foreground:</label><input v-model="blackColor" type="color" style="width: 100%; height: 32px; border: 1px solid #d1d5db; border-radius: 4px;" /></div>
|
|
196
|
-
<div><label style="font-size: 12px; color: #6b7280;">Background:</label><input v-model="whiteColor" type="color" style="width: 100%; height: 32px; border: 1px solid #d1d5db; border-radius: 4px;" /></div>
|
|
197
|
-
</div>
|
|
198
|
-
</div>
|
|
199
|
-
</div>
|
|
210
|
+
|
|
211
|
+
<p style="margin: 0;">2,900 characters, just under capacity: the densest code the encoder makes.</p>
|
|
212
|
+
<DisplayQrCode :qr-value="nearCapacity" size="32rem"></DisplayQrCode>
|
|
213
|
+
|
|
214
|
+
<p style="margin: 0;">Over capacity: the <code>fallback</code> slot renders in place of the code (left), or nothing (right) when the slot is empty.</p>
|
|
215
|
+
<div style="display: flex; flex-wrap: wrap; gap: 2.4rem; align-items: start;">
|
|
216
|
+
<DisplayQrCode :qr-value="overCapacity" size="16rem" style="outline: 0.1rem dashed currentColor;">
|
|
217
|
+
<template #fallback>
|
|
218
|
+
<span style="padding: 1.2rem; text-align: center;">Thislinkistoolongtoshowasaqrcodepleaseusethebuttonbelow</span>
|
|
219
|
+
</template>
|
|
220
|
+
</DisplayQrCode>
|
|
221
|
+
<DisplayQrCode :qr-value="overCapacity" size="16rem" style="outline: 0.1rem dashed currentColor;"></DisplayQrCode>
|
|
200
222
|
</div>
|
|
201
|
-
</div>
|
|
202
|
-
</div>
|
|
203
|
-
`,
|
|
204
|
-
});
|
|
205
223
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
224
|
+
<p style="margin: 0;">An 80rem <code>size</code> inside an 18rem column: the code shrinks to the column.</p>
|
|
225
|
+
<div style="inline-size: 18rem; outline: 0.1rem dashed currentColor;">
|
|
226
|
+
<DisplayQrCode qr-value="https://example.com" size="80rem"></DisplayQrCode>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<p style="margin: 0;">Out-of-range radius (-1 and 5): the library clamps it to 0 and 1.</p>
|
|
230
|
+
<div style="display: flex; flex-wrap: wrap; gap: 2.4rem;">
|
|
231
|
+
<DisplayQrCode qr-value="https://example.com" size="12rem" :radius="-1" :variant="{ inner: 'rounded', marker: 'rounded', pixel: 'rounded' }"></DisplayQrCode>
|
|
232
|
+
<DisplayQrCode qr-value="https://example.com" size="12rem" :radius="5" :variant="{ inner: 'rounded', marker: 'rounded', pixel: 'rounded' }"></DisplayQrCode>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
`,
|
|
236
|
+
}),
|
|
237
|
+
parameters: {
|
|
238
|
+
controls: { disable: true },
|
|
239
|
+
docs: {
|
|
240
|
+
story: { inline: false, height: "1400px" },
|
|
241
|
+
description: {
|
|
242
|
+
story:
|
|
243
|
+
"Worst-case values. Check: empty, single-character, emoji and right-to-left values all render a code; the near-capacity code renders without errors; an over-capacity value shows the `fallback` slot (with its unbroken text wrapping inside the box) or an empty box, never an error; an oversized `size` shrinks to its container; an out-of-range radius renders as 0 and 1.",
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
},
|
|
214
247
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { defineComponent, h, nextTick } from "vue";
|
|
2
3
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
4
|
import DisplayQrCode from "../DisplayQrCode.vue";
|
|
4
5
|
import type { QrCodeVariant } from "~/types/components";
|
|
@@ -152,3 +153,60 @@ describe("DisplayQrCode accessibility", () => {
|
|
|
152
153
|
expect(wrapper.attributes("aria-label")).toBe("Scan to open our booking page");
|
|
153
154
|
});
|
|
154
155
|
});
|
|
156
|
+
|
|
157
|
+
// Stands in for nuxt-qrcode, whose encoder throws "Data too long" while rendering over-capacity values.
|
|
158
|
+
const QrcodeStub = defineComponent({
|
|
159
|
+
props: { value: { type: String, required: true } },
|
|
160
|
+
setup(stubProps) {
|
|
161
|
+
return () => {
|
|
162
|
+
if (stubProps.value.length > 2953) throw new Error("Data too long");
|
|
163
|
+
return h("svg");
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const mountWithEncoder = (props: { qrValue: string; styleClassPassthrough?: string }, slots: Record<string, string> = {}) =>
|
|
169
|
+
mountSuspended(DisplayQrCode, { props, slots, global: { stubs: { Qrcode: QrcodeStub } } });
|
|
170
|
+
|
|
171
|
+
describe("DisplayQrCode worst-case values", () => {
|
|
172
|
+
const tooLong = "x".repeat(5000);
|
|
173
|
+
|
|
174
|
+
it("renders an empty value without throwing", async () => {
|
|
175
|
+
const wrapper = await mountWithEncoder({ qrValue: "" });
|
|
176
|
+
expect(wrapper.attributes("role")).toBe("img");
|
|
177
|
+
expect(wrapper.attributes("data-error")).toBeUndefined();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("encodes emoji and right-to-left text", async () => {
|
|
181
|
+
const wrapper = await mountWithEncoder({ qrValue: "😀 التوصيل المجاني" });
|
|
182
|
+
expect(wrapper.attributes("role")).toBe("img");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("renders the fallback instead of throwing when the value exceeds QR capacity", async () => {
|
|
186
|
+
const wrapper = await mountWithEncoder({ qrValue: tooLong, styleClassPassthrough: "extra" }, { fallback: '<span class="custom-fallback">Too long to encode</span>' });
|
|
187
|
+
await nextTick();
|
|
188
|
+
expect(wrapper.find(".display-qr-code-fallback").exists()).toBe(true);
|
|
189
|
+
expect(wrapper.attributes("data-error")).toBeDefined();
|
|
190
|
+
expect(wrapper.attributes("role")).toBeUndefined();
|
|
191
|
+
expect(wrapper.classes()).toContain("extra");
|
|
192
|
+
expect(wrapper.find(".custom-fallback").text()).toBe("Too long to encode");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("emits error when the value exceeds QR capacity", async () => {
|
|
196
|
+
const wrapper = await mountWithEncoder({ qrValue: tooLong });
|
|
197
|
+
await nextTick();
|
|
198
|
+
const events = wrapper.emitted("error");
|
|
199
|
+
expect(events).toHaveLength(1);
|
|
200
|
+
expect(events?.[0]?.[0]).toBeInstanceOf(Error);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("recovers when the value changes to one that fits", async () => {
|
|
204
|
+
const wrapper = await mountWithEncoder({ qrValue: tooLong });
|
|
205
|
+
await nextTick();
|
|
206
|
+
expect(wrapper.find(".display-qr-code-fallback").exists()).toBe(true);
|
|
207
|
+
await wrapper.setProps({ qrValue: "https://example.com" });
|
|
208
|
+
await nextTick();
|
|
209
|
+
expect(wrapper.find(".display-qr-code-fallback").exists()).toBe(false);
|
|
210
|
+
expect(wrapper.attributes("role")).toBe("img");
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -22,13 +22,21 @@ The library theme also sets the colour and size tokens on `:root` (`03.theming/_
|
|
|
22
22
|
|
|
23
23
|
No private `--_` tokens.
|
|
24
24
|
|
|
25
|
+
No line-clamp token: an eyebrow is a short label, and clamping needs `overflow: hidden`, which
|
|
26
|
+
clips the default italic overhang. Long text wraps (`overflow-wrap: anywhere`). A parent that needs
|
|
27
|
+
a cap clamps it itself with an allowance for the overhang, as `ServiceDetail` does.
|
|
28
|
+
|
|
25
29
|
---
|
|
26
30
|
|
|
27
31
|
## State hooks
|
|
28
32
|
|
|
29
33
|
| Class | When |
|
|
30
34
|
|---|---|
|
|
31
|
-
|
|
|
35
|
+
| `[data-font-size="large" \| "medium" \| "small"]` | The `font-size` prop |
|
|
36
|
+
|
|
37
|
+
> Changed 2026-10-19: the size was a bare `large`/`medium`/`small` class on the root, which a
|
|
38
|
+
> consumer's own `.small` or `.large` rule could restyle. Select `.eyebrow-text[data-font-size="small"]`
|
|
39
|
+
> instead of `.eyebrow-text.small`.
|
|
32
40
|
|
|
33
41
|
---
|
|
34
42
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<component :is="tag" class="eyebrow-text" :class="[elementClasses
|
|
2
|
+
<component :is="tag" class="eyebrow-text" :class="[elementClasses]" :data-font-size="fontSize">
|
|
3
3
|
{{ textContent }}
|
|
4
4
|
</component>
|
|
5
5
|
</template>
|
|
@@ -36,16 +36,17 @@ watch(
|
|
|
36
36
|
font-weight: var(--eyebrow-text-font-weight, inherit);
|
|
37
37
|
letter-spacing: var(--eyebrow-text-letter-spacing, inherit);
|
|
38
38
|
color: transparent;
|
|
39
|
+
overflow-wrap: anywhere;
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
&[data-font-size="large"] {
|
|
41
42
|
font-size: var(--eyebrow-text-large, 1.4rem);
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
&[data-font-size="medium"] {
|
|
45
46
|
font-size: var(--eyebrow-text-medium, 1.2rem);
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
&[data-font-size="small"] {
|
|
49
50
|
font-size: var(--eyebrow-text-small, 1rem);
|
|
50
51
|
}
|
|
51
52
|
}
|
package/app/components/01.atoms/text-blocks/eyebrow-text/playwright/eyebrow-text.playwright.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { test, expect, type Page, type Locator } from "@playwright/test";
|
|
|
2
2
|
|
|
3
3
|
// --- Config ---
|
|
4
4
|
const STORYBOOK_URL = "http://127.0.0.1:6006";
|
|
5
|
-
const STORY_BASE = "atoms-text-blocks-
|
|
5
|
+
const STORY_BASE = "atoms-text-blocks-eyebrow-text";
|
|
6
6
|
const HEADING_TIMEOUT = 20_000;
|
|
7
7
|
|
|
8
8
|
// --- Types ---
|