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
@@ -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
  }
@@ -5,16 +5,16 @@
5
5
  "body": [
6
6
  "<ProfileSection",
7
7
  " tag=\"section\"",
8
- " :profile-picture=\"{ src: '$1', alt: '$2' }\"",
8
+ " :profile-picture=\"{ src: '$2', alt: '$3' }\"",
9
9
  ">",
10
10
  " <template #eyebrowText>",
11
- " <EyebrowText text-content=\"$3\" />",
11
+ " <EyebrowText text-content=\"$4\" />",
12
12
  " </template>",
13
13
  " <template #heroText=\"{ headingId }\">",
14
- " <HeroText :id=\"headingId\" tag=\"h2\" :text-content=\"[{ text: '$4' }]\" />",
14
+ " <HeroText :id=\"headingId\" tag=\"h2\" :text-content=\"[{ text: '$5' }]\" />",
15
15
  " </template>",
16
16
  " <template #profile-info-1>",
17
- " <p>$5</p>",
17
+ " <p>$6</p>",
18
18
  " </template>",
19
19
  " <template #profile-info-2>",
20
20
  " <p>$6</p>",
@@ -22,6 +22,27 @@
22
22
  "</ProfileSection>"
23
23
  ]
24
24
  },
25
+ "SRCDEV ProfileSection Header In Picture": {
26
+ "description": "ProfileSection with the eyebrow and heading over the bottom or top of the picture, on a scrim",
27
+ "scope": "vue,html",
28
+ "body": [
29
+ "<ProfileSection",
30
+ " tag=\"section\"",
31
+ " header-placement=\"${1|picture-bottom,picture-top|}\"",
32
+ " :profile-picture=\"{ src: '$2', alt: '$3' }\"",
33
+ ">",
34
+ " <template #eyebrowText>",
35
+ " <EyebrowText text-content=\"$4\" />",
36
+ " </template>",
37
+ " <template #heroText=\"{ headingId }\">",
38
+ " <HeroText :id=\"headingId\" tag=\"h2\" :text-content=\"[{ text: '$5' }]\" />",
39
+ " </template>",
40
+ " <template #profile-info-1>",
41
+ " <p>$6</p>",
42
+ " </template>",
43
+ "</ProfileSection>"
44
+ ]
45
+ },
25
46
  "SRCDEV ProfileSection CSS Override": {
26
47
  "description": "CSS override scaffold for ProfileSection tokens",
27
48
  "scope": "css",
@@ -30,6 +51,8 @@
30
51
  " --profile-section-picture-width: $2;",
31
52
  " --profile-section-gap-wide: $3;",
32
53
  " --profile-section-picture-border-radius: $4;",
54
+ " --profile-section-highlight-colour: $5;",
55
+ " --profile-section-link-colour: $6;",
33
56
  "}"
34
57
  ]
35
58
  }
@@ -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
  }