srcdev-nuxt-components 9.4.9 → 9.4.10

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 (112) hide show
  1. package/.claude/commands/migrate-component.md +20 -6
  2. package/.claude/component-ledger/audit.json +1 -1
  3. package/.claude/component-ledger/build.mjs +26 -0
  4. package/.claude/component-ledger/output.html +8 -3
  5. package/.claude/component-ledger/template.html +7 -2
  6. package/.claude/skills/component-dynamic-slots.md +1 -1
  7. package/.claude/skills/components/alert-mask-core.md +84 -0
  8. package/.claude/skills/components/canvas-switcher.md +1 -1
  9. package/.claude/skills/components/carousel-flip.md +112 -168
  10. package/.claude/skills/components/container-glow.md +123 -0
  11. package/.claude/skills/components/dashboard-quad-grid.md +98 -0
  12. package/.claude/skills/components/dashboard-stats-grid.md +103 -0
  13. package/.claude/skills/components/display-theme-switch.md +60 -0
  14. package/.claude/skills/components/glowing-border.md +66 -0
  15. package/.claude/skills/components/input-range-core.md +142 -0
  16. package/.claude/skills/components/section-parallax.md +32 -10
  17. package/.claude/skills/components/ui-block-decorated.md +56 -0
  18. package/.claude/skills/components/wipe-away-vertical.md +98 -0
  19. package/.claude/skills/icon-sets.md +1 -1
  20. package/.claude/skills/index.md +10 -1
  21. package/.vscode/css-custom-data.json +38 -0
  22. package/.vscode/settings.json +3 -0
  23. package/.vscode/srcdev-component-alert-mask-core.code-snippets +31 -0
  24. package/.vscode/srcdev-component-carousel-flip.code-snippets +41 -0
  25. package/.vscode/srcdev-component-container-glow.code-snippets +33 -0
  26. package/.vscode/srcdev-component-dashboard-quad-grid.code-snippets +36 -0
  27. package/.vscode/srcdev-component-dashboard-stats-grid.code-snippets +35 -0
  28. package/.vscode/srcdev-component-display-theme-switch.code-snippets +27 -0
  29. package/.vscode/srcdev-component-glowing-border.code-snippets +23 -0
  30. package/.vscode/srcdev-component-input-range.code-snippets +85 -0
  31. package/.vscode/srcdev-component-section-parallax.code-snippets +27 -0
  32. package/.vscode/srcdev-component-ui-block-decorated.code-snippets +24 -0
  33. package/.vscode/srcdev-component-wipe-away-vertical.code-snippets +25 -0
  34. package/README.md +18 -37
  35. package/app/components/{alert-mask → 01.atoms/alert-mask}/AlertMaskCore.vue +16 -17
  36. package/app/components/01.atoms/alert-mask/CONSUMER-STYLING.md +46 -0
  37. package/app/components/01.atoms/alert-mask/stories/AlertMaskCore.stories.ts +79 -0
  38. package/app/components/{alert-mask → 01.atoms/alert-mask}/tests/AlertMaskCore.spec.ts +10 -10
  39. package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +86 -0
  40. package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +299 -0
  41. package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +175 -0
  42. package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +131 -0
  43. package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +14 -0
  44. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +61 -0
  45. package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +132 -0
  46. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +165 -0
  47. package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +94 -0
  48. package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +3 -0
  49. package/app/components/01.atoms/animations/section-parallax/CONSUMER-STYLING.md +44 -0
  50. package/app/components/{parallax → 01.atoms/animations/section-parallax}/SectionParallax.vue +11 -10
  51. package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts +98 -0
  52. package/app/components/01.atoms/animations/section-parallax/tests/SectionParallax.spec.ts +102 -0
  53. package/app/components/01.atoms/animations/section-parallax/tests/__snapshots__/SectionParallax.spec.ts.snap +9 -0
  54. package/app/components/01.atoms/animations/view-timeline/CONSUMER-STYLING.md +70 -0
  55. package/app/components/01.atoms/animations/view-timeline/WipeAwayVertical.vue +181 -0
  56. package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts +269 -0
  57. package/app/components/01.atoms/animations/view-timeline/tests/WipeAwayVertical.spec.ts +127 -0
  58. package/app/components/01.atoms/canvas-switcher/CONSUMER-STYLING.md +1 -1
  59. package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +1 -1
  60. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +42 -0
  61. package/app/components/{layout-grids/LayoutGridA.vue → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue} +23 -13
  62. package/app/components/{layout-grids/stories/LayoutGridA.stories.ts → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts} +151 -23
  63. package/app/components/{layout-grids/tests/LayoutGridA.spec.ts → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/tests/DashboardQuadGrid.spec.ts} +25 -25
  64. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +41 -0
  65. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +133 -0
  66. package/app/components/{layout-grids/stories/LayoutGridB.stories.ts → 01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/stories/DashboardStatsGrid.stories.ts} +24 -24
  67. package/app/components/{layout-grids/tests/LayoutGridB.spec.ts → 01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/tests/DashboardStatsGrid.spec.ts} +23 -23
  68. package/app/components/01.atoms/ui-block-decorated/CONSUMER-STYLING.md +39 -0
  69. package/app/components/01.atoms/ui-block-decorated/UiBlockDecorated.vue +141 -0
  70. package/app/components/01.atoms/ui-block-decorated/stories/UiBlockDecorated.stories.ts +148 -0
  71. package/app/components/01.atoms/ui-block-decorated/tests/UiBlockDecorated.spec.ts +80 -0
  72. package/app/components/01.atoms/ui-block-decorated/tests/__snapshots__/UiBlockDecorated.spec.ts.snap +3 -0
  73. package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +6 -6
  74. package/app/components/02.molecules/display-theme-switch/CONSUMER-STYLING.md +34 -0
  75. package/app/components/02.molecules/display-theme-switch/DisplayThemeSwitch.vue +102 -0
  76. package/app/components/02.molecules/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +115 -0
  77. package/app/components/02.molecules/display-theme-switch/tests/DisplayThemeSwitch.spec.ts +86 -0
  78. package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +46 -0
  79. package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/CarouselFlip.vue +63 -42
  80. package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/stories/CarouselFlip.stories.ts +96 -11
  81. package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/tests/CarouselFlip.spec.ts +80 -0
  82. package/app/components/05.forms/form-field/FormField.vue +1 -1
  83. package/app/components/05.forms/form-wrapper/FormWrapper.vue +1 -1
  84. package/app/components/05.forms/input-range/CONSUMER-STYLING.md +45 -0
  85. package/app/components/05.forms/input-range/InputRangeCore.vue +79 -143
  86. package/app/components/05.forms/input-range/stories/InputRangeCore.stories.ts +126 -0
  87. package/app/components/05.forms/input-range/tests/InputRangeCore.spec.ts +124 -0
  88. package/app/components/05.forms/input-range/variants/InputRangeDefault.vue +9 -32
  89. package/app/components/05.forms/input-range/variants/stories/InputRangeDefault.stories.ts +108 -0
  90. package/app/components/05.forms/input-range/variants/tests/InputRangeDefault.spec.ts +140 -0
  91. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +135 -0
  92. package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +16 -2
  93. package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitchCore.vue +6 -4
  94. package/app/types/components/container-glow.d.ts +8 -0
  95. package/app/types/components/index.ts +1 -1
  96. package/package.json +3 -2
  97. package/app/components/05.forms/input-range-fancy/InputRangeFancyCore.vue +0 -426
  98. package/app/components/05.forms/input-range-fancy/InputRangeFancyWithLabel.vue +0 -94
  99. package/app/components/carousels/CarouselBasic.vue +0 -317
  100. package/app/components/carousels/CarouselInfinite.vue +0 -358
  101. package/app/components/container-glow/ContainerGlowCore.vue +0 -296
  102. package/app/components/container-glow/stories/ContainerGlowCore.stories.ts +0 -337
  103. package/app/components/display-theme-switch/DisplayThemeSwitch.vue +0 -189
  104. package/app/components/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +0 -154
  105. package/app/components/glowing-border/GlowingBorder.vue +0 -142
  106. package/app/components/glowing-border/stories/GlowingBorder.stories.ts +0 -141
  107. package/app/components/layout-grids/LayoutGridB.vue +0 -134
  108. package/app/components/test-storybook/TestStorybook.vue +0 -49
  109. package/app/components/test-storybook/stories/TestStorybook.stories.ts +0 -28
  110. package/app/components/ui-block-decorated/UiBlockDecorated.vue +0 -167
  111. package/app/components/view-timeline/WipeAwayVertical.vue +0 -180
  112. package/app/types/components/carousel-basic.d.ts +0 -19
@@ -0,0 +1,299 @@
1
+ <template>
2
+ <div ref="containerGlowWrapper" class="container-glow-wrapper" :class="elementClasses">
3
+ <component
4
+ :is="tag"
5
+ v-for="(_, name) in $slots"
6
+ :key="name"
7
+ ref="containerGlowItem"
8
+ class="container-glow"
9
+ >
10
+ <div class="glows"></div>
11
+ <slot :name="name"></slot>
12
+ </component>
13
+ </div>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ import type { ContainerGlowConfig } from "~/types/components";
18
+
19
+ interface Props {
20
+ tag?: "div" | "li" | "article" | "section";
21
+ config?: ContainerGlowConfig;
22
+ styleClassPassthrough?: string | string[];
23
+ }
24
+
25
+ const props = withDefaults(defineProps<Props>(), {
26
+ tag: "div",
27
+ config: () => ({}),
28
+ styleClassPassthrough: () => [],
29
+ });
30
+
31
+ const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
32
+
33
+ watch(
34
+ () => props.styleClassPassthrough,
35
+ () => {
36
+ resetElementClasses(props.styleClassPassthrough);
37
+ }
38
+ );
39
+
40
+ const controller = new AbortController();
41
+
42
+ const containerGlowWrapper = ref<HTMLElement>();
43
+ const containerGlowItem = ref<HTMLElement[]>([]);
44
+
45
+ // Cache frequently used values to avoid repeated access
46
+ 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),
52
+ direction: props.config.vertical ? "column" : "row",
53
+ }));
54
+
55
+ // Pre-calculate constants to avoid repeated calculations
56
+ const ANGLE_OFFSET = 90;
57
+ const FULL_CIRCLE = 360;
58
+ const RAD_TO_DEG = 180 / Math.PI;
59
+
60
+ // Throttle mechanism using RAF
61
+ let rafId: number | null = null;
62
+ let lastEventData: { x: number; y: number } | null = null;
63
+
64
+ const updateStyles = (event: PointerEvent) => {
65
+ // Early return if event coordinates are not available
66
+ if (typeof event.x !== "number" || typeof event.y !== "number") {
67
+ return;
68
+ }
69
+
70
+ // Store event data and schedule update
71
+ lastEventData = { x: event.x, y: event.y };
72
+
73
+ if (rafId !== null) {
74
+ return; // Already scheduled
75
+ }
76
+
77
+ rafId = requestAnimationFrame(() => {
78
+ if (!lastEventData) {
79
+ rafId = null;
80
+ return;
81
+ }
82
+
83
+ const { x: eventX, y: eventY } = lastEventData;
84
+ const { proximity, inactiveOpacityStr } = configCache.value;
85
+
86
+ // Process all elements
87
+ for (const cardElem of containerGlowItem.value) {
88
+ // Check the card against the proximity and then start updating
89
+ const cardBounds = cardElem.getBoundingClientRect();
90
+
91
+ // Pre-calculate bounds to avoid repeated operations
92
+ const leftBound = cardBounds.left - proximity;
93
+ const rightBound = cardBounds.left + cardBounds.width + proximity;
94
+ const topBound = cardBounds.top - proximity;
95
+ const bottomBound = cardBounds.top + cardBounds.height + proximity;
96
+
97
+ // Check proximity with pre-calculated bounds
98
+ const isInProximity = eventX > leftBound && eventX < rightBound && eventY > topBound && eventY < bottomBound;
99
+
100
+ // Set opacity based on proximity
101
+ cardElem.style.setProperty("--_opacity-active", isInProximity ? "1" : inactiveOpacityStr);
102
+
103
+ if (isInProximity) {
104
+ // Only calculate angle when in proximity
105
+ const cardCentreX = cardBounds.left + cardBounds.width * 0.5;
106
+ const cardCentreY = cardBounds.top + cardBounds.height * 0.5;
107
+
108
+ let angle = Math.atan2(eventY - cardCentreY, eventX - cardCentreX) * RAD_TO_DEG;
109
+ angle = angle < 0 ? angle + FULL_CIRCLE : angle;
110
+
111
+ cardElem.style.setProperty("--_start", String(angle + ANGLE_OFFSET));
112
+ }
113
+ }
114
+
115
+ rafId = null;
116
+ });
117
+ };
118
+
119
+ const applyStyles = () => {
120
+ if (!containerGlowWrapper.value) return;
121
+
122
+ const { gapStr, blurStr, spreadStr, direction } = configCache.value;
123
+ const wrapper = containerGlowWrapper.value;
124
+
125
+ // Batch DOM updates
126
+ wrapper.style.setProperty("--_gap", gapStr);
127
+ wrapper.style.setProperty("--_blur", blurStr);
128
+ wrapper.style.setProperty("--_spread", spreadStr);
129
+ wrapper.style.setProperty("--_direction", direction);
130
+ };
131
+
132
+ // Watch for config changes and reapply styles
133
+ watch(() => props.config, applyStyles, { deep: true });
134
+
135
+ onMounted(() => {
136
+ applyStyles();
137
+ if (containerGlowWrapper.value) {
138
+ document.body.addEventListener("pointermove", updateStyles, {
139
+ signal: controller.signal,
140
+ passive: true, // Improve scroll performance
141
+ });
142
+ }
143
+ });
144
+
145
+ onBeforeUnmount(() => {
146
+ // Clean up RAF if pending
147
+ if (rafId !== null) {
148
+ cancelAnimationFrame(rafId);
149
+ rafId = null;
150
+ }
151
+ lastEventData = null;
152
+ controller.abort();
153
+ });
154
+ </script>
155
+
156
+ <style lang="css">
157
+ @layer components {
158
+ @property --_start {
159
+ syntax: "<number>";
160
+ inherits: true;
161
+ initial-value: 0;
162
+ }
163
+
164
+ .container-glow-wrapper {
165
+ display: flex;
166
+ flex-direction: var(--_direction, row);
167
+ gap: calc(var(--_gap, 32) * 1px);
168
+
169
+ .container-glow {
170
+ & *,
171
+ & *:after,
172
+ & *:before {
173
+ box-sizing: border-box;
174
+ }
175
+
176
+ --_gradient: var(
177
+ --container-glow-gradient,
178
+ conic-gradient(
179
+ from 180deg at 50% 70%,
180
+ hsla(0, 0%, 98%, 1) 0deg,
181
+ #eec32d 72.0000010728836deg,
182
+ #ec4b4b 144.0000021457672deg,
183
+ #709ab9 216.00000858306885deg,
184
+ #4dffbf 288.0000042915344deg,
185
+ hsla(0, 0%, 98%, 1) 1turn
186
+ )
187
+ );
188
+ --_opacity-active: v-bind("props.config.inactiveOpacity ?? 0");
189
+ --_start: 0;
190
+
191
+ position: relative;
192
+
193
+ height: 100%;
194
+ background: var(--container-glow-background, light-dark(white, hsl(246 44% 7%)));
195
+ padding: var(--container-glow-padding, 2rem);
196
+ aspect-ratio: var(--container-glow-aspect-ratio, 330 / 400);
197
+ border-radius: var(--container-glow-border-radius, 12px);
198
+ min-width: var(--container-glow-min-width, 280px);
199
+ max-width: var(--container-glow-max-width, 280px);
200
+ display: flex;
201
+ flex-direction: column;
202
+ gap: var(--container-glow-content-gap, 0.25rem);
203
+
204
+ &:is(:hover, :focus-visible) {
205
+ z-index: 2;
206
+ }
207
+
208
+ &::before,
209
+ &::after {
210
+ content: "";
211
+ position: absolute;
212
+ inset: 0;
213
+ }
214
+
215
+ &::before {
216
+ pointer-events: none;
217
+
218
+ border: 2px solid transparent;
219
+ border-radius: var(--container-glow-border-radius, 12px);
220
+ background: var(--container-glow-highlight-colour, hsl(280 10% 50% / 1));
221
+ background-attachment: fixed;
222
+ mask:
223
+ linear-gradient(#0000, #0000),
224
+ conic-gradient(
225
+ from calc(((var(--_start) + (var(--_spread) * 0.25)) - (var(--_spread) * 1.5)) * 1deg),
226
+ hsl(0 0% 100% / 0.15) 0deg,
227
+ white,
228
+ hsl(0 0% 100% / 0.15) calc(var(--_spread) * 2.5deg)
229
+ );
230
+ mask-clip: padding-box, border-box;
231
+ mask-composite: intersect;
232
+ opacity: var(--_opacity-active);
233
+ transition: opacity var(--container-glow-transition-duration, 1s);
234
+ }
235
+
236
+ &::after {
237
+ pointer-events: none;
238
+
239
+ background: var(--_gradient);
240
+ background-attachment: fixed;
241
+ border-radius: var(--container-glow-border-radius, 12px);
242
+ opacity: var(--_opacity-active, 0);
243
+ transition: opacity var(--container-glow-transition-duration, 1s);
244
+ border: 2px solid transparent;
245
+ mask:
246
+ linear-gradient(#0000, #0000),
247
+ conic-gradient(
248
+ from calc(((var(--_start) + (var(--_spread) * 0.25)) - (var(--_spread) * 0.5)) * 1deg),
249
+ #0000 0deg,
250
+ #fff,
251
+ #0000 calc(var(--_spread) * 0.5deg)
252
+ );
253
+ filter: brightness(var(--container-glow-brightness, 1.5));
254
+ mask-clip: padding-box, border-box;
255
+ mask-composite: intersect;
256
+ }
257
+
258
+ .glows {
259
+ pointer-events: none;
260
+ position: absolute;
261
+ inset: 0;
262
+ filter: blur(calc(var(--_blur) * 1px));
263
+
264
+ &::after,
265
+ &::before {
266
+ content: "";
267
+ background: var(--_gradient);
268
+ background-attachment: fixed;
269
+ position: absolute;
270
+ inset: -5px;
271
+ border: 10px solid transparent;
272
+ border-radius: var(--container-glow-border-radius, 12px);
273
+ mask:
274
+ linear-gradient(#0000, #0000),
275
+ conic-gradient(
276
+ from calc((var(--_start) - (var(--_spread) * 0.5)) * 1deg),
277
+ #000 0deg,
278
+ #fff,
279
+ #0000 calc(var(--_spread) * 1deg)
280
+ );
281
+ mask-composite: intersect;
282
+ mask-clip: padding-box, border-box;
283
+ opacity: var(--_opacity-active);
284
+ transition: opacity var(--container-glow-transition-duration, 1s);
285
+ }
286
+ }
287
+
288
+ @media (prefers-reduced-motion: reduce) {
289
+ &::before,
290
+ &::after,
291
+ .glows::before,
292
+ .glows::after {
293
+ transition: none;
294
+ }
295
+ }
296
+ }
297
+ }
298
+ }
299
+ </style>
@@ -0,0 +1,175 @@
1
+ import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
+ import { computed } from "vue";
3
+ import StorybookComponent from "../ContainerGlow.vue";
4
+
5
+ type StoryArgs = {
6
+ tag: "div" | "li" | "article" | "section";
7
+ proximity: number;
8
+ spread: number;
9
+ blur: number;
10
+ gap: number;
11
+ vertical: boolean;
12
+ inactiveOpacity: number;
13
+ };
14
+
15
+ export default {
16
+ title: "Atoms/Effects/ContainerGlow",
17
+ component: StorybookComponent,
18
+ argTypes: {
19
+ tag: {
20
+ control: { type: "select" },
21
+ options: ["div", "li", "article", "section"],
22
+ description: "HTML tag rendered for each glow container",
23
+ },
24
+ proximity: {
25
+ control: { type: "range", min: 10, max: 200, step: 10 },
26
+ description: "Mouse proximity distance (px) that triggers the glow",
27
+ table: { category: "config" },
28
+ },
29
+ spread: {
30
+ control: { type: "range", min: 20, max: 180, step: 10 },
31
+ description: "Angular spread of the glow effect (degrees)",
32
+ table: { category: "config" },
33
+ },
34
+ blur: {
35
+ control: { type: "range", min: 0, max: 50, step: 2 },
36
+ description: "Blur amount for the glow effect (px)",
37
+ table: { category: "config" },
38
+ },
39
+ gap: {
40
+ control: { type: "range", min: 8, max: 64, step: 4 },
41
+ description: "Gap between containers (px)",
42
+ table: { category: "config" },
43
+ },
44
+ vertical: {
45
+ control: { type: "boolean" },
46
+ description: "Arrange containers vertically instead of horizontally",
47
+ table: { category: "config" },
48
+ },
49
+ inactiveOpacity: {
50
+ control: { type: "range", min: 0, max: 1, step: 0.05 },
51
+ description: "Opacity of the glow when not being hovered near",
52
+ table: { category: "config" },
53
+ },
54
+ styleClassPassthrough: {
55
+ table: { disable: true },
56
+ },
57
+ class: {
58
+ table: { disable: true },
59
+ },
60
+ style: {
61
+ table: { disable: true },
62
+ },
63
+ },
64
+ args: {
65
+ tag: "div",
66
+ proximity: 40,
67
+ spread: 80,
68
+ blur: 20,
69
+ gap: 32,
70
+ vertical: false,
71
+ inactiveOpacity: 0,
72
+ },
73
+ parameters: {
74
+ docs: {
75
+ description: {
76
+ 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.",
78
+ },
79
+ },
80
+ },
81
+ } as Meta<StoryArgs>;
82
+
83
+ const cardStyle =
84
+ "height: 100%; display: flex; flex-direction: column; justify-content: center; text-align: center; color: #374151;";
85
+
86
+ const Template: StoryFn<StoryArgs> = (args) => ({
87
+ components: { StorybookComponent },
88
+ 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 };
98
+ },
99
+ 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;">
101
+ <StorybookComponent :tag="args.tag" :config="config">
102
+ <template #one>
103
+ <div style="${cardStyle}">
104
+ <h3 style="margin: 0 0 8px 0;">Container 1</h3>
105
+ <p style="margin: 0; font-size: 1.3rem;">Move your cursor nearby</p>
106
+ </div>
107
+ </template>
108
+ <template #two>
109
+ <div style="${cardStyle}">
110
+ <h3 style="margin: 0 0 8px 0;">Container 2</h3>
111
+ <p style="margin: 0; font-size: 1.3rem;">Each named slot is one card</p>
112
+ </div>
113
+ </template>
114
+ <template #three>
115
+ <div style="${cardStyle}">
116
+ <h3 style="margin: 0 0 8px 0;">Container 3</h3>
117
+ <p style="margin: 0; font-size: 1.3rem;">No item-count prop needed</p>
118
+ </div>
119
+ </template>
120
+ </StorybookComponent>
121
+ </div>
122
+ `,
123
+ });
124
+
125
+ export const Default = Template.bind({});
126
+
127
+ export const SingleContainer: StoryFn<StoryArgs> = (args) => ({
128
+ components: { StorybookComponent },
129
+ 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 };
139
+ },
140
+ 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;">
142
+ <StorybookComponent :tag="args.tag" :config="config">
143
+ <template #cta>
144
+ <div style="${cardStyle}">
145
+ <h3 style="margin: 0 0 8px 0;">Single container</h3>
146
+ <p style="margin: 0; font-size: 1.3rem;">Perfect for one highlighted call-to-action card</p>
147
+ </div>
148
+ </template>
149
+ </StorybookComponent>
150
+ </div>
151
+ `,
152
+ });
153
+
154
+ export const Vertical = Template.bind({});
155
+ Vertical.args = {
156
+ vertical: true,
157
+ };
158
+
159
+ export const HighIntensity = Template.bind({});
160
+ HighIntensity.args = {
161
+ proximity: 100,
162
+ spread: 120,
163
+ blur: 40,
164
+ gap: 48,
165
+ inactiveOpacity: 0,
166
+ };
167
+
168
+ export const SubtleGlow = Template.bind({});
169
+ SubtleGlow.args = {
170
+ proximity: 30,
171
+ spread: 40,
172
+ blur: 10,
173
+ gap: 20,
174
+ inactiveOpacity: 0,
175
+ };
@@ -0,0 +1,131 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
3
+ import ContainerGlow from "../ContainerGlow.vue";
4
+
5
+ describe("ContainerGlow", () => {
6
+ // ─── Mount ───────────────────────────────────────────────────────────────
7
+
8
+ it("mounts without error", async () => {
9
+ const wrapper = await mountSuspended(ContainerGlow, {
10
+ slots: { one: "<p>One</p>" },
11
+ });
12
+ expect(wrapper.vm).toBeTruthy();
13
+ });
14
+
15
+ // ─── Snapshots ───────────────────────────────────────────────────────────
16
+
17
+ it("renders correct HTML structure (default)", async () => {
18
+ const wrapper = await mountSuspended(ContainerGlow, {
19
+ slots: { one: "<p>One</p>", two: "<p>Two</p>" },
20
+ });
21
+ expect(wrapper.html()).toMatchSnapshot();
22
+ });
23
+
24
+ // ─── Wrapper ────────────────────────────────────────────────────────────
25
+
26
+ it("always has the container-glow-wrapper class on the root", async () => {
27
+ const wrapper = await mountSuspended(ContainerGlow, {
28
+ slots: { one: "<p>One</p>" },
29
+ });
30
+ expect(wrapper.classes()).toContain("container-glow-wrapper");
31
+ });
32
+
33
+ // ─── Named dynamic slots ────────────────────────────────────────────────
34
+
35
+ it("renders one container-glow item per named slot", async () => {
36
+ const wrapper = await mountSuspended(ContainerGlow, {
37
+ slots: { one: "<p class='one'>One</p>", two: "<p class='two'>Two</p>", three: "<p class='three'>Three</p>" },
38
+ });
39
+ expect(wrapper.findAll(".container-glow").length).toBe(3);
40
+ expect(wrapper.find(".one").exists()).toBe(true);
41
+ expect(wrapper.find(".two").exists()).toBe(true);
42
+ expect(wrapper.find(".three").exists()).toBe(true);
43
+ });
44
+
45
+ it("renders no container-glow items when no slots are provided", async () => {
46
+ const wrapper = await mountSuspended(ContainerGlow);
47
+ expect(wrapper.findAll(".container-glow").length).toBe(0);
48
+ });
49
+
50
+ // ─── Tag ────────────────────────────────────────────────────────────────
51
+
52
+ it("renders each item as <div> by default", async () => {
53
+ const wrapper = await mountSuspended(ContainerGlow, {
54
+ slots: { one: "<p>One</p>" },
55
+ });
56
+ expect(wrapper.find(".container-glow").element.tagName).toBe("DIV");
57
+ });
58
+
59
+ it.each(["div", "li", "article", "section"] as const)("renders each item as <%s> when tag='%s'", async (tag) => {
60
+ const wrapper = await mountSuspended(ContainerGlow, {
61
+ props: { tag },
62
+ slots: { one: "<p>One</p>" },
63
+ });
64
+ expect(wrapper.find(".container-glow").element.tagName).toBe(tag.toUpperCase());
65
+ });
66
+
67
+ // ─── Config ─────────────────────────────────────────────────────────────
68
+
69
+ it("applies default config values to the wrapper as CSS custom properties", async () => {
70
+ const wrapper = await mountSuspended(ContainerGlow, {
71
+ slots: { one: "<p>One</p>" },
72
+ });
73
+ const style = wrapper.element.style;
74
+ expect(style.getPropertyValue("--_gap")).toBe("32");
75
+ expect(style.getPropertyValue("--_blur")).toBe("20");
76
+ expect(style.getPropertyValue("--_spread")).toBe("80");
77
+ expect(style.getPropertyValue("--_direction")).toBe("row");
78
+ });
79
+
80
+ it("applies custom config values to the wrapper as CSS custom properties", async () => {
81
+ const wrapper = await mountSuspended(ContainerGlow, {
82
+ props: { config: { gap: 48, blur: 40, spread: 120, vertical: true } },
83
+ slots: { one: "<p>One</p>" },
84
+ });
85
+ const style = wrapper.element.style;
86
+ expect(style.getPropertyValue("--_gap")).toBe("48");
87
+ expect(style.getPropertyValue("--_blur")).toBe("40");
88
+ expect(style.getPropertyValue("--_spread")).toBe("120");
89
+ expect(style.getPropertyValue("--_direction")).toBe("column");
90
+ });
91
+
92
+ it("does not require every config field to be set", async () => {
93
+ const wrapper = await mountSuspended(ContainerGlow, {
94
+ props: { config: { blur: 5 } },
95
+ slots: { one: "<p>One</p>" },
96
+ });
97
+ const style = wrapper.element.style;
98
+ expect(style.getPropertyValue("--_blur")).toBe("5");
99
+ expect(style.getPropertyValue("--_gap")).toBe("32");
100
+ });
101
+
102
+ // ─── styleClassPassthrough ────────────────────────────────────────────────
103
+
104
+ it("applies a single styleClassPassthrough string", async () => {
105
+ const wrapper = await mountSuspended(ContainerGlow, {
106
+ props: { styleClassPassthrough: "featured" },
107
+ slots: { one: "<p>One</p>" },
108
+ });
109
+ expect(wrapper.classes()).toContain("featured");
110
+ });
111
+
112
+ it("applies multiple styleClassPassthrough classes from an array", async () => {
113
+ const wrapper = await mountSuspended(ContainerGlow, {
114
+ props: { styleClassPassthrough: ["featured", "highlighted"] },
115
+ slots: { one: "<p>One</p>" },
116
+ });
117
+ expect(wrapper.classes()).toContain("featured");
118
+ expect(wrapper.classes()).toContain("highlighted");
119
+ });
120
+
121
+ it("updates classes when styleClassPassthrough prop changes", async () => {
122
+ const wrapper = await mountSuspended(ContainerGlow, {
123
+ props: { styleClassPassthrough: ["featured"] },
124
+ slots: { one: "<p>One</p>" },
125
+ });
126
+ expect(wrapper.classes()).toContain("featured");
127
+ await wrapper.setProps({ styleClassPassthrough: ["highlighted"] });
128
+ expect(wrapper.classes()).not.toContain("featured");
129
+ expect(wrapper.classes()).toContain("highlighted");
130
+ });
131
+ });
@@ -0,0 +1,14 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`ContainerGlow > renders correct HTML structure (default) 1`] = `
4
+ "<div class="container-glow-wrapper" style="--_gap: 32; --_blur: 20; --_spread: 80; --_direction: row;">
5
+ <div class="container-glow">
6
+ <div class="glows"></div>
7
+ <p>One</p>
8
+ </div>
9
+ <div class="container-glow">
10
+ <div class="glows"></div>
11
+ <p>Two</p>
12
+ </div>
13
+ </div>"
14
+ `;
@@ -0,0 +1,61 @@
1
+ # GlowingBorder — Consumer Styling Guide
2
+
3
+ ## Public CSS custom properties
4
+
5
+ | Token | Default | Description |
6
+ |---|---|---|
7
+ | `--glowing-border-width` | `3px` | Border thickness. |
8
+ | `--glowing-border-radius` | `30px` | Corner radius. |
9
+ | `--glowing-border-surface` | `canvas` | Fill colour behind the glow (the padding-box layer). |
10
+ | `--glowing-border-animation-duration` | `10s` | Time for the glow to complete one full rotation. |
11
+
12
+ ```vue
13
+ <GlowingBorder
14
+ style="--glowing-border-width: 6px; --glowing-border-radius: 8px; --glowing-border-animation-duration: 3s;"
15
+ >
16
+ <p>Fast, sharp-cornered glow</p>
17
+ </GlowingBorder>
18
+ ```
19
+
20
+ ## Variant colour stops
21
+
22
+ Each `variant` drives a 5-stop conic gradient. Every stop is an individually overridable token,
23
+ named `--glowing-border-{variant}-color-{1-5}`:
24
+
25
+ | Variant | Stops (defaults) |
26
+ |---|---|
27
+ | `subtle` (default) | `--glowing-border-subtle-color-1` `#ff9a9e` · `-2` `#fad0c4` · `-3` `#fad0c4` · `-4` `#fbc2eb` · `-5` `#a18cd1` |
28
+ | `vivid` | `--glowing-border-vivid-color-1` `#ff0000` · `-2` `#ffa500` · `-3` `#ffff00` · `-4` `#008000` · `-5` `#0000ff` |
29
+ | `silver` | `--glowing-border-silver-color-1` `#d4d4d4` · `-2` `#e4e4e4` · `-3` `#f5f5f5` · `-4` `#e4e4e4` · `-5` `#d4d4d4` |
30
+ | `steel` | `--glowing-border-steel-color-1` `#434343` · `-2` `#5a5a5a` · `-3` `#6e6e6e` · `-4` `#5a5a5a` · `-5` `#434343` |
31
+ | `green` | `--glowing-border-green-color-1` `#00ff87` · `-2` `#39ff14` · `-3` `#00c853` · `-4` `#64dd17` · `-5` `#00e676` |
32
+
33
+ ```vue
34
+ <GlowingBorder
35
+ variant="vivid"
36
+ style="--glowing-border-vivid-color-1: var(--theme-accent); --glowing-border-vivid-color-5: var(--theme-accent-alt);"
37
+ >
38
+ <p>Brand-coloured glow</p>
39
+ </GlowingBorder>
40
+ ```
41
+
42
+ ## Motion
43
+
44
+ The glow rotates continuously via CSS animation and stops automatically when the visitor has
45
+ `prefers-reduced-motion: reduce` set.
46
+
47
+ ## Class passthrough
48
+
49
+ Use `styleClassPassthrough` to add classes to the root element for layout purposes (positioning,
50
+ sizing) — it doesn't affect the glow's colours or geometry, which stay governed by the tokens
51
+ above.
52
+
53
+ ```vue
54
+ <GlowingBorder style-class-passthrough="my-glowing-border">...</GlowingBorder>
55
+ ```
56
+
57
+ ## Internal (non-overridable) custom properties
58
+
59
+ `--_glow-deg`, `--_clr-1` through `--_clr-5`, and `--_gradient-glow` are internal composition
60
+ plumbing (the resolved per-variant colour stops and the animated rotation angle) — use the public
61
+ tokens above instead.