srcdev-nuxt-components 9.5.7 → 9.6.1

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 (159) hide show
  1. package/.claude/skills/component-compliance-checklist.md +8 -2
  2. package/.claude/skills/component-dynamic-slots.md +3 -3
  3. package/.claude/skills/component-export-types.md +10 -16
  4. package/.claude/skills/component-naming.md +1 -1
  5. package/.claude/skills/components/alert-content.md +9 -1
  6. package/.claude/skills/components/breadcrumb.md +5 -0
  7. package/.claude/skills/components/container-glow.md +21 -8
  8. package/.claude/skills/components/display-avatar.md +13 -7
  9. package/.claude/skills/components/display-chip.md +37 -24
  10. package/.claude/skills/components/display-pill.md +58 -8
  11. package/.claude/skills/components/display-prompt.md +8 -5
  12. package/.claude/skills/components/display-toast.md +21 -4
  13. package/.claude/skills/components/glowing-border.md +20 -2
  14. package/.claude/skills/components/input-button.md +3 -0
  15. package/.claude/skills/components/responsive-header.md +59 -0
  16. package/.claude/skills/components/select-menu.md +54 -5
  17. package/.claude/skills/components/service-detail.md +32 -0
  18. package/.claude/skills/components/service-summary.md +8 -0
  19. package/.claude/skills/components/services-card-grid.md +5 -3
  20. package/.claude/skills/components/services-card.md +22 -12
  21. package/.claude/skills/components/site-header.md +2 -2
  22. package/.claude/skills/components/slider-gallery.md +10 -4
  23. package/.claude/skills/components/tabbed-content.md +300 -0
  24. package/.claude/skills/composable-anchor-scroll.md +16 -15
  25. package/.claude/skills/index.md +4 -5
  26. package/.claude/skills/storybook-add-story.md +53 -0
  27. package/.claude/skills/theming-status-tokens.md +60 -0
  28. package/.vscode/srcdev-component-display-avatar.code-snippets +2 -1
  29. package/.vscode/srcdev-component-display-chip.code-snippets +3 -2
  30. package/.vscode/srcdev-component-display-pill.code-snippets +10 -0
  31. package/.vscode/srcdev-component-responsive-header.code-snippets +16 -0
  32. package/.vscode/srcdev-component-select-menu.code-snippets +28 -0
  33. package/.vscode/srcdev-component-service-detail.code-snippets +1 -0
  34. package/.vscode/srcdev-component-service-summary.code-snippets +3 -1
  35. package/.vscode/srcdev-component-services-card.code-snippets +38 -36
  36. package/.vscode/srcdev-component-services-grids.code-snippets +2 -1
  37. package/.vscode/{srcdev-component-tabs-core.code-snippets → srcdev-component-tabbed-content.code-snippets} +35 -16
  38. package/app/assets/styles/setup/03.theming/_status.css +24 -0
  39. package/app/assets/styles/setup/03.theming/index.css +1 -0
  40. package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +39 -35
  41. package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +25 -15
  42. package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +130 -24
  43. package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +45 -0
  44. package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +2 -2
  45. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +76 -19
  46. package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +12 -10
  47. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +201 -42
  48. package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +15 -4
  49. package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +1 -1
  50. package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +78 -9
  51. package/app/components/01.atoms/display-pill/DisplayPill.vue +59 -23
  52. package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +240 -16
  53. package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +43 -14
  54. package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +3 -3
  55. package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +16 -4
  56. package/app/components/01.atoms/navigation/breadcrumb/CONSUMER-STYLING.md +28 -5
  57. package/app/components/01.atoms/navigation/breadcrumb/stories/Breadcrumb.stories.ts +94 -0
  58. package/app/components/01.atoms/navigation/breadcrumb/tests/Breadcrumb.spec.ts +27 -0
  59. package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +201 -0
  60. package/app/components/01.atoms/tabbed-content/TabbedContent.vue +750 -0
  61. package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +279 -0
  62. package/app/components/01.atoms/tabbed-content/tests/TabbedContent.spec.ts +816 -0
  63. package/app/components/02.molecules/alert-content/AlertContent.vue +11 -2
  64. package/app/components/02.molecules/alert-content/AlertContentInner.vue +19 -0
  65. package/app/components/02.molecules/alert-content/CONSUMER-STYLING.md +17 -4
  66. package/app/components/02.molecules/alert-content/stories/AlertContent.stories.ts +200 -17
  67. package/app/components/02.molecules/alert-content/tests/AlertContent.spec.ts +33 -0
  68. package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +7 -5
  69. package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +6 -2
  70. package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +24 -3
  71. package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +19 -0
  72. package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +2 -1
  73. package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +52 -19
  74. package/app/components/02.molecules/display-chip/DisplayChip.vue +72 -54
  75. package/app/components/02.molecules/display-chip/stories/DisplayChip.stories.ts +103 -6
  76. package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +98 -29
  77. package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +8 -4
  78. package/app/components/02.molecules/prompt/CONSUMER-STYLING.md +9 -1
  79. package/app/components/02.molecules/prompt/DisplayPrompt.vue +2 -2
  80. package/app/components/02.molecules/prompt/stories/DisplayPrompt.stories.ts +208 -71
  81. package/app/components/02.molecules/prompt/tests/DisplayPrompt.spec.ts +19 -6
  82. package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +66 -8
  83. package/app/components/02.molecules/select-menu/SelectMenu.vue +141 -18
  84. package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +232 -2
  85. package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +151 -0
  86. package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +6 -2
  87. package/app/components/02.molecules/toast/CONSUMER-STYLING.md +91 -27
  88. package/app/components/02.molecules/toast/DisplayToast.vue +109 -50
  89. package/app/components/02.molecules/toast/DisplayToastProvider.vue +109 -45
  90. package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +23 -0
  91. package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +73 -0
  92. package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +132 -18
  93. package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +105 -39
  94. package/app/components/03.organisms/image-galleries/slider-gallery/CONSUMER-STYLING.md +19 -1
  95. package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +65 -7
  96. package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +201 -63
  97. package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +41 -2
  98. package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +6 -6
  99. package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +42 -11
  100. package/app/components/03.organisms/responsive-header/NavigationItems.vue +60 -16
  101. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +86 -24
  102. package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +52 -0
  103. package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +20 -0
  104. package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +40 -0
  105. package/app/components/03.organisms/services/service-detail/CONSUMER-STYLING.md +92 -41
  106. package/app/components/03.organisms/services/service-detail/ServiceDetail.vue +198 -73
  107. package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts +157 -9
  108. package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +58 -0
  109. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +5 -4
  110. package/app/components/03.organisms/services/service-summary/CONSUMER-STYLING.md +30 -0
  111. package/app/components/03.organisms/services/service-summary/ServiceSummary.vue +32 -8
  112. package/app/components/03.organisms/services/service-summary/stories/ServiceSummary.stories.ts +147 -0
  113. package/app/components/03.organisms/services/service-summary/tests/ServiceSummary.spec.ts +26 -0
  114. package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +2 -2
  115. package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +104 -63
  116. package/app/components/03.organisms/services/services-card/ServicesCard.vue +92 -66
  117. package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +194 -27
  118. package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +34 -8
  119. package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +8 -8
  120. package/app/components/03.organisms/services/services-grids/CONSUMER-STYLING.md +9 -0
  121. package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +2 -1
  122. package/app/components/03.organisms/services/services-grids/stories/ServiceSummaryGrid.stories.ts +46 -6
  123. package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +89 -11
  124. package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +8 -0
  125. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +6 -6
  126. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +24 -24
  127. package/app/components/05.forms/input-button/CONSUMER-STYLING.md +8 -0
  128. package/app/components/05.forms/input-button/InputButton.vue +3 -1
  129. package/app/composables/tests/useAnchorScroll.spec.ts +8 -0
  130. package/app/composables/useAnchorScroll.ts +1 -1
  131. package/app/composables/useTabs.ts +148 -206
  132. package/app/types/components/display-chip.d.ts +8 -4
  133. package/app/types/components/display-toast.d.ts +2 -0
  134. package/app/types/components/index.ts +0 -1
  135. package/app/types/components/select-menu.d.ts +2 -0
  136. package/app/utils/pausableTimeout.ts +41 -0
  137. package/nuxt.config.ts +1 -1
  138. package/package.json +1 -1
  139. package/.claude/skills/components/site-navigation.md +0 -145
  140. package/.claude/skills/components/tab-navigation.md +0 -272
  141. package/.claude/skills/components/tabs-core.md +0 -187
  142. package/.vscode/srcdev-component-site-navigation.code-snippets +0 -38
  143. package/.vscode/srcdev-component-tab-navigation.code-snippets +0 -30
  144. package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +0 -120
  145. package/app/components/01.atoms/navigation/tabs/TabsCore.vue +0 -272
  146. package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +0 -199
  147. package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +0 -274
  148. package/app/components/02.molecules/navigation/site-navigation/CONSUMER-STYLING.md +0 -109
  149. package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +0 -751
  150. package/app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts +0 -356
  151. package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +0 -367
  152. package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +0 -30
  153. package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +0 -150
  154. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +0 -518
  155. package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +0 -340
  156. package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +0 -339
  157. package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +0 -33
  158. package/app/composables/useNavCollapse.ts +0 -86
  159. package/app/types/components/nav-item.d.ts +0 -11
@@ -1,5 +1,10 @@
1
1
  import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
+ import { computed, ref } from "vue";
2
3
  import StorybookComponent from "../GlowingBorder.vue";
4
+ import InputTextCore from "../../../../05.forms/input-text/InputTextCore.vue";
5
+ import InputButton from "../../../../05.forms/input-button/InputButton.vue";
6
+ import CanvasSwitcher from "../../../canvas-switcher/CanvasSwitcher.vue";
7
+ import type { MediaCanvas } from "~/types/components";
3
8
 
4
9
  const PX_OPTIONS = ["1px", "2px", "3px", "4px", "5px", "6px"] as const;
5
10
  const DURATION_OPTIONS = ["5s", "6s", "7s", "8s", "9s", "10s", "11s", "12s", "13s", "14s", "15s"] as const;
@@ -11,8 +16,24 @@ type StoryArgs = {
11
16
  borderWidth: (typeof PX_OPTIONS)[number];
12
17
  borderRadius: (typeof PX_OPTIONS)[number];
13
18
  animationDuration: (typeof DURATION_OPTIONS)[number];
19
+ canvasBackground: string;
20
+ surface: string;
14
21
  };
15
22
 
23
+ const darkColours = ["#000000", "#020024", "#0f0f23", "#1e293b", "#262626"];
24
+ const darkColourLabels = {
25
+ "": "Theme default (light)",
26
+ "#000000": "Black",
27
+ "#020024": "Midnight",
28
+ "#0f0f23": "Ink",
29
+ "#1e293b": "Slate",
30
+ "#262626": "Charcoal",
31
+ };
32
+ const surfaceLabels = { ...darkColourLabels, "#ffffff": "White" };
33
+ const isDark = (colour: string) => darkColours.includes(colour);
34
+
35
+ const tokenCategory = "CSS tokens (story only, set in your CSS)";
36
+
16
37
  export default {
17
38
  title: "Atoms/Effects/GlowingBorder",
18
39
  component: StorybookComponent,
@@ -29,25 +50,41 @@ export default {
29
50
  },
30
51
  content: {
31
52
  control: { type: "text" },
32
- description: "Slot content",
53
+ description: "**Story control, not a prop.** Text placed in the default slot.",
54
+ table: { category: "Slot content (story only)" },
33
55
  },
34
56
  borderWidth: {
35
57
  control: { type: "select" },
36
58
  options: PX_OPTIONS,
37
- description: "--glowing-border-width",
38
- table: { category: "CSS tokens" },
59
+ description: "**Story control, not a prop.** Sets `--glowing-border-width`. In your CSS: `.promo { --glowing-border-width: 4px; }`",
60
+ table: { category: tokenCategory },
39
61
  },
40
62
  borderRadius: {
41
63
  control: { type: "select" },
42
64
  options: PX_OPTIONS,
43
- description: "--glowing-border-radius",
44
- table: { category: "CSS tokens" },
65
+ description: "**Story control, not a prop.** Sets `--glowing-border-radius`. In your CSS: `.promo { --glowing-border-radius: 6px; }`",
66
+ table: { category: tokenCategory },
45
67
  },
46
68
  animationDuration: {
47
69
  control: { type: "select" },
48
70
  options: DURATION_OPTIONS,
49
- description: "--glowing-border-animation-duration",
50
- table: { category: "CSS tokens" },
71
+ description:
72
+ "**Story control, not a prop.** Sets `--glowing-border-animation-duration`. In your CSS: `.promo { --glowing-border-animation-duration: 6s; }`",
73
+ table: { category: tokenCategory },
74
+ },
75
+ surface: {
76
+ control: { type: "select", labels: surfaceLabels },
77
+ options: ["", "#ffffff", ...darkColours],
78
+ description:
79
+ "**Story control, not a prop.** Sets `--glowing-border-surface` (and a light `--glowing-border-text-colour` for dark values). In your CSS: `.promo { --glowing-border-surface: #0f0f23; --glowing-border-text-colour: #f3f4f6; }`",
80
+ table: { category: tokenCategory },
81
+ },
82
+ canvasBackground: {
83
+ control: { type: "select", labels: darkColourLabels },
84
+ options: ["", ...darkColours],
85
+ description:
86
+ "**Story control, not a prop.** Background of the demo stage, so the glow can be checked on a dark page.",
87
+ table: { category: "Story canvas (story only)" },
51
88
  },
52
89
  styleClassPassthrough: {
53
90
  table: { disable: true },
@@ -66,26 +103,43 @@ export default {
66
103
  borderWidth: "3px",
67
104
  borderRadius: "6px",
68
105
  animationDuration: "10s",
106
+ canvasBackground: "",
107
+ surface: "",
108
+ },
109
+ parameters: {
110
+ docs: {
111
+ description: {
112
+ component:
113
+ "Wraps its default slot in an animated conic-gradient glow border. The canvas and surface controls are story-only, for checking the glow against dark themes.",
114
+ },
115
+ },
69
116
  },
70
117
  } as Meta<StoryArgs>;
71
118
 
119
+ function useStorySetup(args: StoryArgs) {
120
+ const stageStyle = computed(() => ({
121
+ background: args.canvasBackground || undefined,
122
+ }));
123
+ const tokenStyle = computed(() => ({
124
+ "--glowing-border-width": args.borderWidth,
125
+ "--glowing-border-radius": args.borderRadius,
126
+ "--glowing-border-animation-duration": args.animationDuration,
127
+ "--glowing-border-surface": args.surface || undefined,
128
+ "--glowing-border-text-colour":
129
+ isDark(args.surface) ? "#f3f4f6" : undefined,
130
+ }));
131
+ return { args, stageStyle, tokenStyle };
132
+ }
133
+
72
134
  const Template: StoryFn<StoryArgs> = (args) => ({
73
135
  components: { StorybookComponent },
74
136
  setup() {
75
- return { args };
137
+ return useStorySetup(args);
76
138
  },
77
139
  template: `
78
- <div style="padding: 40px; background-color: var(--slate-10);">
79
- <StorybookComponent
80
- :tag="args.tag"
81
- :variant="args.variant"
82
- :style="{
83
- '--glowing-border-width': args.borderWidth,
84
- '--glowing-border-radius': args.borderRadius,
85
- '--glowing-border-animation-duration': args.animationDuration,
86
- }"
87
- >
88
- <div style="padding: 20px; color: var(--slate-02);">
140
+ <div style="padding: 40px;" :style="stageStyle">
141
+ <StorybookComponent :tag="args.tag" :variant="args.variant" :style="tokenStyle">
142
+ <div style="padding: 20px;">
89
143
  <h3 style="margin: 0 0 12px 0; font-size: 1.5rem; font-weight: 600;">GlowingBorder</h3>
90
144
  <p style="margin: 0; line-height: 1.6;">{{ args.content }}</p>
91
145
  </div>
@@ -127,39 +181,144 @@ Steel.args = {
127
181
  export const Green = Template.bind({});
128
182
  Green.args = {
129
183
  variant: "green",
130
- content:
131
- "Green variant cycles through vibrant, saturated greens for a fresh, energetic glow effect.",
184
+ content: "Green variant cycles through vibrant, saturated greens for a fresh, energetic glow effect.",
185
+ };
186
+
187
+ export const CustomTokens = Template.bind({});
188
+ CustomTokens.args = {
189
+ variant: "vivid",
190
+ content: "Border width, radius, and animation speed all driven by the CSS token controls.",
191
+ borderWidth: "6px",
192
+ borderRadius: "4px",
193
+ animationDuration: "5s",
194
+ };
195
+
196
+ export const HighlightedControls: StoryFn<StoryArgs> = (args) => ({
197
+ components: { StorybookComponent, InputTextCore, InputButton },
198
+ setup() {
199
+ const { args: storyArgs, stageStyle } = useStorySetup(args);
200
+ const glowStyle = (controlRadius: string) =>
201
+ computed(() => ({
202
+ "--glowing-border-width": args.borderWidth,
203
+ "--glowing-border-radius": `calc(${controlRadius} + ${args.borderWidth})`,
204
+ "--glowing-border-animation-duration": args.animationDuration,
205
+ "--glowing-border-surface": "transparent",
206
+ "--glowing-border-overflow": "visible",
207
+ display: "inline-block",
208
+ }));
209
+ const buttonGlowStyle = glowStyle("var(--button-border-radius)");
210
+ const inputGlowStyle = computed(() => ({
211
+ ...glowStyle("var(--form-input-border-radius)").value,
212
+ "--input-text-border": "transparent",
213
+ display: "block",
214
+ }));
215
+ const email = ref("");
216
+ return { args: storyArgs, stageStyle, buttonGlowStyle, inputGlowStyle, email };
217
+ },
218
+ template: `
219
+ <div style="padding: 40px; display: grid; gap: 32px; justify-items: start;" :style="stageStyle">
220
+ <StorybookComponent tag="span" :variant="args.variant" :style="buttonGlowStyle">
221
+ <InputButton type="button" variant="primary" button-text="Book a free consultation"></InputButton>
222
+ </StorybookComponent>
223
+
224
+ <div style="display: grid; gap: 8px; inline-size: min(100%, 40rem);">
225
+ <label for="highlighted-email">Early-access email</label>
226
+ <StorybookComponent :variant="args.variant" :style="inputGlowStyle">
227
+ <InputTextCore
228
+ id="highlighted-email"
229
+ v-model="email"
230
+ type="email"
231
+ name="highlighted-email"
232
+ placeholder="eg. jane@example.com"
233
+ ></InputTextCore>
234
+ </StorybookComponent>
235
+ </div>
236
+ </div>
237
+ `,
238
+ });
239
+ HighlightedControls.args = {
240
+ variant: "vivid",
241
+ borderWidth: "3px",
242
+ };
243
+ HighlightedControls.parameters = {
244
+ docs: {
245
+ description: {
246
+ story:
247
+ "The glow replaces a single control's border, for when a design calls out something special. No changes to the controls: " +
248
+ "InputButton's border already matches its surface, and the input gets `--input-text-border: transparent`. On the GlowingBorder, " +
249
+ "the radius is the control's radius plus the glow width, the surface is transparent, and `--glowing-border-overflow: visible` " +
250
+ "keeps the controls' hover and focus outlines from being clipped. Tab through to check the focus rings.",
251
+ },
252
+ },
132
253
  };
133
254
 
134
- const CustomTokensTemplate: StoryFn<StoryArgs> = (args) => ({
255
+ export const StressTest: StoryFn<StoryArgs> = (args) => ({
135
256
  components: { StorybookComponent },
136
257
  setup() {
137
- return { args };
258
+ const longUnbroken = "Donaudampfschifffahrtselektrizitätenhauptbetriebswerkbauunterbeamtengesellschaft";
259
+ const longUrl = "https://example.com/" + "a-very-long-path-segment-without-spaces".repeat(4);
260
+ const longCopy =
261
+ "Unsere Behandlungen werden individuell auf Ihre Bedürfnisse abgestimmt, damit Sie sich rundum wohlfühlen und entspannt nach Hause gehen können. ".repeat(
262
+ 6
263
+ );
264
+ return { ...useStorySetup(args), longUnbroken, longUrl, longCopy };
138
265
  },
139
266
  template: `
140
- <div style="padding: 40px; background-color: var(--slate-10);">
141
- <StorybookComponent
142
- :tag="args.tag"
143
- :variant="args.variant"
144
- :style="{
145
- '--glowing-border-width': args.borderWidth,
146
- '--glowing-border-radius': args.borderRadius,
147
- '--glowing-border-animation-duration': args.animationDuration,
148
- }"
149
- >
150
- <div style="padding: 20px; color: var(--slate-02);">
151
- <p style="margin: 0; line-height: 1.6;">{{ args.content }}</p>
267
+ <div style="padding: 24px 16px; display: grid; gap: 24px;" :style="stageStyle">
268
+ <StorybookComponent :tag="args.tag" :variant="args.variant" :style="tokenStyle">
269
+ <div style="padding: 20px;">
270
+ <h3 style="margin: 0 0 12px 0;">{{ longUnbroken }}</h3>
271
+ <p style="margin: 0 0 12px 0;">{{ longUrl }}</p>
272
+ <p style="margin: 0;">{{ longCopy }}</p>
152
273
  </div>
153
274
  </StorybookComponent>
275
+ <StorybookComponent :tag="args.tag" :variant="args.variant" :style="tokenStyle"></StorybookComponent>
276
+ <StorybookComponent :tag="args.tag" :variant="args.variant" :style="tokenStyle">A</StorybookComponent>
277
+ <StorybookComponent :tag="args.tag" :variant="args.variant" :style="tokenStyle">
278
+ <p style="margin: 0; padding: 12px;">🎉 Emoji first, then &lt;b&gt;html&lt;/b&gt; as text</p>
279
+ </StorybookComponent>
280
+ <StorybookComponent :tag="args.tag" :variant="args.variant" :style="tokenStyle">
281
+ <p dir="rtl" style="margin: 0; padding: 12px;">نص عربي طويل من اليمين إلى اليسار للتحقق من الاتجاه</p>
282
+ </StorybookComponent>
283
+ <p style="margin: 0;">
284
+ Inline use:
285
+ <StorybookComponent tag="span" :variant="args.variant" :style="tokenStyle">{{ longUnbroken }}</StorybookComponent>
286
+ sits in running text.
287
+ </p>
154
288
  </div>
155
289
  `,
156
290
  });
157
-
158
- export const CustomTokens = CustomTokensTemplate.bind({});
159
- CustomTokens.args = {
160
- variant: "vivid",
161
- content: "Border width, radius, and animation speed all driven by the controls above.",
291
+ StressTest.storyName = "Stress Test (Worst-Case Data)";
292
+ StressTest.args = {
162
293
  borderWidth: "6px",
163
- borderRadius: "4px",
164
- animationDuration: "5s",
294
+ borderRadius: "6px",
295
+ };
296
+ StressTest.decorators = [
297
+ (story, context) => ({
298
+ components: { story, CanvasSwitcher },
299
+ setup() {
300
+ const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "mobileCanvas");
301
+ return { canvasName };
302
+ },
303
+ template: `
304
+ <div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
305
+ <CanvasSwitcher v-model:canvas-name="canvasName" />
306
+ </div>
307
+ <div :class="canvasName" style="margin-inline: auto; outline: 1px dashed currentColor;">
308
+ <story />
309
+ </div>
310
+ `,
311
+ }),
312
+ ];
313
+ StressTest.parameters = {
314
+ initialCanvas: "mobileCanvas",
315
+ docs: {
316
+ description: {
317
+ story:
318
+ "Oversized and odd slot content. Long unbroken words and URLs should wrap inside the border rather than being clipped " +
319
+ "(the root has overflow: hidden), long German copy should grow the box, the empty one should collapse to just its border, " +
320
+ "HTML-like text should render as text, RTL text should align right, and the inline span should wrap within running text. " +
321
+ "Check at every canvas width and with each variant.",
322
+ },
323
+ },
165
324
  };
@@ -52,19 +52,30 @@ describe("GlowingBorder", () => {
52
52
 
53
53
  // ─── Variant ──────────────────────────────────────────────────────────────
54
54
 
55
- it("applies subtle variant class by default", async () => {
55
+ it("sets data-variant='subtle' by default", async () => {
56
56
  const wrapper = await mountSuspended(GlowingBorder);
57
- expect(wrapper.classes()).toContain("subtle");
57
+ expect(wrapper.attributes("data-variant")).toBe("subtle");
58
58
  });
59
59
 
60
60
  it.each(["subtle", "vivid", "silver", "steel", "green"] as const)(
61
- "applies %s variant class when variant='%s'",
61
+ "sets data-variant='%s' when variant='%s'",
62
62
  async (variant) => {
63
63
  const wrapper = await mountSuspended(GlowingBorder, { props: { variant } });
64
- expect(wrapper.classes()).toContain(variant);
64
+ expect(wrapper.attributes("data-variant")).toBe(variant);
65
65
  }
66
66
  );
67
67
 
68
+ it("does not add the variant name as a class, so consumer classes like .green can't collide", async () => {
69
+ const wrapper = await mountSuspended(GlowingBorder, { props: { variant: "green" } });
70
+ expect(wrapper.classes()).not.toContain("green");
71
+ });
72
+
73
+ it("updates data-variant when the variant prop changes", async () => {
74
+ const wrapper = await mountSuspended(GlowingBorder, { props: { variant: "vivid" } });
75
+ await wrapper.setProps({ variant: "steel" });
76
+ expect(wrapper.attributes("data-variant")).toBe("steel");
77
+ });
78
+
68
79
  // ─── styleClassPassthrough ────────────────────────────────────────────────
69
80
 
70
81
  it("applies a single styleClassPassthrough string", async () => {
@@ -1,3 +1,3 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
- exports[`GlowingBorder > renders correct HTML structure (default) 1`] = `"<div class="glowing-border subtle">Content</div>"`;
3
+ exports[`GlowingBorder > renders correct HTML structure (default) 1`] = `"<div class="glowing-border" data-variant="subtle">Content</div>"`;
@@ -17,9 +17,9 @@ the base token restyles every variant, and a variant token still wins for that o
17
17
  | `--display-pill-background` | `var(--slate-01)` | Background (the `default` variant, and the fallback for every other variant) |
18
18
  | `--display-pill-text-colour` | `var(--slate-09)` | Text/icon colour (same fallback role) |
19
19
  | `--display-pill-primary-background` / `-text-colour` | `var(--blue-01)` / `var(--blue-09)` | `primary` variant |
20
- | `--display-pill-success-background` / `-text-colour` | `var(--green-01)` / `var(--green-10)` | `success` variant |
21
- | `--display-pill-warning-background` / `-text-colour` | `var(--orange-01)` / `var(--orange-10)` | `warning` variant |
22
- | `--display-pill-danger-background` / `-text-colour` | `var(--red-01)` / `var(--red-10)` | `danger` variant |
20
+ | `--display-pill-success-background` / `-text-colour` | `var(--status-success-surface)` / `var(--status-success-text)` | `success` variant |
21
+ | `--display-pill-warning-background` / `-text-colour` | `var(--status-warning-surface)` / `var(--status-warning-text)` | `warning` variant |
22
+ | `--display-pill-danger-background` / `-text-colour` | `var(--status-danger-surface)` / `var(--status-danger-text)` | `danger` variant |
23
23
  | `--display-pill-neutral-background` / `-text-colour` | `var(--slate-08)` / `var(--slate-03)` | `neutral` variant (dark) |
24
24
  | `--display-pill-focus-ring` | `var(--theme-border-focus)` | `:focus-visible` outline on `button`/`a` pills |
25
25
 
@@ -29,16 +29,26 @@ the base token restyles every variant, and a variant token still wins for that o
29
29
  > `--yellow-*` ramp that doesn't exist, so it always showed its hardcoded hex fallbacks; it now uses
30
30
  > the `orange` ramp the warning theme moved to.
31
31
 
32
+ `success`, `warning` and `danger` default to the global `--status-*` tokens
33
+ (`03.theming/_status.css`), so overriding those restyles pills along with every other status
34
+ indicator; same colours as before (changed 2026-10-07). `primary` (brand) and `neutral` (a dark,
35
+ inverted pill) keep their own defaults and don't read the status set. See
36
+ `.claude/skills/theming-status-tokens.md`.
37
+
32
38
  ### Border, outline and shape
33
39
 
34
40
  | Token | Default | Controls |
35
41
  |---|---|---|
36
- | `--display-pill-border-colour` | `transparent` | Border colour |
42
+ | `--display-pill-border-colour` | `transparent` | Border colour (every variant's fallback) |
43
+ | `--display-pill-<variant>-border-colour` | the base border colour | Border colour for one variant (`primary`, `success`, `warning`, `danger`, `neutral`) |
37
44
  | `--display-pill-border-width` | `0.1rem` | Border width |
38
45
  | `--display-pill-border-style` | `solid` | Border style |
39
46
  | `--display-pill-border-radius` | `100vw` | Radius (100vw is always a full pill) |
40
47
  | `--display-pill-outline` | `none` | Decorative outline (shorthand) on every pill |
41
48
  | `--display-pill-outline-offset` | `0` | Offset for that outline |
49
+ | `--display-pill-ring-width` | `0` | Width of a ring drawn just outside the border (a `box-shadow`, so it follows the radius on every browser and takes no layout space) |
50
+ | `--display-pill-<variant>-ring-colour` | the base ring colour | Ring colour for one variant (`primary`, `success`, `warning`, `danger`, `neutral`) |
51
+ | `--display-pill-ring-colour` | the pill's resolved background | Ring colour. The default follows each variant's background, so `border-colour: currentColor` plus a ring gives a two-tone border in every variant's own colours |
42
52
 
43
53
  ### Size
44
54
 
@@ -50,6 +60,16 @@ the base token restyles every variant, and a variant token still wins for that o
50
60
 
51
61
  The unsuffixed token is the `md` value; `-sm`/`-lg` apply to those sizes.
52
62
 
63
+ ### Label
64
+
65
+ | Token | Default | Controls |
66
+ |---|---|---|
67
+ | `--display-pill-label-line-clamp` | `1` | Lines of label text before an ellipsis. `1` keeps the pill on one line; `none` lets a long label wrap; any other number caps the lines |
68
+
69
+ Applies to the `label` prop and to default-slot content alike. Clamped text stays in the DOM, so
70
+ screen readers still read all of it. If a pill's text must stay fully readable (a price, say) in a
71
+ narrow space, set `none`.
72
+
53
73
  > **Changed 2026-09-27**: the icon-size tokens were declared but never used, so icons weren't
54
74
  > sized. The icon slot is now wrapped in `.display-pill-icon`, which sets its `font-size`.
55
75
 
@@ -70,20 +90,35 @@ keeps the same proportions. Tune a **ratio** to change spacing across all sizes,
70
90
  > `-lg`, etc., now removed). It's now derived from the icon size; results are within about 0.1rem
71
91
  > of the old values, except `lg` block padding (`0.6rem` → `0.48rem`), which was out of proportion.
72
92
 
73
- Private tokens (not public API): `--_background`, `--_text-colour`, `--_font-size`, `--_padding-inline`,
74
- `--_padding-block`, `--_padding-inline-icon` (derived from `--_icon-size`), `--_icon-size`, swapped by the size and variant classes.
93
+ Private tokens (not public API): `--_background`, `--_text-colour`, `--_border-colour`, `--_ring-colour`, `--_font-size`, `--_padding-inline`,
94
+ `--_padding-block`, `--_padding-inline-icon` (derived from `--_icon-size`), `--_icon-size`, swapped by the size and variant attributes.
95
+
96
+ > **Changed 2026-10-06**: a pill is capped at its container's width (`max-inline-size: 100%`), and a
97
+ > label too long for it ends in an ellipsis instead of overflowing. Short labels are unaffected.
98
+ >
99
+ > **Changed 2026-10-08**: default-slot text is now wrapped in `.display-pill-label` too, so it
100
+ > ellipsizes like a `label` prop instead of spilling out of the pill. The ellipsis is now a
101
+ > line clamp (`--display-pill-label-line-clamp`, default `1`) rather than `white-space: nowrap`,
102
+ > so a long label breaks at a space before the ellipsis rather than mid-word. A pill with no text
103
+ > and no icon (empty or whitespace-only `label`, no slots) no longer renders a blank coloured blob.
75
104
 
76
105
  ## State hooks
77
106
 
78
- - Size class: `.sm`, `.md`, `.lg`. Variant class: `.default`, `.primary`, `.success`, `.warning`,
79
- `.danger`, `.neutral`. `.is-reversed` when `reversed`.
107
+ - `data-size`: `sm`, `md`, `lg`. `data-variant`: `default`, `primary`, `success`, `warning`,
108
+ `danger`, `neutral` (an unknown value gets the default colours). `data-reversed` when `reversed`.
80
109
  - `data-icon-position`: `start` (icon before the text), `end` (`reversed`), `only` (icon with no
81
110
  label or text), or absent (no icon). It picks which side gets `--display-pill-padding-inline-icon`.
82
111
  - Classes: `.display-pill` (root), `.display-pill-icon` (icon slot wrapper, only when used),
83
- `.display-pill-label` (only with the `label` prop).
112
+ `.display-pill-label` (wraps the `label` prop or default-slot content, absent when there's no text).
84
113
  - `button`/`a` roots get a pointer cursor and a `:focus-visible` outline.
85
114
 
86
115
  > **Changed 2026-09-27**: `.pill-label` → `.display-pill-label`.
116
+ >
117
+ > **Changed 2026-10-08**: size, variant and reversed were bare classes (`.sm`, `.primary`,
118
+ > `.success`, `.is-reversed`...), which a consumer's own utility classes of the same name could
119
+ > match, since the pill renders inside the consumer's DOM. Replace `.display-pill.primary` with
120
+ > `.display-pill[data-variant="primary"]`, `.display-pill.sm` with `.display-pill[data-size="sm"]`,
121
+ > and `.display-pill.is-reversed` with `.display-pill[data-reversed]`.
87
122
 
88
123
  ## Local overrides
89
124
 
@@ -97,6 +132,19 @@ component need `:deep()`. Patterns and examples: `.claude/skills/component-local
97
132
  won't reach those pills. `ServiceSummary` and `ServiceDetail` set `--display-pill-background`,
98
133
  `-text-colour` and `-border-colour` on their pill rows; use their own `--service-*-pill-*` tokens.
99
134
 
135
+ ## Recipe: two-tone ringed pills
136
+
137
+ A 1px border in each variant's dark text colour, inside a 2px ring in its light background colour:
138
+
139
+ ```css
140
+ .tag-list {
141
+ --display-pill-border-colour: currentColor;
142
+ --display-pill-ring-width: 0.2rem;
143
+ }
144
+ ```
145
+
146
+ The `neutral` variant is inverted (light text on dark), so its border comes out light and its ring dark.
147
+
100
148
  ## Recipe: outlined pills
101
149
 
102
150
  ```css
@@ -107,6 +155,27 @@ won't reach those pills. `ServiceSummary` and `ServiceDetail` set `--display-pil
107
155
  }
108
156
  ```
109
157
 
158
+ That gives every variant the surrounding text colour. To keep each variant's own colour, leave
159
+ `--display-pill-text-colour` unset and set the per-variant text tokens instead. The default text
160
+ steps (`-09`/`-10`) are near-black in every hue, so lift them to `-07` to tell the variants apart
161
+ (the border follows, being `currentColor`):
162
+
163
+ ```css
164
+ .tag-list {
165
+ --display-pill-background: transparent;
166
+ --display-pill-border-colour: currentColor;
167
+ --display-pill-text-colour: var(--slate-07);
168
+ --display-pill-primary-text-colour: var(--blue-07);
169
+ --display-pill-success-text-colour: var(--green-07);
170
+ --display-pill-warning-text-colour: var(--orange-07);
171
+ --display-pill-danger-text-colour: var(--red-07);
172
+ --display-pill-neutral-text-colour: var(--slate-10);
173
+ }
174
+ ```
175
+
176
+ Add a ring in each variant's light `-02` step with `--display-pill-ring-width: 0.2rem` and
177
+ `--display-pill-<variant>-ring-colour` (`var(--blue-02)`, `var(--green-02)`, and so on).
178
+
110
179
  ## Class passthrough
111
180
 
112
181
  `style-class-passthrough` adds classes to the root. Reachable in normal use.