srcdev-nuxt-components 9.6.0 → 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 (107) 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/container-glow.md +21 -8
  7. package/.claude/skills/components/display-avatar.md +13 -7
  8. package/.claude/skills/components/display-chip.md +37 -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/responsive-header.md +11 -1
  14. package/.claude/skills/components/select-menu.md +54 -5
  15. package/.claude/skills/components/site-header.md +2 -2
  16. package/.claude/skills/components/tabbed-content.md +300 -0
  17. package/.claude/skills/composable-anchor-scroll.md +16 -15
  18. package/.claude/skills/index.md +4 -4
  19. package/.claude/skills/storybook-add-story.md +53 -0
  20. package/.claude/skills/theming-status-tokens.md +60 -0
  21. package/.vscode/srcdev-component-display-avatar.code-snippets +2 -1
  22. package/.vscode/srcdev-component-display-chip.code-snippets +3 -2
  23. package/.vscode/srcdev-component-display-pill.code-snippets +10 -0
  24. package/.vscode/srcdev-component-responsive-header.code-snippets +6 -0
  25. package/.vscode/srcdev-component-select-menu.code-snippets +28 -0
  26. package/.vscode/{srcdev-component-tabs-core.code-snippets → srcdev-component-tabbed-content.code-snippets} +35 -16
  27. package/app/assets/styles/setup/03.theming/_status.css +24 -0
  28. package/app/assets/styles/setup/03.theming/index.css +1 -0
  29. package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +39 -35
  30. package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +25 -15
  31. package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +130 -24
  32. package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +45 -0
  33. package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +2 -2
  34. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +76 -19
  35. package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +12 -10
  36. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +201 -42
  37. package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +15 -4
  38. package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +1 -1
  39. package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +75 -9
  40. package/app/components/01.atoms/display-pill/DisplayPill.vue +57 -24
  41. package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +240 -16
  42. package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +43 -14
  43. package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +3 -3
  44. package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +201 -0
  45. package/app/components/01.atoms/tabbed-content/TabbedContent.vue +750 -0
  46. package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +279 -0
  47. package/app/components/01.atoms/tabbed-content/tests/TabbedContent.spec.ts +816 -0
  48. package/app/components/02.molecules/alert-content/AlertContent.vue +11 -2
  49. package/app/components/02.molecules/alert-content/AlertContentInner.vue +19 -0
  50. package/app/components/02.molecules/alert-content/CONSUMER-STYLING.md +17 -4
  51. package/app/components/02.molecules/alert-content/stories/AlertContent.stories.ts +200 -17
  52. package/app/components/02.molecules/alert-content/tests/AlertContent.spec.ts +33 -0
  53. package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +7 -5
  54. package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +6 -2
  55. package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +24 -3
  56. package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +19 -0
  57. package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +2 -1
  58. package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +52 -19
  59. package/app/components/02.molecules/display-chip/DisplayChip.vue +72 -54
  60. package/app/components/02.molecules/display-chip/stories/DisplayChip.stories.ts +103 -6
  61. package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +98 -29
  62. package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +8 -4
  63. package/app/components/02.molecules/prompt/CONSUMER-STYLING.md +9 -1
  64. package/app/components/02.molecules/prompt/DisplayPrompt.vue +2 -2
  65. package/app/components/02.molecules/prompt/stories/DisplayPrompt.stories.ts +208 -71
  66. package/app/components/02.molecules/prompt/tests/DisplayPrompt.spec.ts +19 -6
  67. package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +66 -8
  68. package/app/components/02.molecules/select-menu/SelectMenu.vue +141 -18
  69. package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +232 -2
  70. package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +151 -0
  71. package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +6 -2
  72. package/app/components/02.molecules/toast/CONSUMER-STYLING.md +91 -27
  73. package/app/components/02.molecules/toast/DisplayToast.vue +109 -50
  74. package/app/components/02.molecules/toast/DisplayToastProvider.vue +109 -45
  75. package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +23 -0
  76. package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +73 -0
  77. package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +132 -18
  78. package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +105 -39
  79. package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +38 -11
  80. package/app/components/03.organisms/responsive-header/NavigationItems.vue +27 -12
  81. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +35 -20
  82. package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +1 -1
  83. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +1 -1
  84. package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +1 -1
  85. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +3 -3
  86. package/app/composables/useTabs.ts +148 -206
  87. package/app/types/components/display-chip.d.ts +8 -4
  88. package/app/types/components/display-toast.d.ts +2 -0
  89. package/app/types/components/index.ts +0 -1
  90. package/app/types/components/select-menu.d.ts +2 -0
  91. package/app/utils/pausableTimeout.ts +41 -0
  92. package/nuxt.config.ts +1 -1
  93. package/package.json +1 -1
  94. package/.claude/skills/components/tab-navigation.md +0 -288
  95. package/.claude/skills/components/tabs-core.md +0 -187
  96. package/.vscode/srcdev-component-tab-navigation.code-snippets +0 -30
  97. package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +0 -120
  98. package/app/components/01.atoms/navigation/tabs/TabsCore.vue +0 -272
  99. package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +0 -199
  100. package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +0 -274
  101. package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +0 -191
  102. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +0 -550
  103. package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +0 -259
  104. package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +0 -379
  105. package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +0 -33
  106. package/app/composables/useNavCollapse.ts +0 -86
  107. package/app/types/components/nav-item.d.ts +0 -11
@@ -285,6 +285,59 @@ prop and try to pass it. Make it unmistakable:
285
285
 
286
286
  `PriceList.stories.ts` and `GoogleReviews.stories.ts` (line-clamp controls) follow this.
287
287
 
288
+ ## Link to the stories of components that use this one
289
+
290
+ When the component you're working on is a building block of other components (it's rendered
291
+ inside them, or they're a specialised version of it), its stories only show it in isolation. Add a
292
+ short note to each story that names the components using it, says what they add, and links to their
293
+ stories, so a reader can see it in real use. Examples: `AlertContent` is the panel inside
294
+ `DisplayToast`, `DisplayPrompt` and `CookieConsentBanner`; `AlertContentInner` is shared by
295
+ `AlertContent` and `AlertMaskedContent`.
296
+
297
+ - **Say how they're related, not just "see also".** Name the parent and what it adds, e.g. "The
298
+ #actions slot wired to real accept and reject handlers: see the CookieConsentBanner story."
299
+ - **Link to the story that shows the relevant feature**, not just any story. A story about a slot
300
+ or behaviour links to the parent story that exercises it. Other stories can share one general
301
+ note.
302
+ - **Check the parent story actually shows what the note says.** Read its component before writing
303
+ the note. `DisplayToast` doesn't fill `AlertContent`'s `#actions` slot, so a toast note that
304
+ promised action buttons would send people to the wrong place.
305
+ - **Put the note in the canvas**, as a `<p>` above the component, so it's visible on the Canvas tab
306
+ as well as Docs. If the story has a `parameters.docs.description`, add the link there too.
307
+ - Find the parent's story id from its `title` and export name: `"Molecules/CookieConsentBanner"` +
308
+ `Default` gives `molecules-cookieconsentbanner--default` (lowercase, `/` and spaces become `-`,
309
+ then `--` and the kebab-cased export name).
310
+
311
+ ```ts
312
+ // Links in the canvas open in the Storybook manager (target="_top"), not inside the preview iframe.
313
+ const storyNoteStyle = "margin: 0 0 1.6rem; font-size: 1.4rem;";
314
+ const cookieNote = `
315
+ <p style="${storyNoteStyle}">
316
+ For the #actions slot wired up to real accept and reject handlers, open the
317
+ <a href="/?path=/story/molecules-cookieconsentbanner--default" target="_top">CookieConsentBanner</a> story.
318
+ </p>
319
+ `;
320
+
321
+ // In the story template:
322
+ template: `
323
+ <div style="max-width: 600px; padding: 2rem;">
324
+ ${cookieNote}
325
+ <AlertContent v-bind="componentArgs">...</AlertContent>
326
+ </div>
327
+ `,
328
+
329
+ // In parameters.docs.description (markdown): use "?path=..." with no leading slash.
330
+ story: "See the [CookieConsentBanner](?path=/story/molecules-cookieconsentbanner--default) story. ...",
331
+ ```
332
+
333
+ - Canvas links need `target="_top"` and a leading `/` (`/?path=...`). Without `target="_top"` the
334
+ whole Storybook UI loads inside the preview frame. The leading `/` assumes Storybook is served
335
+ from the root of its domain, which holds locally and on the deployed Storybook.
336
+ - Define each note once as a constant at the top of the file and interpolate it, rather than
337
+ repeating the markup in every story.
338
+
339
+ `AlertContent.stories.ts` follows this.
340
+
288
341
  ## Notes
289
342
 
290
343
  - **Colour scheme:** `.storybook/preview.ts` pins every story to the light scheme
@@ -0,0 +1,60 @@
1
+ # Status tokens
2
+
3
+ Global status colours, declared on `:root` in `app/assets/styles/setup/03.theming/_status.css`.
4
+ Components that show a status (a dot, a badge, a pill) fall back to these, so a consumer restyles
5
+ every status indicator in the library from one place.
6
+
7
+ ## The set
8
+
9
+ Each status has three roles:
10
+
11
+ | Role | Token | Use |
12
+ |---|---|---|
13
+ | Solid | `--status-<name>` | Dots, markers, small fills (mid-ramp step, visible on light and dark) |
14
+ | Surface | `--status-<name>-surface` | Tinted background of a badge or pill |
15
+ | Text | `--status-<name>-text` | Text/icon on that surface |
16
+
17
+ | Status | Solid | Surface / text | Meaning |
18
+ |---|---|---|---|
19
+ | `success` | `--green-06` | `--green-01` / `--green-10` | Good, active, online, complete |
20
+ | `warning` | `--orange-05` | `--orange-01` / `--orange-10` | Needs attention, pending, idle |
21
+ | `danger` | `--red-06` | `--red-01` / `--red-10` | Error, blocked, do not disturb |
22
+ | `info` | `--blue-06` | `--blue-01` / `--blue-09` | Informational, in progress, new |
23
+ | `neutral` | `--slate-05` | `--slate-01` / `--slate-09` | Inactive, offline, archived, draft |
24
+
25
+ Keep the set to these five. A domain-specific state (`pending`, `archived`, `in-progress`) maps
26
+ onto one of them rather than getting its own token; if a consumer genuinely needs a sixth colour,
27
+ they define it in their app and pass it wherever the component takes a colour (e.g. `dotColor`).
28
+
29
+ ## Who reads them
30
+
31
+ The component's own token always wins, then the global status token:
32
+
33
+ | Component | Mapping |
34
+ |---|---|
35
+ | `DisplayChip` | `online` → success, `idle` → warning, `dnd` → danger, no class → neutral (solid) |
36
+ | `DisplayPill` | `success`/`warning`/`danger` variants → surface + text. `primary` (brand) and `neutral` (dark, inverted) do **not** read the set |
37
+ | `SelectMenu` | Nothing automatic; options pass `dotColor: "var(--status-success)"` etc. |
38
+
39
+ When adding a component that shows a status, default its colour tokens to these
40
+ (`var(--component-colour-online, var(--status-success))`), never to a palette step or hex value
41
+ directly.
42
+
43
+ ## Overriding
44
+
45
+ ```css
46
+ /* consuming app, unlayered */
47
+ :root {
48
+ --status-success: #1f9d55;
49
+ --status-success-surface: #e6f6ec;
50
+ --status-success-text: #0f5132;
51
+ }
52
+ ```
53
+
54
+ One component only: set its own token instead (`--display-chip-colour-online: hotpink`).
55
+
56
+ ## Dark mode
57
+
58
+ Light values only, like every other layer token (no `light-dark()`). Storybook's dark values live
59
+ in `.storybook/color-scheme.css`: surfaces move to step `09`, text to step `02`; solids stay put.
60
+ A consumer app with a dark scheme copies those overrides (see `theming-dark-mode.md`).
@@ -21,7 +21,8 @@
21
21
  " src=\"${1:/images/profile.jpg}\"",
22
22
  " alt=\"${2:Jane Smith}\"",
23
23
  " :chip=\"{ size: '${3:12px}', maskWidth: '${4:4px}', offset: '${5:0px}', angle: '${6:45deg}' }\"",
24
- " :style-class-passthrough=\"['${7|online,offline|}']\"",
24
+ " status=\"${7|online,idle,dnd,offline|}\"",
25
+ " status-label=\"${8:Online}\"",
25
26
  "/>"
26
27
  ]
27
28
  }
@@ -6,9 +6,10 @@
6
6
  "<DisplayChip",
7
7
  " shape=\"${1|circle,square|}\"",
8
8
  " :config=\"{ size: '${2:12px}', maskWidth: '${3:4px}', offset: '${4:0px}', angle: '${5:135deg}' }\"",
9
- " :style-class-passthrough=\"['${6|online,idle,dnd|}']\"",
9
+ " status=\"${6|online,idle,dnd,offline|}\"",
10
+ " status-label=\"${7:Online}\"",
10
11
  ">",
11
- " $7",
12
+ " $8",
12
13
  "</DisplayChip>"
13
14
  ]
14
15
  },
@@ -34,5 +34,15 @@
34
34
  " --display-pill-border-colour: ${4:currentColor};",
35
35
  "}"
36
36
  ]
37
+ },
38
+ "SRCDEV DisplayPill Two-Tone Ring": {
39
+ "description": "DisplayPill dark border plus light ring in each variant's own colours",
40
+ "scope": "css",
41
+ "body": [
42
+ ".${1:tag-list} {",
43
+ " --display-pill-border-colour: currentColor;",
44
+ " --display-pill-ring-width: ${2:0.2rem};",
45
+ "}"
46
+ ]
37
47
  }
38
48
  }
@@ -51,6 +51,12 @@
51
51
  "body": [
52
52
  ".$1site-header-nav {",
53
53
  " --responsive-header-link-color: $2inherit;",
54
+ " --responsive-header-link-color-hover: ;",
55
+ " --responsive-header-link-color-active: ;",
56
+ " --responsive-nav-decorator-indicator-color: ;",
57
+ " --overflow-nav-link-color: ;",
58
+ " --overflow-nav-link-color-hover: ;",
59
+ " --overflow-nav-link-color-active: ;",
54
60
  " --responsive-header-bg: $3transparent;",
55
61
  " /* Fixed, NOT var(--step-*) — see responsive-header.md's measurement-pipeline note. */",
56
62
  " --responsive-header-link-font-size: ${4:1.4rem};",
@@ -61,6 +61,34 @@
61
61
  "/>"
62
62
  ]
63
63
  },
64
+ "SRCDEV SelectMenu Filter Count": {
65
+ "description": "SelectMenu — multi-select filter with a category icon and a selected-count badge",
66
+ "scope": "vue,html",
67
+ "body": [
68
+ "<SelectMenu",
69
+ " v-model=\"${1:selectedValues}\"",
70
+ " :options=\"${2:options}\"",
71
+ " label=\"${3:Category}\"",
72
+ " multiple",
73
+ " trigger-icon=\"${4:lucide:list}\"",
74
+ " indicator=\"count\"",
75
+ "/>"
76
+ ]
77
+ },
78
+ "SRCDEV SelectMenu Filter Status Dot": {
79
+ "description": "SelectMenu — single-select filter with a category icon and an active-filter status dot, fixed width across selections",
80
+ "scope": "vue,html",
81
+ "body": [
82
+ "<SelectMenu",
83
+ " v-model=\"${1:status}\"",
84
+ " :options=\"${2:statusOptions}\"",
85
+ " label=\"${3:Status}\"",
86
+ " trigger-icon=\"${4:lucide:circle-dot}\"",
87
+ " indicator=\"dot\"",
88
+ " reserve-label-width",
89
+ "/>"
90
+ ]
91
+ },
64
92
  "SRCDEV SelectMenu Multi-Select Showing Selection": {
65
93
  "description": "SelectMenu — multi-select where the trigger updates to list checked options",
66
94
  "scope": "vue,html",
@@ -1,9 +1,9 @@
1
1
  {
2
- "SRCDEV TabsCore Basic": {
3
- "description": "TabsCore — horizontal tablist with indexed trigger/content slots",
2
+ "SRCDEV TabbedContent Basic": {
3
+ "description": "TabbedContent — horizontal tablist with indexed trigger/content slots",
4
4
  "scope": "vue,html",
5
5
  "body": [
6
- "<TabsCore :item-count=\"${1:3}\">",
6
+ "<TabbedContent :item-count=\"${1:3}\" aria-label=\"${8:Product details}\">",
7
7
  " <template #tab-0-trigger>${2:Overview}</template>",
8
8
  " <template #tab-0-content>",
9
9
  " $3",
@@ -16,39 +16,39 @@
16
16
  " <template #tab-2-content>",
17
17
  " $7",
18
18
  " </template>",
19
- "</TabsCore>"
19
+ "</TabbedContent>"
20
20
  ]
21
21
  },
22
- "SRCDEV TabsCore Vertical": {
23
- "description": "TabsCore with axis=\"y\" for a vertical tab column",
22
+ "SRCDEV TabbedContent Vertical": {
23
+ "description": "TabbedContent with axis=\"y\" for a vertical tab column",
24
24
  "scope": "vue,html",
25
25
  "body": [
26
- "<TabsCore axis=\"y\" :item-count=\"${1:3}\" aria-label=\"${2:Settings}\">",
26
+ "<TabbedContent axis=\"y\" :item-count=\"${1:3}\" aria-label=\"${2:Settings}\">",
27
27
  " <template #tab-0-trigger>$3</template>",
28
28
  " <template #tab-0-content>$4</template>",
29
- "</TabsCore>"
29
+ "</TabbedContent>"
30
30
  ]
31
31
  },
32
- "SRCDEV TabsCore Indicators Off": {
33
- "description": "TabsCore with the moving hover/active/underline decorators all disabled",
32
+ "SRCDEV TabbedContent Indicators Off": {
33
+ "description": "TabbedContent with the moving hover/active/underline decorators all disabled",
34
34
  "scope": "vue,html",
35
35
  "body": [
36
- "<TabsCore",
36
+ "<TabbedContent",
37
37
  " :item-count=\"${1:3}\"",
38
38
  " :track-hover=\"false\"",
39
39
  " :track-active=\"false\"",
40
40
  " :track-indicator=\"false\"",
41
41
  ">",
42
42
  " $2",
43
- "</TabsCore>"
43
+ "</TabbedContent>"
44
44
  ]
45
45
  },
46
- "SRCDEV TabsCore CSS Override": {
47
- "description": "CSS override scaffold for TabsCore — scope to a page or section class",
46
+ "SRCDEV TabbedContent CSS Override": {
47
+ "description": "CSS override scaffold for TabbedContent — scope to a page or section class",
48
48
  "scope": "css",
49
49
  "body": [
50
50
  ".$1my-page {",
51
- " .tabs-core {",
51
+ " .tabbed-content {",
52
52
  " --tabs-nav-border: ;",
53
53
  " --tabs-hover-indicator-colour: ;",
54
54
  " --tabs-hover-indicator-text-colour: ;",
@@ -56,6 +56,8 @@
56
56
  " --tabs-active-indicator-text-colour: ;",
57
57
  " --tabs-underline-indicator-colour: ;",
58
58
  " --tabs-underline-indicator-height: ;",
59
+ " --tabs-hover-underline-indicator-colour: ;",
60
+ " --tabs-hover-underline-indicator-height: ;",
59
61
  " --tabs-list-item-opacity: ;",
60
62
  " --tabs-list-item-colour: ;",
61
63
  " --tabs-list-item-colour-selected: ;",
@@ -63,16 +65,33 @@
63
65
  " --tabs-list-item-font-weight: ;",
64
66
  " --tabs-list-item-padding-block: ;",
65
67
  " --tabs-list-item-padding-inline: ;",
68
+ " --tabs-list-item-max-inline-size: ;",
66
69
  " --tabs-list-item-colour-transition-duration: ;",
67
- " --tabs-list-item-focus-outline-width: ;",
70
+ " --tabs-focus-ring-width: ;",
71
+ " --tabs-focus-ring-colour: ;",
68
72
  " --tabs-list-item-focus-outline-offset: ;",
73
+ " --tabs-more-popover-surface: ;",
74
+ " --tabs-more-popover-border: ;",
75
+ " --tabs-more-item-surface-hover: ;",
76
+ " --tabs-more-item-colour-selected: ;",
69
77
  " --tabs-content-background-colour: ;",
78
+ " --tabs-content-text-colour: ;",
70
79
  " --tabs-content-border-width: ;",
71
80
  " --tabs-content-border-colour: ;",
72
81
  " --tabs-content-border-radius: ;",
73
82
  " --tabs-axis-y-gap: ;",
83
+ " --tabs-axis-y-list-max-inline-size: ;",
74
84
  " }",
75
85
  "}"
76
86
  ]
87
+ },
88
+ "SRCDEV TabbedContent Scrolling Row": {
89
+ "description": "TabbedContent whose tab row scrolls sideways instead of collapsing into a More menu",
90
+ "scope": "vue,html",
91
+ "body": [
92
+ "<TabbedContent :item-count=\"${1:3}\" overflow-mode=\"scroll\" aria-label=\"${2:Product details}\">",
93
+ " $3",
94
+ "</TabbedContent>"
95
+ ]
77
96
  }
78
97
  }
@@ -0,0 +1,24 @@
1
+ /* Shared status colours. Each status has a solid colour (dots, markers) plus a tinted
2
+ surface/text pair (badges, pills). Component tokens fall back to these, so overriding
3
+ one here restyles that status across every component. */
4
+ :root {
5
+ --status-success: var(--green-06);
6
+ --status-success-surface: var(--green-01);
7
+ --status-success-text: var(--green-10);
8
+
9
+ --status-warning: var(--orange-05);
10
+ --status-warning-surface: var(--orange-01);
11
+ --status-warning-text: var(--orange-10);
12
+
13
+ --status-danger: var(--red-06);
14
+ --status-danger-surface: var(--red-01);
15
+ --status-danger-text: var(--red-10);
16
+
17
+ --status-info: var(--blue-06);
18
+ --status-info-surface: var(--blue-01);
19
+ --status-info-text: var(--blue-09);
20
+
21
+ --status-neutral: var(--slate-05);
22
+ --status-neutral-surface: var(--slate-01);
23
+ --status-neutral-text: var(--slate-09);
24
+ }
@@ -4,3 +4,4 @@
4
4
  @import "./_error.css";
5
5
  @import "./_success.css";
6
6
  @import "./_warning.css";
7
+ @import "./_status.css";
@@ -1,17 +1,18 @@
1
1
  # ContainerGlow — Consumer Styling Guide
2
2
 
3
- ## Public CSS custom properties
3
+ ## Public token API
4
4
 
5
- These are static, per-card visual tokens — override them with a `style` attribute or from a
6
- consuming page's own stylesheet:
5
+ Static, per-card visual tokens. Set them on an element you own (see Local overrides) or with a
6
+ `style` attribute:
7
7
 
8
8
  | Token | Default | Description |
9
9
  |---|---|---|
10
- | `--container-glow-background` | `white` | Card background colour. |
10
+ | `--container-glow-background` | `var(--theme-surface-subtle)` | Card background colour. |
11
+ | `--container-glow-text-colour` | `var(--theme-text)` | Card text colour. |
11
12
  | `--container-glow-padding` | `2rem` | Card inner padding. |
12
- | `--container-glow-aspect-ratio` | `330 / 400` | Card aspect ratio. |
13
+ | `--container-glow-aspect-ratio` | `330 / 400` | Card aspect ratio. Content taller than this grows the card. |
13
14
  | `--container-glow-border-radius` | `12px` | Card and glow-effect corner radius (shared by the card and every glow layer). |
14
- | `--container-glow-min-width` | `280px` | Card minimum width. |
15
+ | `--container-glow-min-width` | `280px` | Card minimum width, capped at the wrapper's width so a narrow viewport never overflows. |
15
16
  | `--container-glow-max-width` | `280px` | Card maximum width. |
16
17
  | `--container-glow-content-gap` | `0.25rem` | Gap between a card's own slotted children. |
17
18
  | `--container-glow-highlight-colour` | `hsl(280 10% 50% / 1)` | Tint of the thin proximity-tracking ring drawn under the coloured glow. |
@@ -19,9 +20,14 @@ consuming page's own stylesheet:
19
20
  | `--container-glow-transition-duration` | `1s` | Fade duration when the glow's opacity changes (also disabled under `prefers-reduced-motion: reduce`). |
20
21
  | `--container-glow-gradient` | 5-stop conic rainbow gradient | The glow's colour gradient. Override with any valid `background` value (a `conic-gradient(...)`, a flat colour, etc). |
21
22
 
23
+ > Changed 2026-10-08: `--container-glow-background` defaulted to `white` and the card set no text
24
+ > colour. It now defaults to `var(--theme-surface-subtle)`, with the new
25
+ > `--container-glow-text-colour` defaulting to `var(--theme-text)`. Set
26
+ > `--container-glow-background: white` to keep the old look.
27
+
22
28
  ```vue
23
29
  <ContainerGlow
24
- style="--container-glow-background: #111; --container-glow-border-radius: 4px; --container-glow-gradient: conic-gradient(from 180deg, #22d3ee, #a855f7, #22d3ee);"
30
+ style="--container-glow-background: #111; --container-glow-text-colour: #eee; --container-glow-gradient: conic-gradient(from 180deg, #22d3ee, #a855f7, #22d3ee);"
25
31
  >
26
32
  <template #cta>
27
33
  <p>Custom-coloured glow card</p>
@@ -29,25 +35,40 @@ consuming page's own stylesheet:
29
35
  </ContainerGlow>
30
36
  ```
31
37
 
32
- ---
38
+ Private tokens, not public API: `--_start`, `--_opacity-active`, `--_gap`, `--_blur`, `--_spread`,
39
+ `--_direction` and `--_gradient`. They're driven by pointer events and the `config` prop; use
40
+ `config` (gap/blur/spread/direction/opacity) or the public tokens above (colour/geometry) instead.
41
+
42
+ ## State hooks
43
+
44
+ | Selector | Element |
45
+ |---|---|
46
+ | `.container-glow-wrapper` | Root. A `<ul>` when `tag="li"`, otherwise a `<div>`. Flex row (column with `config.vertical`) that wraps onto new lines. |
47
+ | `.container-glow` | One card per named slot, rendered as the `tag` element. |
48
+ | `.container-glow-glows` | Decorative blurred glow layer inside each card (`aria-hidden`). |
49
+
50
+ > Changed 2026-10-08: the glow layer class was `.glows`; renamed to `.container-glow-glows` so a
51
+ > consumer's own `.glows` class can't collide with it. The wrapper also gained `flex-wrap: wrap`
52
+ > (many cards used to overflow sideways) and renders as a `<ul>` when `tag="li"` (previously an
53
+ > invalid `<li>` inside a `<div>`), with list margin, padding and bullets reset.
33
54
 
34
55
  ## The `config` prop (layout and interaction behaviour)
35
56
 
36
57
  Proximity distance, glow spread/blur, wrapper gap, wrapper direction, and the inactive-state
37
- opacity are **not** CSS tokens — they're computed per-pointer-move in JavaScript and applied as
38
- private custom properties, so they're driven entirely by the `config` prop instead
39
- (`ContainerGlowConfig`, importable from `srcdev-nuxt-components`):
58
+ opacity are **not** CSS tokens. They're computed in JavaScript and applied as private custom
59
+ properties, so they're driven entirely by the `config` prop (`ContainerGlowConfig`, importable
60
+ from `srcdev-nuxt-components`):
40
61
 
41
62
  | Field | Type | Default | Description |
42
63
  |---|---|---|---|
43
- | `proximity` | `number` | `40` | Pointer distance (px) at which a card starts glowing. |
44
- | `spread` | `number` | `80` | Angular spread of the glow (degrees). |
45
- | `blur` | `number` | `20` | Blur applied to the glow layer (px). |
46
- | `gap` | `number` | `32` | Gap between cards in the wrapper (px). |
64
+ | `proximity` | `number` | `40` | Pointer distance (px) at which a card starts glowing. Minimum `0`. |
65
+ | `spread` | `number` | `80` | Angular spread of the glow (degrees). Clamped to `0`–`360`. |
66
+ | `blur` | `number` | `20` | Blur applied to the glow layer (px). Minimum `0`. |
67
+ | `gap` | `number` | `32` | Gap between cards in the wrapper (px). Minimum `0`. |
47
68
  | `vertical` | `boolean` | `false` | Stack cards vertically instead of horizontally. |
48
- | `inactiveOpacity` | `number` | `0` | Glow opacity when the pointer isn't nearby. |
69
+ | `inactiveOpacity` | `number` | `0` | Glow opacity when the pointer isn't nearby. Clamped to `0`–`1`. |
49
70
 
50
- All fields are optional — pass only the ones you want to change:
71
+ Non-finite values (`NaN`, `Infinity`) fall back to the default. All fields are optional:
51
72
 
52
73
  ```vue
53
74
  <ContainerGlow :config="{ blur: 40, vertical: true }">
@@ -56,11 +77,9 @@ All fields are optional — pass only the ones you want to change:
56
77
  </ContainerGlow>
57
78
  ```
58
79
 
59
- ---
60
-
61
80
  ## Named dynamic slots
62
81
 
63
- Each named slot renders as one glow card — the consumer controls the slot names, there's no
82
+ Each named slot renders as one glow card. The consumer controls the slot names; there's no
64
83
  `itemCount`-style prop:
65
84
 
66
85
  ```vue
@@ -70,23 +89,11 @@ Each named slot renders as one glow card — the consumer controls the slot name
70
89
  </ContainerGlow>
71
90
  ```
72
91
 
73
- ---
74
-
75
92
  ## Motion
76
93
 
77
94
  The glow's opacity fade (`--container-glow-transition-duration`) is disabled under
78
95
  `prefers-reduced-motion: reduce`, so proximity changes are instant rather than animated.
79
96
 
80
- ---
81
-
82
- ## Internal (non-overridable) custom properties
83
-
84
- `--_start`, `--_opacity-active`, `--_gap`, `--_blur`, `--_spread`, `--_direction`, and `--_gradient`
85
- are internal plumbing driven by pointer events and the `config` prop — use `config` (for
86
- gap/blur/spread/direction/opacity) or the public tokens above (for colour/geometry) instead.
87
-
88
- ---
89
-
90
97
  ## Local overrides
91
98
 
92
99
  Set the tokens above on an element you own (a page or section class, or a class added with
@@ -95,8 +102,6 @@ Set the tokens above on an element you own (a page or section class, or a class
95
102
  `<style scoped>`, tokens set on your element still work, but selectors that reach inside the component need
96
103
  `:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
97
104
 
98
- ---
99
-
100
105
  ## Class passthrough
101
106
 
102
107
  Use `styleClassPassthrough` to add classes to the root `.container-glow-wrapper` element.
@@ -104,4 +109,3 @@ Use `styleClassPassthrough` to add classes to the root `.container-glow-wrapper`
104
109
  ```vue
105
110
  <ContainerGlow style-class-passthrough="my-container-glow">...</ContainerGlow>
106
111
  ```
107
-
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div ref="containerGlowWrapper" class="container-glow-wrapper" :class="elementClasses">
2
+ <component :is="wrapperTag" ref="containerGlowWrapper" class="container-glow-wrapper" :class="elementClasses">
3
3
  <component
4
4
  :is="tag"
5
5
  v-for="(_, name) in $slots"
@@ -7,10 +7,10 @@
7
7
  ref="containerGlowItem"
8
8
  class="container-glow"
9
9
  >
10
- <div class="glows"></div>
10
+ <div class="container-glow-glows" aria-hidden="true"></div>
11
11
  <slot :name="name"></slot>
12
12
  </component>
13
- </div>
13
+ </component>
14
14
  </template>
15
15
 
16
16
  <script setup lang="ts">
@@ -37,18 +37,22 @@ watch(
37
37
  }
38
38
  );
39
39
 
40
+ const wrapperTag = computed(() => (props.tag === "li" ? "ul" : "div"));
41
+
40
42
  const controller = new AbortController();
41
43
 
42
44
  const containerGlowWrapper = ref<HTMLElement>();
43
45
  const containerGlowItem = ref<HTMLElement[]>([]);
44
46
 
45
- // Cache frequently used values to avoid repeated access
47
+ const clampConfig = (value: number | undefined, fallback: number, min: number, max: number) =>
48
+ typeof value === "number" && Number.isFinite(value) ? Math.min(Math.max(value, min), max) : fallback;
49
+
46
50
  const configCache = computed(() => ({
47
- proximity: props.config.proximity ?? 40,
48
- inactiveOpacityStr: String(props.config.inactiveOpacity ?? 0),
49
- gapStr: String(props.config.gap ?? 32),
50
- blurStr: String(props.config.blur ?? 20),
51
- spreadStr: String(props.config.spread ?? 80),
51
+ proximity: clampConfig(props.config.proximity, 40, 0, Infinity),
52
+ inactiveOpacityStr: String(clampConfig(props.config.inactiveOpacity, 0, 0, 1)),
53
+ gapStr: String(clampConfig(props.config.gap, 32, 0, Infinity)),
54
+ blurStr: String(clampConfig(props.config.blur, 20, 0, Infinity)),
55
+ spreadStr: String(clampConfig(props.config.spread, 80, 0, 360)),
52
56
  direction: props.config.vertical ? "column" : "row",
53
57
  }));
54
58
 
@@ -164,7 +168,11 @@ onBeforeUnmount(() => {
164
168
  .container-glow-wrapper {
165
169
  display: flex;
166
170
  flex-direction: var(--_direction, row);
171
+ flex-wrap: wrap;
167
172
  gap: calc(var(--_gap, 32) * 1px);
173
+ margin: 0;
174
+ padding: 0;
175
+ list-style: none;
168
176
 
169
177
  .container-glow {
170
178
  & *,
@@ -185,18 +193,20 @@ onBeforeUnmount(() => {
185
193
  hsla(0, 0%, 98%, 1) 1turn
186
194
  )
187
195
  );
188
- --_opacity-active: v-bind("props.config.inactiveOpacity ?? 0");
196
+ --_opacity-active: v-bind("configCache.inactiveOpacityStr");
189
197
  --_start: 0;
190
198
 
191
199
  position: relative;
192
200
 
193
201
  height: 100%;
194
- background: var(--container-glow-background, white);
202
+ background: var(--container-glow-background, var(--theme-surface-subtle));
203
+ color: var(--container-glow-text-colour, var(--theme-text));
195
204
  padding: var(--container-glow-padding, 2rem);
196
205
  aspect-ratio: var(--container-glow-aspect-ratio, 330 / 400);
197
206
  border-radius: var(--container-glow-border-radius, 12px);
198
- min-width: var(--container-glow-min-width, 280px);
207
+ min-width: min(var(--container-glow-min-width, 280px), 100%);
199
208
  max-width: var(--container-glow-max-width, 280px);
209
+ overflow-wrap: anywhere;
200
210
  display: flex;
201
211
  flex-direction: column;
202
212
  gap: var(--container-glow-content-gap, 0.25rem);
@@ -255,7 +265,7 @@ onBeforeUnmount(() => {
255
265
  mask-composite: intersect;
256
266
  }
257
267
 
258
- .glows {
268
+ .container-glow-glows {
259
269
  pointer-events: none;
260
270
  position: absolute;
261
271
  inset: 0;
@@ -288,8 +298,8 @@ onBeforeUnmount(() => {
288
298
  @media (prefers-reduced-motion: reduce) {
289
299
  &::before,
290
300
  &::after,
291
- .glows::before,
292
- .glows::after {
301
+ .container-glow-glows::before,
302
+ .container-glow-glows::after {
293
303
  transition: none;
294
304
  }
295
305
  }