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.
Files changed (111) hide show
  1. package/.claude/rules/consumer/ids-from-useid.md +2 -1
  2. package/.claude/rules/consumer/story-register-components.md +1 -1
  3. package/.claude/rules/consumer/vfor-keys-from-data.md +14 -0
  4. package/.claude/skills/component-compliance-checklist.md +24 -1
  5. package/.claude/skills/component-naming.md +1 -0
  6. package/.claude/skills/components/banner-video.md +24 -30
  7. package/.claude/skills/components/content-docs.md +18 -5
  8. package/.claude/skills/components/display-dialog.md +16 -6
  9. package/.claude/skills/components/display-qr-code.md +22 -4
  10. package/.claude/skills/components/{display-tooltip-defined.md → display-tooltip-rich.md} +40 -42
  11. package/.claude/skills/components/display-tooltip.md +17 -6
  12. package/.claude/skills/components/eyebrow-text.md +9 -5
  13. package/.claude/skills/components/header-block.md +13 -5
  14. package/.claude/skills/components/hero-text.md +13 -9
  15. package/.claude/skills/components/link-text.md +8 -5
  16. package/.claude/skills/components/pop-over.md +17 -3
  17. package/.claude/skills/components/skip-links.md +16 -0
  18. package/.claude/skills/composable-tooltips-guide.md +4 -4
  19. package/.claude/skills/index.md +6 -6
  20. package/.claude/skills/storybook-add-story.md +5 -2
  21. package/.claude/skills/testing-add-playwright.md +12 -7
  22. package/.claude/skills/testing-add-unit-test.md +8 -0
  23. package/.vscode/srcdev-component-banner-video.code-snippets +38 -25
  24. package/.vscode/srcdev-component-content-docs.code-snippets +69 -60
  25. package/.vscode/srcdev-component-display-dialog.code-snippets +7 -5
  26. package/.vscode/srcdev-component-display-qr-code.code-snippets +12 -1
  27. package/.vscode/srcdev-component-display-tooltip-rich.code-snippets +50 -0
  28. package/.vscode/srcdev-component-display-tooltip.code-snippets +31 -0
  29. package/.vscode/srcdev-component-hero-text.code-snippets +21 -5
  30. package/.vscode/srcdev-component-link-text.code-snippets +3 -3
  31. package/.vscode/srcdev-component-pop-over.code-snippets +28 -0
  32. package/.vscode/srcdev-component-skip-links.code-snippets +1 -1
  33. package/app/components/01.atoms/banner-video/BannerVideo.vue +51 -19
  34. package/app/components/01.atoms/banner-video/CONSUMER-STYLING.md +59 -40
  35. package/app/components/01.atoms/banner-video/stories/BannerVideo.stories.ts +88 -1
  36. package/app/components/01.atoms/banner-video/tests/BannerVideo.spec.ts +127 -78
  37. package/app/components/01.atoms/banner-video/tests/__snapshots__/BannerVideo.spec.ts.snap +12 -4
  38. package/app/components/01.atoms/content-wrappers/docs-pages/CONSUMER-STYLING.md +42 -14
  39. package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +86 -112
  40. package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +134 -8
  41. package/app/components/01.atoms/content-wrappers/docs-pages/tests/ContentDocs.spec.ts +131 -65
  42. package/app/components/01.atoms/display-dialog/CONSUMER-STYLING.md +28 -3
  43. package/app/components/01.atoms/display-dialog/DisplayDialog.vue +39 -31
  44. package/app/components/01.atoms/display-dialog/stories/DisplayDialog.stories.ts +92 -1
  45. package/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts +87 -41
  46. package/app/components/01.atoms/display-dialog/tests/__snapshots__/DisplayDialog.spec.ts.snap +5 -1
  47. package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +70 -24
  48. package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +24 -12
  49. package/app/components/01.atoms/display-tooltip/stories/DisplayTooltip.stories.ts +89 -2
  50. package/app/components/01.atoms/display-tooltip/tests/DisplayTooltip.spec.ts +65 -18
  51. package/app/components/01.atoms/display-tooltip/tests/__snapshots__/DisplayTooltip.spec.ts.snap +3 -3
  52. package/app/components/01.atoms/navigation/skip-links/CONSUMER-STYLING.md +65 -19
  53. package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +20 -14
  54. package/app/components/01.atoms/navigation/skip-links/stories/SkipLinks.stories.ts +89 -20
  55. package/app/components/01.atoms/navigation/skip-links/tests/SkipLinks.spec.ts +70 -10
  56. package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +42 -9
  57. package/app/components/01.atoms/pop-over/PopOver.vue +28 -12
  58. package/app/components/01.atoms/pop-over/stories/PopOver.stories.ts +114 -2
  59. package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +35 -0
  60. package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +5 -1
  61. package/app/components/01.atoms/qr-code/CONSUMER-STYLING.md +16 -3
  62. package/app/components/01.atoms/qr-code/DisplayQrCode.vue +31 -0
  63. package/app/components/01.atoms/qr-code/stories/DisplayQrCode.stories.ts +190 -157
  64. package/app/components/01.atoms/qr-code/tests/DisplayQrCode.spec.ts +58 -0
  65. package/app/components/01.atoms/text-blocks/eyebrow-text/CONSUMER-STYLING.md +9 -1
  66. package/app/components/01.atoms/text-blocks/eyebrow-text/EyebrowText.vue +5 -4
  67. package/app/components/01.atoms/text-blocks/eyebrow-text/playwright/eyebrow-text.playwright.ts +1 -1
  68. package/app/components/01.atoms/text-blocks/eyebrow-text/stories/EyebrowText.stories.ts +104 -17
  69. package/app/components/01.atoms/text-blocks/eyebrow-text/tests/EyebrowText.spec.ts +68 -118
  70. package/app/components/01.atoms/text-blocks/eyebrow-text/tests/__snapshots__/EyebrowText.spec.ts.snap +8 -8
  71. package/app/components/01.atoms/text-blocks/header-block/CONSUMER-STYLING.md +49 -24
  72. package/app/components/01.atoms/text-blocks/header-block/HeaderBlock.vue +9 -1
  73. package/app/components/01.atoms/text-blocks/header-block/stories/HeaderBlock.stories.ts +96 -26
  74. package/app/components/01.atoms/text-blocks/header-block/tests/HeaderBlock.spec.ts +64 -48
  75. package/app/components/01.atoms/text-blocks/header-block/tests/__snapshots__/HeaderBlock.spec.ts.snap +2 -2
  76. package/app/components/01.atoms/text-blocks/hero-text/CONSUMER-STYLING.md +22 -6
  77. package/app/components/01.atoms/text-blocks/hero-text/HeroText.vue +32 -23
  78. package/app/components/01.atoms/text-blocks/hero-text/playwright/hero-text.playwright.ts +1 -1
  79. package/app/components/01.atoms/text-blocks/hero-text/stories/HeroText.stories.ts +176 -23
  80. package/app/components/01.atoms/text-blocks/hero-text/tests/HeroText.spec.ts +129 -123
  81. package/app/components/01.atoms/text-blocks/hero-text/tests/__snapshots__/HeroText.spec.ts.snap +2 -2
  82. package/app/components/01.atoms/text-blocks/link-text/CONSUMER-STYLING.md +17 -0
  83. package/app/components/01.atoms/text-blocks/link-text/LinkText.vue +9 -3
  84. package/app/components/01.atoms/text-blocks/link-text/stories/LinkText.stories.ts +96 -99
  85. package/app/components/01.atoms/text-blocks/link-text/tests/LinkText.spec.ts +84 -93
  86. package/app/components/02.molecules/display-tooltip-rich/CONSUMER-STYLING.md +110 -0
  87. package/app/components/02.molecules/display-tooltip-rich/DisplayTooltipRich.vue +148 -0
  88. package/app/components/02.molecules/display-tooltip-rich/stories/DisplayTooltipRich.stories.ts +254 -0
  89. package/app/components/02.molecules/display-tooltip-rich/tests/DisplayTooltipRich.spec.ts +156 -0
  90. package/app/components/02.molecules/display-tooltip-rich/tests/__snapshots__/DisplayTooltipRich.spec.ts.snap +22 -0
  91. package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +1 -0
  92. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +1 -1
  93. package/app/components/02.molecules/price-list/tests/__snapshots__/PriceList.spec.ts.snap +8 -8
  94. package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +1 -0
  95. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +15 -15
  96. package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +3 -3
  97. package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +3 -3
  98. package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +3 -3
  99. package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +1 -1
  100. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +9 -9
  101. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +9 -9
  102. package/app/composables/tests/useTooltips.spec.ts +3 -3
  103. package/app/types/components/index.ts +1 -1
  104. package/package.json +1 -1
  105. package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +0 -29
  106. package/app/components/02.molecules/display-tooltip-defined/CONSUMER-STYLING.md +0 -53
  107. package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +0 -117
  108. package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts +0 -129
  109. package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts +0 -85
  110. package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +0 -17
  111. /package/app/types/components/{display-tooltip-defined.d.ts → display-tooltip-rich.d.ts} +0 -0
@@ -1,214 +1,247 @@
1
- import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
- import DisplayQrCodeComponent from "../DisplayQrCode.vue";
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
- const meta: Meta<typeof DisplayQrCodeComponent> = {
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: DisplayQrCodeComponent,
31
+ component: DisplayQrCode,
8
32
  argTypes: {
9
33
  qrValue: {
10
34
  control: { type: "text" },
11
- description: "The value/content to encode in the QR code",
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: "Size of the QR code (CSS value like '256px', '10rem')",
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: 20, step: 1 },
21
- description: "Border radius of the QR code corners",
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: "Color of the QR code foreground elements",
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: "Color of the QR code background",
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 for a more specific or translated label)",
60
+ description: "Accessible name for the QR image (override with a more specific or translated label)",
37
61
  table: { category: "Accessibility" },
38
62
  },
39
- variant: { table: { disable: true } },
40
- styleClassPassthrough: { table: { disable: true } },
63
+ variantInner: variantArgType("inner"),
64
+ variantMarker: variantArgType("marker"),
65
+ variantPixel: variantArgType("pixel"),
41
66
  },
42
67
  args: {
43
- qrValue: "https://github.com/srcdev/nuxt-components",
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
- styleClassPassthrough: [],
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
- interface QrStoryArgs {
56
- qrValue: string;
57
- size: string;
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: 40px; display: flex; align-items: center; justify-content: center; background: #f5f5f5; border-radius: 8px;">
77
- <DisplayQrCodeComponent
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
- export const Default = DisplayTemplate.bind({});
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 Website = DisplayTemplate.bind({});
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 VariantDefault = DisplayTemplate.bind({});
98
- VariantDefault.args = { qrValue: "Default variant style", radius: 4 };
99
- VariantDefault.parameters = { variant: { inner: "default", marker: "default", pixel: "default" } };
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 = DisplayTemplate.bind({});
102
- VariantCircle.args = { qrValue: "Circle variant style", radius: 8, blackColor: "#1e40af" };
103
- VariantCircle.parameters = { variant: { inner: "circle", marker: "circle", pixel: "circle" } };
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 = DisplayTemplate.bind({});
106
- VariantRounded.args = { qrValue: "Rounded variant style", radius: 12, blackColor: "#059669" };
107
- VariantRounded.parameters = { variant: { inner: "rounded", marker: "rounded", pixel: "rounded" } };
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 = DisplayTemplate.bind({});
110
- VariantDots.args = { qrValue: "Dots variant style", radius: 6, blackColor: "#dc2626" };
111
- VariantDots.parameters = { variant: { inner: "dots", marker: "dots", pixel: "dots" } };
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 VariantMixed = DisplayTemplate.bind({});
114
- VariantMixed.args = { qrValue: "Mixed variants for creative styling", radius: 10, blackColor: "#7c3aed" };
115
- VariantMixed.parameters = { variant: { inner: "circle", marker: "rounded", pixel: "dots" } };
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 = DisplayTemplate.bind({});
118
- WiFiConnection.args = { qrValue: "WIFI:T:WPA;S:MyHomeNetwork;P:mySecurePassword123;H:false;", blackColor: "#1e40af", radius: 4 };
119
- WiFiConnection.parameters = { variant: { inner: "rounded", marker: "circle", pixel: "default" } };
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 = DisplayTemplate.bind({});
122
- VCardContact.args = {
123
- qrValue: "BEGIN:VCARD\\nVERSION:3.0\\nFN:John Doe\\nORG:Acme Corp\\nTITLE:Software Engineer\\nTEL:+1234567890\\nEMAIL:john.doe@acme.com\\nURL:https://johndoe.dev\\nEND:VCARD",
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: 12,
126
- };
127
- VCardContact.parameters = { variant: { inner: "dots", marker: "rounded", pixel: "circle" } };
128
-
129
- export const SmallSize = DisplayTemplate.bind({});
130
- SmallSize.args = { qrValue: "Small QR for tight spaces", size: "128px", radius: 2, blackColor: "#f59e0b" };
131
- SmallSize.parameters = { variant: { inner: "dots", marker: "circle", pixel: "rounded" } };
132
-
133
- export const LargeSize = DisplayTemplate.bind({});
134
- LargeSize.args = { qrValue: "Large QR for posters and signage", size: "400px", radius: 16, blackColor: "#0ea5e9" };
135
- LargeSize.parameters = { variant: { inner: "circle", marker: "rounded", pixel: "dots" } };
136
-
137
- export const HighContrast = DisplayTemplate.bind({});
138
- HighContrast.args = { qrValue: "High contrast for accessibility", blackColor: "#000000", whiteColor: "#ffffff", radius: 0, size: "256px" };
139
- HighContrast.parameters = { variant: { inner: "default", marker: "default", pixel: "default" } };
140
-
141
- const PlaygroundTemplate: StoryFn<QrStoryArgs> = (args) => ({
142
- components: { DisplayQrCodeComponent },
143
- setup() {
144
- const qrValue = ref(args.qrValue);
145
- const variant = ref<QrCodeVariant>({ inner: "circle", marker: "rounded", pixel: "dots" });
146
- const radius = ref(args.radius);
147
- const blackColor = ref(args.blackColor);
148
- const whiteColor = ref(args.whiteColor);
149
- const sizeValue = ref(256);
150
- const size = computed(() => `${sizeValue.value}px`);
151
- const presetValues = [
152
- { label: "Website URL", value: "https://nuxt.com" },
153
- { label: "WiFi Network", value: "WIFI:T:WPA;S:MyNetwork;P:password123;" },
154
- { label: "Phone Number", value: "tel:+1234567890" },
155
- { label: "Email", value: "mailto:hello@example.com" },
156
- { label: "SMS", value: "sms:+1234567890?body=Hello!" },
157
- { label: "Location", value: "geo:40.7128,-74.0060" },
158
- ];
159
- const variantOptions = ["default", "circle", "rounded", "dots"];
160
- return { qrValue, variant, radius, blackColor, whiteColor, sizeValue, size, presetValues, variantOptions };
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
- <div style="background: white; padding: 24px; border-radius: 8px; border: 1px solid #e5e7eb;">
169
- <h3 style="margin: 0 0 20px 0; color: #374151; font-size: 18px;">QR Code Playground</h3>
170
- <div style="margin-bottom: 20px;">
171
- <label style="display: block; margin-bottom: 8px; font-weight: 500; color: #374151;">Quick Presets:</label>
172
- <select @change="qrValue = $event.target.value" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px;">
173
- <option value="">Select a preset...</option>
174
- <option v-for="preset in presetValues" :key="preset.label" :value="preset.value">{{ preset.label }}</option>
175
- </select>
176
- </div>
177
- <div style="margin-bottom: 20px;">
178
- <label style="display: block; margin-bottom: 8px; font-weight: 500; color: #374151;">Custom Content:</label>
179
- <textarea v-model="qrValue" rows="3" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px; font-family: monospace; font-size: 12px;" placeholder="Enter QR code content..." />
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
- export const InteractivePlayground = PlaygroundTemplate.bind({});
207
- InteractivePlayground.args = {
208
- qrValue: "https://github.com/srcdev/nuxt-components",
209
- radius: 8,
210
- blackColor: "#000000",
211
- whiteColor: "transparent",
212
- size: "256px",
213
- styleClassPassthrough: [],
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
- | `.eyebrow-text.large`, `.medium`, `.small` | The `font-size` prop |
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, props.fontSize]">
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
- &.large {
41
+ &[data-font-size="large"] {
41
42
  font-size: var(--eyebrow-text-large, 1.4rem);
42
43
  }
43
44
 
44
- &.medium {
45
+ &[data-font-size="medium"] {
45
46
  font-size: var(--eyebrow-text-medium, 1.2rem);
46
47
  }
47
48
 
48
- &.small {
49
+ &[data-font-size="small"] {
49
50
  font-size: var(--eyebrow-text-small, 1rem);
50
51
  }
51
52
  }
@@ -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-eyebrowtext";
5
+ const STORY_BASE = "atoms-text-blocks-eyebrow-text";
6
6
  const HEADING_TIMEOUT = 20_000;
7
7
 
8
8
  // --- Types ---