srcdev-nuxt-components 9.4.9 → 9.4.11
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.
- package/.claude/commands/migrate-component.md +38 -6
- package/.claude/component-ledger/audit.json +1 -1
- package/.claude/component-ledger/build.mjs +26 -0
- package/.claude/component-ledger/output.html +8 -3
- package/.claude/component-ledger/template.html +7 -2
- package/.claude/skills/component-dynamic-slots.md +1 -1
- package/.claude/skills/components/alert-mask-core.md +84 -0
- package/.claude/skills/components/canvas-switcher.md +1 -1
- package/.claude/skills/components/carousel-flip.md +112 -168
- package/.claude/skills/components/container-glow.md +123 -0
- package/.claude/skills/components/dashboard-quad-grid.md +98 -0
- package/.claude/skills/components/dashboard-stats-grid.md +103 -0
- package/.claude/skills/components/display-theme-switch.md +60 -0
- package/.claude/skills/components/glowing-border.md +66 -0
- package/.claude/skills/components/input-number-core.md +214 -0
- package/.claude/skills/components/input-range-core.md +142 -0
- package/.claude/skills/components/input-select-core.md +151 -0
- package/.claude/skills/components/input-text-core.md +16 -0
- package/.claude/skills/components/input-textarea-core.md +154 -0
- package/.claude/skills/components/section-parallax.md +32 -10
- package/.claude/skills/components/select-menu.md +195 -0
- package/.claude/skills/components/toggle-switch-core.md +150 -0
- package/.claude/skills/components/ui-block-decorated.md +56 -0
- package/.claude/skills/components/wipe-away-vertical.md +98 -0
- package/.claude/skills/icon-sets.md +1 -1
- package/.claude/skills/index.md +15 -1
- package/.vscode/css-custom-data.json +38 -0
- package/.vscode/settings.json +3 -0
- package/.vscode/srcdev-component-alert-mask-core.code-snippets +31 -0
- package/.vscode/srcdev-component-carousel-flip.code-snippets +41 -0
- package/.vscode/srcdev-component-container-glow.code-snippets +33 -0
- package/.vscode/srcdev-component-dashboard-quad-grid.code-snippets +36 -0
- package/.vscode/srcdev-component-dashboard-stats-grid.code-snippets +35 -0
- package/.vscode/srcdev-component-display-theme-switch.code-snippets +27 -0
- package/.vscode/srcdev-component-glowing-border.code-snippets +23 -0
- package/.vscode/srcdev-component-input-number.code-snippets +65 -0
- package/.vscode/srcdev-component-input-range.code-snippets +85 -0
- package/.vscode/srcdev-component-input-select.code-snippets +42 -0
- package/.vscode/srcdev-component-input-textarea.code-snippets +40 -0
- package/.vscode/srcdev-component-section-parallax.code-snippets +27 -0
- package/.vscode/srcdev-component-select-menu.code-snippets +65 -0
- package/.vscode/srcdev-component-toggle-switch.code-snippets +49 -0
- package/.vscode/srcdev-component-ui-block-decorated.code-snippets +24 -0
- package/.vscode/srcdev-component-wipe-away-vertical.code-snippets +25 -0
- package/README.md +18 -37
- package/app/components/{alert-mask → 01.atoms/alert-mask}/AlertMaskCore.vue +16 -17
- package/app/components/01.atoms/alert-mask/CONSUMER-STYLING.md +46 -0
- package/app/components/01.atoms/alert-mask/stories/AlertMaskCore.stories.ts +79 -0
- package/app/components/{alert-mask → 01.atoms/alert-mask}/tests/AlertMaskCore.spec.ts +10 -10
- package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +86 -0
- package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +299 -0
- package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +175 -0
- package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +131 -0
- package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +14 -0
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +61 -0
- package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +132 -0
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +165 -0
- package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +94 -0
- package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +3 -0
- package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +1 -1
- package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +1 -1
- package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +1 -1
- package/app/components/01.atoms/animations/section-parallax/CONSUMER-STYLING.md +44 -0
- package/app/components/{parallax → 01.atoms/animations/section-parallax}/SectionParallax.vue +11 -10
- package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts +98 -0
- package/app/components/01.atoms/animations/section-parallax/tests/SectionParallax.spec.ts +102 -0
- package/app/components/01.atoms/animations/section-parallax/tests/__snapshots__/SectionParallax.spec.ts.snap +9 -0
- package/app/components/01.atoms/animations/view-timeline/CONSUMER-STYLING.md +70 -0
- package/app/components/01.atoms/animations/view-timeline/WipeAwayVertical.vue +181 -0
- package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts +269 -0
- package/app/components/01.atoms/animations/view-timeline/tests/WipeAwayVertical.spec.ts +127 -0
- package/app/components/01.atoms/canvas-switcher/CONSUMER-STYLING.md +3 -3
- package/app/components/01.atoms/canvas-switcher/CanvasSwitcher.vue +2 -2
- package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +1 -1
- package/app/components/01.atoms/card/CardCore.vue +4 -4
- package/app/components/01.atoms/clipped-panel/CONSUMER-STYLING.md +2 -2
- package/app/components/01.atoms/clipped-panel/ClippedPanel.vue +2 -2
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +42 -0
- package/app/components/{layout-grids/LayoutGridA.vue → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue} +23 -13
- package/app/components/{layout-grids/stories/LayoutGridA.stories.ts → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts} +151 -23
- package/app/components/{layout-grids/tests/LayoutGridA.spec.ts → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/tests/DashboardQuadGrid.spec.ts} +25 -25
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +41 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +133 -0
- package/app/components/{layout-grids/stories/LayoutGridB.stories.ts → 01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/stories/DashboardStatsGrid.stories.ts} +24 -24
- package/app/components/{layout-grids/tests/LayoutGridB.spec.ts → 01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/tests/DashboardStatsGrid.spec.ts} +23 -23
- package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +8 -8
- package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +8 -8
- package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +6 -6
- package/app/components/01.atoms/pop-over/PopOver.vue +6 -6
- package/app/components/01.atoms/ui-block-decorated/CONSUMER-STYLING.md +39 -0
- package/app/components/01.atoms/ui-block-decorated/UiBlockDecorated.vue +141 -0
- package/app/components/01.atoms/ui-block-decorated/stories/UiBlockDecorated.stories.ts +148 -0
- package/app/components/01.atoms/ui-block-decorated/tests/UiBlockDecorated.spec.ts +80 -0
- package/app/components/01.atoms/ui-block-decorated/tests/__snapshots__/UiBlockDecorated.spec.ts.snap +3 -0
- package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +6 -6
- package/app/components/02.molecules/display-theme-switch/CONSUMER-STYLING.md +34 -0
- package/app/components/02.molecules/display-theme-switch/DisplayThemeSwitch.vue +102 -0
- package/app/components/02.molecules/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +115 -0
- package/app/components/02.molecules/display-theme-switch/tests/DisplayThemeSwitch.spec.ts +86 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +1 -1
- package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +1 -1
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +1 -1
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +1 -1
- package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +168 -0
- package/app/components/02.molecules/select-menu/SelectMenu.vue +384 -0
- package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +359 -0
- package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +476 -0
- package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +42 -0
- package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +46 -0
- package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/CarouselFlip.vue +63 -42
- package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/stories/CarouselFlip.stories.ts +114 -29
- package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/tests/CarouselFlip.spec.ts +80 -0
- package/app/components/05.forms/form-field/FormField.vue +1 -1
- package/app/components/05.forms/form-wrapper/FormWrapper.vue +1 -1
- package/app/components/05.forms/input-button/CONSUMER-STYLING.md +2 -7
- package/app/components/05.forms/input-button/InputButtonCore.vue +1 -5
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +26 -6
- package/app/components/05.forms/input-number/InputNumberCore.vue +125 -85
- package/app/components/05.forms/input-number/stories/InputNumberCore.stories.ts +92 -0
- package/app/components/05.forms/input-number/tests/InputNumberCore.spec.ts +119 -0
- package/app/components/05.forms/input-number/variants/InputNumberDefault.vue +30 -22
- package/app/components/05.forms/input-number/variants/stories/InputNumberDefault.stories.ts +123 -0
- package/app/components/05.forms/input-number/variants/tests/InputNumberDefault.spec.ts +143 -0
- package/app/components/05.forms/input-range/CONSUMER-STYLING.md +45 -0
- package/app/components/05.forms/input-range/InputRangeCore.vue +79 -143
- package/app/components/05.forms/input-range/stories/InputRangeCore.stories.ts +126 -0
- package/app/components/05.forms/input-range/tests/InputRangeCore.spec.ts +124 -0
- package/app/components/05.forms/input-range/variants/InputRangeDefault.vue +9 -32
- package/app/components/05.forms/input-range/variants/stories/InputRangeDefault.stories.ts +108 -0
- package/app/components/05.forms/input-range/variants/tests/InputRangeDefault.spec.ts +140 -0
- package/app/components/05.forms/input-select/InputSelectCore.vue +23 -30
- package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +2 -2
- package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +115 -0
- package/app/components/05.forms/input-select/tests/InputSelectCore.spec.ts +129 -0
- package/app/components/05.forms/input-select/variants/InputSelectWithLabel.vue +1 -1
- package/app/components/05.forms/input-select/variants/tests/InputSelectWithLabel.spec.ts +87 -0
- package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +14 -0
- package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +1 -1
- package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +55 -0
- package/app/components/05.forms/input-textarea/InputTextareaCore.vue +24 -16
- package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +1 -1
- package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +119 -0
- package/app/components/05.forms/input-textarea/variants/InputTextareaWithLabel.vue +3 -13
- package/app/components/05.forms/input-textarea/variants/tests/InputTextareaWithLabel.spec.ts +93 -0
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +220 -0
- package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +29 -0
- package/app/components/05.forms/toggle-switch/ToggleSwitchCore.vue +120 -119
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +1 -1
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +1 -1
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +2 -2
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +101 -0
- package/app/components/05.forms/toggle-switch/variants/ToggleSwitchWithLabel.vue +3 -5
- package/app/components/05.forms/toggle-switch/variants/ToggleSwitchWithLabelInline.vue +1 -2
- package/app/components/05.forms/toggle-switch/variants/tests/ToggleSwitchWithLabel.spec.ts +77 -0
- package/app/components/05.forms/toggle-switch/variants/tests/ToggleSwitchWithLabelInline.spec.ts +66 -0
- package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +20 -6
- package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitchCore.vue +12 -10
- package/app/types/components/container-glow.d.ts +8 -0
- package/app/types/components/index.ts +2 -1
- package/app/types/components/select-menu.d.ts +5 -0
- package/package.json +3 -2
- package/.vscode/srcdev-component-toggle-switch-core.code-snippets +0 -13
- package/app/components/05.forms/input-range-fancy/InputRangeFancyCore.vue +0 -426
- package/app/components/05.forms/input-range-fancy/InputRangeFancyWithLabel.vue +0 -94
- package/app/components/carousels/CarouselBasic.vue +0 -317
- package/app/components/carousels/CarouselInfinite.vue +0 -358
- package/app/components/container-glow/ContainerGlowCore.vue +0 -296
- package/app/components/container-glow/stories/ContainerGlowCore.stories.ts +0 -337
- package/app/components/display-theme-switch/DisplayThemeSwitch.vue +0 -189
- package/app/components/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +0 -154
- package/app/components/glowing-border/GlowingBorder.vue +0 -142
- package/app/components/glowing-border/stories/GlowingBorder.stories.ts +0 -141
- package/app/components/layout-grids/LayoutGridB.vue +0 -134
- package/app/components/test-storybook/TestStorybook.vue +0 -49
- package/app/components/test-storybook/stories/TestStorybook.stories.ts +0 -28
- package/app/components/ui-block-decorated/UiBlockDecorated.vue +0 -167
- package/app/components/view-timeline/WipeAwayVertical.vue +0 -180
- 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, white);
|
|
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.
|