srcdev-nuxt-components 9.6.0 → 9.6.2

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 (121) hide show
  1. package/.claude/skills/component-compliance-checklist.md +22 -3
  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/container-glow.md +21 -8
  7. package/.claude/skills/components/display-avatar.md +27 -10
  8. package/.claude/skills/components/display-chip.md +38 -24
  9. package/.claude/skills/components/display-pill.md +58 -10
  10. package/.claude/skills/components/display-prompt.md +8 -5
  11. package/.claude/skills/components/display-toast.md +21 -4
  12. package/.claude/skills/components/glowing-border.md +20 -2
  13. package/.claude/skills/components/google-reviews.md +2 -2
  14. package/.claude/skills/components/profile-section.md +59 -21
  15. package/.claude/skills/components/responsive-header.md +11 -1
  16. package/.claude/skills/components/select-menu.md +54 -5
  17. package/.claude/skills/components/site-header.md +2 -2
  18. package/.claude/skills/components/social-icons-list.md +2 -0
  19. package/.claude/skills/components/tabbed-content.md +300 -0
  20. package/.claude/skills/composable-anchor-scroll.md +16 -15
  21. package/.claude/skills/index.md +8 -7
  22. package/.claude/skills/storybook-add-story.md +55 -0
  23. package/.claude/skills/theming-component-token-pattern.md +48 -0
  24. package/.claude/skills/theming-status-tokens.md +60 -0
  25. package/.vscode/srcdev-component-display-avatar.code-snippets +9 -1
  26. package/.vscode/srcdev-component-display-chip.code-snippets +3 -2
  27. package/.vscode/srcdev-component-display-pill.code-snippets +10 -0
  28. package/.vscode/srcdev-component-profile-section.code-snippets +27 -4
  29. package/.vscode/srcdev-component-responsive-header.code-snippets +6 -0
  30. package/.vscode/srcdev-component-select-menu.code-snippets +28 -0
  31. package/.vscode/{srcdev-component-tabs-core.code-snippets → srcdev-component-tabbed-content.code-snippets} +35 -16
  32. package/app/assets/styles/setup/03.theming/_status.css +24 -0
  33. package/app/assets/styles/setup/03.theming/index.css +1 -0
  34. package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +39 -35
  35. package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +25 -15
  36. package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +130 -24
  37. package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +45 -0
  38. package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +2 -2
  39. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +76 -19
  40. package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +12 -10
  41. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +201 -42
  42. package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +15 -4
  43. package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +1 -1
  44. package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +75 -9
  45. package/app/components/01.atoms/display-pill/DisplayPill.vue +57 -24
  46. package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +240 -16
  47. package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +43 -14
  48. package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +3 -3
  49. package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +201 -0
  50. package/app/components/01.atoms/tabbed-content/TabbedContent.vue +750 -0
  51. package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +279 -0
  52. package/app/components/01.atoms/tabbed-content/tests/TabbedContent.spec.ts +816 -0
  53. package/app/components/02.molecules/alert-content/AlertContent.vue +11 -2
  54. package/app/components/02.molecules/alert-content/AlertContentInner.vue +19 -0
  55. package/app/components/02.molecules/alert-content/CONSUMER-STYLING.md +17 -4
  56. package/app/components/02.molecules/alert-content/stories/AlertContent.stories.ts +200 -17
  57. package/app/components/02.molecules/alert-content/tests/AlertContent.spec.ts +33 -0
  58. package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +33 -15
  59. package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +81 -36
  60. package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +108 -6
  61. package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +104 -13
  62. package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +4 -3
  63. package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +57 -19
  64. package/app/components/02.molecules/display-chip/DisplayChip.vue +72 -54
  65. package/app/components/02.molecules/display-chip/stories/DisplayChip.stories.ts +103 -6
  66. package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +98 -29
  67. package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +8 -4
  68. package/app/components/02.molecules/profile-section/CONSUMER-STYLING.md +103 -13
  69. package/app/components/02.molecules/profile-section/ProfileSection.vue +123 -23
  70. package/app/components/02.molecules/profile-section/stories/ProfileSection.stories.ts +353 -115
  71. package/app/components/02.molecules/profile-section/tests/ProfileSection.spec.ts +144 -11
  72. package/app/components/02.molecules/profile-section/tests/__snapshots__/ProfileSection.spec.ts.snap +6 -11
  73. package/app/components/02.molecules/prompt/CONSUMER-STYLING.md +9 -1
  74. package/app/components/02.molecules/prompt/DisplayPrompt.vue +2 -2
  75. package/app/components/02.molecules/prompt/stories/DisplayPrompt.stories.ts +208 -71
  76. package/app/components/02.molecules/prompt/tests/DisplayPrompt.spec.ts +19 -6
  77. package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +66 -8
  78. package/app/components/02.molecules/select-menu/SelectMenu.vue +141 -18
  79. package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +232 -2
  80. package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +151 -0
  81. package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +6 -2
  82. package/app/components/02.molecules/social-icons-list/CONSUMER-STYLING.md +6 -2
  83. package/app/components/02.molecules/social-icons-list/SocialIconsList.vue +1 -0
  84. package/app/components/02.molecules/toast/CONSUMER-STYLING.md +91 -27
  85. package/app/components/02.molecules/toast/DisplayToast.vue +109 -50
  86. package/app/components/02.molecules/toast/DisplayToastProvider.vue +109 -45
  87. package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +23 -0
  88. package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +73 -0
  89. package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +132 -18
  90. package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +105 -39
  91. package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviewCard.spec.ts.snap +1 -1
  92. package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviews.spec.ts.snap +6 -6
  93. package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +38 -11
  94. package/app/components/03.organisms/responsive-header/NavigationItems.vue +27 -12
  95. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +35 -20
  96. package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +1 -1
  97. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +1 -1
  98. package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +1 -1
  99. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +3 -3
  100. package/app/composables/useTabs.ts +148 -206
  101. package/app/types/components/display-chip.d.ts +8 -4
  102. package/app/types/components/display-toast.d.ts +2 -0
  103. package/app/types/components/index.ts +0 -1
  104. package/app/types/components/select-menu.d.ts +2 -0
  105. package/app/utils/pausableTimeout.ts +41 -0
  106. package/nuxt.config.ts +1 -1
  107. package/package.json +1 -1
  108. package/.claude/skills/components/tab-navigation.md +0 -288
  109. package/.claude/skills/components/tabs-core.md +0 -187
  110. package/.vscode/srcdev-component-tab-navigation.code-snippets +0 -30
  111. package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +0 -120
  112. package/app/components/01.atoms/navigation/tabs/TabsCore.vue +0 -272
  113. package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +0 -199
  114. package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +0 -274
  115. package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +0 -191
  116. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +0 -550
  117. package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +0 -259
  118. package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +0 -379
  119. package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +0 -33
  120. package/app/composables/useNavCollapse.ts +0 -86
  121. package/app/types/components/nav-item.d.ts +0 -11
@@ -1,6 +1,8 @@
1
1
  import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
- import { computed } from "vue";
2
+ import { computed, ref } from "vue";
3
3
  import StorybookComponent from "../ContainerGlow.vue";
4
+ import CanvasSwitcher from "../../../canvas-switcher/CanvasSwitcher.vue";
5
+ import type { MediaCanvas } from "~/types/components";
4
6
 
5
7
  type StoryArgs = {
6
8
  tag: "div" | "li" | "article" | "section";
@@ -10,8 +12,22 @@ type StoryArgs = {
10
12
  gap: number;
11
13
  vertical: boolean;
12
14
  inactiveOpacity: number;
15
+ canvasBackground: string;
16
+ cardBackground: string;
13
17
  };
14
18
 
19
+ const darkColours = ["#000000", "#020024", "#0f0f23", "#1e293b", "#262626"];
20
+ const darkColourLabels = {
21
+ "": "Theme default (light)",
22
+ "#000000": "Black",
23
+ "#020024": "Midnight",
24
+ "#0f0f23": "Ink",
25
+ "#1e293b": "Slate",
26
+ "#262626": "Charcoal",
27
+ };
28
+ const surfaceLabels = { ...darkColourLabels, "#ffffff": "White" };
29
+ const isDark = (colour: string) => darkColours.includes(colour);
30
+
15
31
  export default {
16
32
  title: "Atoms/Effects/ContainerGlow",
17
33
  component: StorybookComponent,
@@ -51,6 +67,20 @@ export default {
51
67
  description: "Opacity of the glow when not being hovered near",
52
68
  table: { category: "config" },
53
69
  },
70
+ canvasBackground: {
71
+ control: { type: "select", labels: darkColourLabels },
72
+ options: ["", ...darkColours],
73
+ description:
74
+ "**Story control, not a prop.** Background of the demo stage behind the cards, so the glow can be checked on a dark page.",
75
+ table: { category: "Story canvas (story only)" },
76
+ },
77
+ cardBackground: {
78
+ control: { type: "select", labels: surfaceLabels },
79
+ options: ["", "#ffffff", ...darkColours],
80
+ description:
81
+ "**Story control, not a prop.** Sets `--container-glow-background` (and a light `--container-glow-text-colour` for dark values). In your CSS: `.pricing { --container-glow-background: #0f0f23; --container-glow-text-colour: #f3f4f6; }`",
82
+ table: { category: "CSS tokens (story only, set in your CSS)" },
83
+ },
54
84
  styleClassPassthrough: {
55
85
  table: { disable: true },
56
86
  },
@@ -69,35 +99,47 @@ export default {
69
99
  gap: 32,
70
100
  vertical: false,
71
101
  inactiveOpacity: 0,
102
+ canvasBackground: "",
103
+ cardBackground: "",
72
104
  },
73
105
  parameters: {
74
106
  docs: {
75
107
  description: {
76
108
  component:
77
- "An interactive glow effect wrapper that creates one card per named slot the consumer provides, each with an animated gradient border that responds to mouse proximity.",
109
+ "An interactive glow effect wrapper that creates one card per named slot the consumer provides, each with an animated gradient border that responds to mouse proximity. The canvas and card background controls are story-only, for checking the glow against dark themes.",
78
110
  },
79
111
  },
80
112
  },
81
113
  } as Meta<StoryArgs>;
82
114
 
83
- const cardStyle =
84
- "height: 100%; display: flex; flex-direction: column; justify-content: center; text-align: center; color: #374151;";
115
+ function useStorySetup(args: StoryArgs) {
116
+ const config = computed(() => ({
117
+ proximity: args.proximity,
118
+ spread: args.spread,
119
+ blur: args.blur,
120
+ gap: args.gap,
121
+ vertical: args.vertical,
122
+ inactiveOpacity: args.inactiveOpacity,
123
+ }));
124
+ const stageStyle = computed(() => ({
125
+ background: args.canvasBackground || undefined,
126
+ "--container-glow-background": args.cardBackground || undefined,
127
+ "--container-glow-text-colour":
128
+ isDark(args.cardBackground) ? "#f3f4f6" : undefined,
129
+ }));
130
+ return { args, config, stageStyle };
131
+ }
132
+
133
+ const stageLayout = "padding: 60px 40px; min-height: 80vh; display: flex; align-items: center; justify-content: center;";
134
+ const cardStyle = "height: 100%; display: flex; flex-direction: column; justify-content: center; text-align: center;";
85
135
 
86
136
  const Template: StoryFn<StoryArgs> = (args) => ({
87
137
  components: { StorybookComponent },
88
138
  setup() {
89
- const config = computed(() => ({
90
- proximity: args.proximity,
91
- spread: args.spread,
92
- blur: args.blur,
93
- gap: args.gap,
94
- vertical: args.vertical,
95
- inactiveOpacity: args.inactiveOpacity,
96
- }));
97
- return { args, config };
139
+ return useStorySetup(args);
98
140
  },
99
141
  template: `
100
- <div style="background: radial-gradient(ellipse at center, #0f0f23 0%, #020024 100%); padding: 60px 40px; min-height: 80vh; display: flex; align-items: center; justify-content: center;">
142
+ <div style="${stageLayout}" :style="stageStyle">
101
143
  <StorybookComponent :tag="args.tag" :config="config">
102
144
  <template #one>
103
145
  <div style="${cardStyle}">
@@ -127,18 +169,10 @@ export const Default = Template.bind({});
127
169
  export const SingleContainer: StoryFn<StoryArgs> = (args) => ({
128
170
  components: { StorybookComponent },
129
171
  setup() {
130
- const config = computed(() => ({
131
- proximity: args.proximity,
132
- spread: args.spread,
133
- blur: args.blur,
134
- gap: args.gap,
135
- vertical: args.vertical,
136
- inactiveOpacity: args.inactiveOpacity,
137
- }));
138
- return { args, config };
172
+ return useStorySetup(args);
139
173
  },
140
174
  template: `
141
- <div style="background: radial-gradient(ellipse at center, #0f0f23 0%, #020024 100%); padding: 60px 40px; min-height: 80vh; display: flex; align-items: center; justify-content: center;">
175
+ <div style="${stageLayout}" :style="stageStyle">
142
176
  <StorybookComponent :tag="args.tag" :config="config">
143
177
  <template #cta>
144
178
  <div style="${cardStyle}">
@@ -173,3 +207,75 @@ SubtleGlow.args = {
173
207
  gap: 20,
174
208
  inactiveOpacity: 0,
175
209
  };
210
+
211
+ export const StressTest: StoryFn<StoryArgs> = (args) => ({
212
+ components: { StorybookComponent },
213
+ setup() {
214
+ const longUnbroken = "Donaudampfschifffahrtselektrizitätenhauptbetriebswerkbauunterbeamtengesellschaft";
215
+ const longUrl = "https://example.com/" + "a-very-long-path-segment-without-spaces".repeat(4);
216
+ const longCopy =
217
+ "Unsere Behandlungen werden individuell auf Ihre Bedürfnisse abgestimmt, damit Sie sich rundum wohlfühlen und entspannt nach Hause gehen können. ".repeat(
218
+ 3
219
+ );
220
+ return { ...useStorySetup(args), longUnbroken, longUrl, longCopy };
221
+ },
222
+ template: `
223
+ <div style="padding: 40px 16px; min-height: 80vh;" :style="stageStyle">
224
+ <StorybookComponent :tag="args.tag" :config="config">
225
+ <template #long-unbroken>
226
+ <h3 style="margin: 0 0 8px 0;">{{ longUnbroken }}</h3>
227
+ <p style="margin: 0;">{{ longUrl }}</p>
228
+ </template>
229
+ <template #long-copy>
230
+ <h3 style="margin: 0 0 8px 0;">Sehr lange übersetzte Überschrift für eine Karte</h3>
231
+ <p style="margin: 0; font-size: 1.3rem;">{{ longCopy }}</p>
232
+ </template>
233
+ <template #empty></template>
234
+ <template #single-char><p style="margin: 0;">A</p></template>
235
+ <template #emoji><p style="margin: 0;">🎉 Emoji first, then &lt;script&gt;alert("html")&lt;/script&gt; as text</p></template>
236
+ <template #rtl><p dir="rtl" style="margin: 0;">نص عربي طويل من اليمين إلى اليسار للتحقق من الاتجاه</p></template>
237
+ <template #broken-image>
238
+ <img src="/does-not-exist.jpg" alt="Broken image" width="200" height="120" style="max-width: 100%; height: auto;" />
239
+ </template>
240
+ <template #eight><p style="margin: 0;">Card 8</p></template>
241
+ <template #nine><p style="margin: 0;">Card 9</p></template>
242
+ <template #ten><p style="margin: 0;">Card 10</p></template>
243
+ </StorybookComponent>
244
+ </div>
245
+ `,
246
+ });
247
+ StressTest.storyName = "Stress Test (Worst-Case Data)";
248
+ StressTest.args = {
249
+ tag: "li",
250
+ gap: 16,
251
+ };
252
+ StressTest.decorators = [
253
+ (story, context) => ({
254
+ components: { story, CanvasSwitcher },
255
+ setup() {
256
+ const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "mobileCanvas");
257
+ return { canvasName };
258
+ },
259
+ template: `
260
+ <div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
261
+ <CanvasSwitcher v-model:canvas-name="canvasName" />
262
+ </div>
263
+ <div :class="canvasName" style="margin-inline: auto; outline: 1px dashed currentColor;">
264
+ <story />
265
+ </div>
266
+ `,
267
+ }),
268
+ ];
269
+ StressTest.parameters = {
270
+ initialCanvas: "mobileCanvas",
271
+ docs: {
272
+ description: {
273
+ story:
274
+ "Hostile content to find breakage. Ten cards should wrap onto new rows instead of overflowing sideways, " +
275
+ "long unbroken words and URLs should wrap inside the 280px card, the long German copy should grow the card past its aspect ratio " +
276
+ "rather than spilling out, the empty card should keep its size, HTML-like text should render as text, RTL text should align right, " +
277
+ "and the broken image should not stretch the card. With tag 'li' the wrapper renders as a ul. " +
278
+ "Negative, fractional and non-finite config values are clamped (covered by unit tests), so the layout should never collapse. Check at every canvas width.",
279
+ },
280
+ },
281
+ };
@@ -64,6 +64,29 @@ describe("ContainerGlow", () => {
64
64
  expect(wrapper.find(".container-glow").element.tagName).toBe(tag.toUpperCase());
65
65
  });
66
66
 
67
+ it("renders the wrapper as <ul> when tag='li' so the items form a valid list", async () => {
68
+ const wrapper = await mountSuspended(ContainerGlow, {
69
+ props: { tag: "li" },
70
+ slots: { one: "<p>One</p>" },
71
+ });
72
+ expect(wrapper.element.tagName).toBe("UL");
73
+ });
74
+
75
+ it.each(["div", "article", "section"] as const)("renders the wrapper as <div> when tag='%s'", async (tag) => {
76
+ const wrapper = await mountSuspended(ContainerGlow, {
77
+ props: { tag },
78
+ slots: { one: "<p>One</p>" },
79
+ });
80
+ expect(wrapper.element.tagName).toBe("DIV");
81
+ });
82
+
83
+ it("hides the decorative glow layer from assistive technology", async () => {
84
+ const wrapper = await mountSuspended(ContainerGlow, {
85
+ slots: { one: "<p>One</p>" },
86
+ });
87
+ expect(wrapper.find(".container-glow-glows").attributes("aria-hidden")).toBe("true");
88
+ });
89
+
67
90
  // ─── Config ─────────────────────────────────────────────────────────────
68
91
 
69
92
  it("applies default config values to the wrapper as CSS custom properties", async () => {
@@ -99,6 +122,28 @@ describe("ContainerGlow", () => {
99
122
  expect(style.getPropertyValue("--_gap")).toBe("32");
100
123
  });
101
124
 
125
+ it("clamps negative and out-of-range config values", async () => {
126
+ const wrapper = await mountSuspended(ContainerGlow, {
127
+ props: { config: { gap: -10, blur: -5, spread: 999 } },
128
+ slots: { one: "<p>One</p>" },
129
+ });
130
+ const style = wrapper.element.style;
131
+ expect(style.getPropertyValue("--_gap")).toBe("0");
132
+ expect(style.getPropertyValue("--_blur")).toBe("0");
133
+ expect(style.getPropertyValue("--_spread")).toBe("360");
134
+ });
135
+
136
+ it("falls back to defaults for non-finite config values", async () => {
137
+ const wrapper = await mountSuspended(ContainerGlow, {
138
+ props: { config: { gap: Number.NaN, blur: Number.POSITIVE_INFINITY, spread: Number.NaN } },
139
+ slots: { one: "<p>One</p>" },
140
+ });
141
+ const style = wrapper.element.style;
142
+ expect(style.getPropertyValue("--_gap")).toBe("32");
143
+ expect(style.getPropertyValue("--_blur")).toBe("20");
144
+ expect(style.getPropertyValue("--_spread")).toBe("80");
145
+ });
146
+
102
147
  // ─── styleClassPassthrough ────────────────────────────────────────────────
103
148
 
104
149
  it("applies a single styleClassPassthrough string", async () => {
@@ -3,11 +3,11 @@
3
3
  exports[`ContainerGlow > renders correct HTML structure (default) 1`] = `
4
4
  "<div class="container-glow-wrapper" style="--_gap: 32; --_blur: 20; --_spread: 80; --_direction: row;">
5
5
  <div class="container-glow">
6
- <div class="glows"></div>
6
+ <div class="container-glow-glows" aria-hidden="true"></div>
7
7
  <p>One</p>
8
8
  </div>
9
9
  <div class="container-glow">
10
- <div class="glows"></div>
10
+ <div class="container-glow-glows" aria-hidden="true"></div>
11
11
  <p>Two</p>
12
12
  </div>
13
13
  </div>"
@@ -1,13 +1,22 @@
1
1
  # GlowingBorder — Consumer Styling Guide
2
2
 
3
- ## Public CSS custom properties
3
+ ## Public token API
4
+
5
+ ### Box
4
6
 
5
7
  | Token | Default | Description |
6
8
  |---|---|---|
7
9
  | `--glowing-border-width` | `3px` | Border thickness. |
8
10
  | `--glowing-border-radius` | `30px` | Corner radius. |
9
- | `--glowing-border-surface` | `canvas` | Fill colour behind the glow (the padding-box layer). |
11
+ | `--glowing-border-surface` | `var(--theme-surface-subtle)` | Fill colour behind the glow (the padding-box layer). |
12
+ | `--glowing-border-text-colour` | `var(--theme-text)` | Text colour inside the box. |
10
13
  | `--glowing-border-animation-duration` | `10s` | Time for the glow to complete one full rotation. |
14
+ | `--glowing-border-overflow` | `hidden` | Clips content to the rounded corners. Set `visible` when wrapping a control whose hover/focus outline must show (see the recipe below). |
15
+
16
+ > Changed 2026-10-08: `--glowing-border-surface` defaulted to the `canvas` system colour and no text
17
+ > colour was set. It now defaults to `var(--theme-surface-subtle)`, with the new
18
+ > `--glowing-border-text-colour` defaulting to `var(--theme-text)`. Set
19
+ > `--glowing-border-surface: canvas` to keep the old fill.
11
20
 
12
21
  ```vue
13
22
  <GlowingBorder
@@ -17,9 +26,7 @@
17
26
  </GlowingBorder>
18
27
  ```
19
28
 
20
- ---
21
-
22
- ## Variant colour stops
29
+ ### Variant colour stops
23
30
 
24
31
  Each `variant` drives a 5-stop conic gradient. Every stop is an individually overridable token,
25
32
  named `--glowing-border-{variant}-color-{1-5}`:
@@ -41,22 +48,26 @@ named `--glowing-border-{variant}-color-{1-5}`:
41
48
  </GlowingBorder>
42
49
  ```
43
50
 
44
- ---
51
+ Private tokens, not public API: `--_glow-deg`, `--_clr-1` through `--_clr-5`, and
52
+ `--_gradient-glow` (the resolved per-variant colour stops and the animated rotation angle). Use the
53
+ public tokens above instead.
45
54
 
46
- ## Motion
47
-
48
- The glow rotates continuously via CSS animation and stops automatically when the visitor has
49
- `prefers-reduced-motion: reduce` set.
55
+ ## State hooks
50
56
 
51
- ---
57
+ | Selector | Meaning |
58
+ |---|---|
59
+ | `.glowing-border` | Root element (rendered as the `tag` element). |
60
+ | `.glowing-border[data-variant="subtle\|vivid\|silver\|steel\|green"]` | The active colour variant. |
52
61
 
53
- ## Internal (non-overridable) custom properties
62
+ > Changed 2026-10-08: the variant used to be added as a bare class (`.glowing-border.green`), which
63
+ > collided with any consumer class of the same name (`.green`, `.silver` are common). It's now a
64
+ > `data-variant` attribute; update any selector that targeted the old class. The animation's
65
+ > keyframes were renamed from the generic `glow` to `glowing-border-rotate` for the same reason.
54
66
 
55
- `--_glow-deg`, `--_clr-1` through `--_clr-5`, and `--_gradient-glow` are internal composition
56
- plumbing (the resolved per-variant colour stops and the animated rotation angle) — use the public
57
- tokens above instead.
67
+ ## Motion
58
68
 
59
- ---
69
+ The glow rotates continuously via CSS animation and stops automatically when the visitor has
70
+ `prefers-reduced-motion: reduce` set.
60
71
 
61
72
  ## Local overrides
62
73
 
@@ -66,15 +77,61 @@ Set the tokens above on an element you own (a page or section class, or a class
66
77
  `<style scoped>`, tokens set on your element still work, but selectors that reach inside the component need
67
78
  `:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
68
79
 
69
- ---
80
+ **Caveat:** the active variant re-declares its `--_clr-*` stops on the root from the
81
+ `--glowing-border-{variant}-color-*` tokens, so set those public tokens, never `--_clr-*`.
82
+
83
+ ## Recipe: glow as a control's border
84
+
85
+ For a single "special" button or input, wrap just the control so the glow becomes its border.
86
+ No changes to the control are needed:
87
+
88
+ - `InputButton`'s border already defaults to its own surface colour, so it shows no edge of its own.
89
+ - `InputTextCore` needs `--input-text-border: transparent`.
90
+ - On the `GlowingBorder`: radius is the control's radius plus the glow width, surface
91
+ `transparent`, overflow `visible` (so the control's hover and focus outlines aren't clipped),
92
+ and an inline or shrink-to-fit display so it hugs the control.
93
+
94
+ ```vue
95
+ <GlowingBorder tag="span" variant="vivid" style-class-passthrough="special-cta">
96
+ <InputButton variant="primary" button-text="Book a free consultation"></InputButton>
97
+ </GlowingBorder>
98
+
99
+ <GlowingBorder variant="vivid" style-class-passthrough="special-input">
100
+ <InputTextCore id="email" v-model="email" type="email" name="email"></InputTextCore>
101
+ </GlowingBorder>
102
+ ```
103
+
104
+ ```css
105
+ .special-cta,
106
+ .special-input {
107
+ --glowing-border-width: 3px;
108
+ --glowing-border-surface: transparent;
109
+ --glowing-border-overflow: visible;
110
+ }
111
+
112
+ .special-cta {
113
+ --glowing-border-radius: calc(var(--button-border-radius) + 3px);
114
+ display: inline-block;
115
+ }
116
+
117
+ .special-input {
118
+ --glowing-border-radius: calc(var(--form-input-border-radius) + 3px);
119
+ --input-text-border: transparent;
120
+ }
121
+ ```
122
+
123
+ The `HighlightedControls` story shows both.
70
124
 
71
125
  ## Class passthrough
72
126
 
73
127
  Use `styleClassPassthrough` to add classes to the root element for layout purposes (positioning,
74
- sizing) — it doesn't affect the glow's colours or geometry, which stay governed by the tokens
75
- above.
128
+ sizing). It doesn't affect the glow's colours or geometry, which stay governed by the tokens above.
76
129
 
77
130
  ```vue
78
131
  <GlowingBorder style-class-passthrough="my-glowing-border">...</GlowingBorder>
79
132
  ```
80
133
 
134
+ ## Notes
135
+
136
+ The root has `overflow: hidden` by default (`--glowing-border-overflow`, to clip content to the
137
+ rounded corners) and `overflow-wrap: anywhere`, so long unbroken words and URLs wrap instead of being clipped.
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <component :is="tag" class="glowing-border" :class="[variant, elementClasses]">
2
+ <component :is="tag" class="glowing-border" :class="elementClasses" :data-variant="variant">
3
3
  <slot></slot>
4
4
  </component>
5
5
  </template>
@@ -66,7 +66,7 @@ watch(
66
66
  }
67
67
 
68
68
  .glowing-border {
69
- &.vivid {
69
+ &[data-variant="vivid"] {
70
70
  --_clr-1: var(--glowing-border-vivid-color-1, #ff0000);
71
71
  --_clr-2: var(--glowing-border-vivid-color-2, #ffa500);
72
72
  --_clr-3: var(--glowing-border-vivid-color-3, #ffff00);
@@ -74,7 +74,7 @@ watch(
74
74
  --_clr-5: var(--glowing-border-vivid-color-5, #0000ff);
75
75
  }
76
76
 
77
- &.subtle {
77
+ &[data-variant="subtle"] {
78
78
  --_clr-1: var(--glowing-border-subtle-color-1, #ff9a9e);
79
79
  --_clr-2: var(--glowing-border-subtle-color-2, #fad0c4);
80
80
  --_clr-3: var(--glowing-border-subtle-color-3, #fad0c4);
@@ -82,7 +82,7 @@ watch(
82
82
  --_clr-5: var(--glowing-border-subtle-color-5, #a18cd1);
83
83
  }
84
84
 
85
- &.silver {
85
+ &[data-variant="silver"] {
86
86
  --_clr-1: var(--glowing-border-silver-color-1, #d4d4d4);
87
87
  --_clr-2: var(--glowing-border-silver-color-2, #e4e4e4);
88
88
  --_clr-3: var(--glowing-border-silver-color-3, #f5f5f5);
@@ -90,7 +90,7 @@ watch(
90
90
  --_clr-5: var(--glowing-border-silver-color-5, #d4d4d4);
91
91
  }
92
92
 
93
- &.steel {
93
+ &[data-variant="steel"] {
94
94
  --_clr-1: var(--glowing-border-steel-color-1, #434343);
95
95
  --_clr-2: var(--glowing-border-steel-color-2, #5a5a5a);
96
96
  --_clr-3: var(--glowing-border-steel-color-3, #6e6e6e);
@@ -98,7 +98,7 @@ watch(
98
98
  --_clr-5: var(--glowing-border-steel-color-5, #434343);
99
99
  }
100
100
 
101
- &.green {
101
+ &[data-variant="green"] {
102
102
  --_clr-1: var(--glowing-border-green-color-1, #00ff87);
103
103
  --_clr-2: var(--glowing-border-green-color-2, #39ff14);
104
104
  --_clr-3: var(--glowing-border-green-color-3, #00c853);
@@ -110,20 +110,22 @@ watch(
110
110
 
111
111
  border: var(--glowing-border-width, 3px) solid transparent;
112
112
  border-radius: var(--glowing-border-radius, 30px);
113
+ color: var(--glowing-border-text-colour, var(--theme-text));
114
+ overflow-wrap: anywhere;
113
115
  background:
114
- linear-gradient(var(--glowing-border-surface, canvas) 0 0) padding-box,
116
+ linear-gradient(var(--glowing-border-surface, var(--theme-surface-subtle)) 0 0) padding-box,
115
117
  conic-gradient(from var(--_glow-deg), var(--_gradient-glow)) border-box;
116
118
 
117
- animation: glow var(--glowing-border-animation-duration, 10s) infinite linear;
119
+ animation: glowing-border-rotate var(--glowing-border-animation-duration, 10s) infinite linear;
118
120
 
119
- overflow: hidden;
121
+ overflow: var(--glowing-border-overflow, hidden);
120
122
 
121
123
  @media (prefers-reduced-motion: reduce) {
122
124
  animation: none;
123
125
  }
124
126
  }
125
127
 
126
- @keyframes glow {
128
+ @keyframes glowing-border-rotate {
127
129
  100% {
128
130
  --_glow-deg: 270deg;
129
131
  }