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