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.
- package/.claude/commands/migrate-component.md +20 -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-range-core.md +142 -0
- package/.claude/skills/components/section-parallax.md +32 -10
- 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 +10 -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-range.code-snippets +85 -0
- package/.vscode/srcdev-component-section-parallax.code-snippets +27 -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/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 +1 -1
- package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +1 -1
- 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/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/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 +96 -11
- 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-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/patterns/stories/MigratedFieldsForm.stories.ts +135 -0
- package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +16 -2
- package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitchCore.vue +6 -4
- package/app/types/components/container-glow.d.ts +8 -0
- package/app/types/components/index.ts +1 -1
- package/package.json +3 -2
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
3
|
import type { DOMWrapper } from "@vue/test-utils";
|
|
4
|
-
import
|
|
4
|
+
import DashboardQuadGrid from "../DashboardQuadGrid.vue";
|
|
5
5
|
|
|
6
6
|
// Mock useStyleClassPassthrough composable
|
|
7
7
|
const mockElementClasses = { value: "" };
|
|
@@ -16,7 +16,7 @@ vi.mock("#imports", () => ({
|
|
|
16
16
|
useStyleClassPassthrough: mockUseStyleClassPassthrough,
|
|
17
17
|
}));
|
|
18
18
|
|
|
19
|
-
describe("
|
|
19
|
+
describe("DashboardQuadGrid", () => {
|
|
20
20
|
let wrapper: ReturnType<typeof mountSuspended>;
|
|
21
21
|
|
|
22
22
|
const createWrapper = async (props = {}, slots = {}) => {
|
|
@@ -25,7 +25,7 @@ describe("LayoutGridA", () => {
|
|
|
25
25
|
...props,
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
wrapper = await mountSuspended(
|
|
28
|
+
wrapper = await mountSuspended(DashboardQuadGrid, {
|
|
29
29
|
props: defaultProps,
|
|
30
30
|
slots,
|
|
31
31
|
});
|
|
@@ -46,14 +46,14 @@ describe("LayoutGridA", () => {
|
|
|
46
46
|
it("mounts without error", async () => {
|
|
47
47
|
await createWrapper();
|
|
48
48
|
|
|
49
|
-
expect(wrapper.find(".
|
|
50
|
-
expect(wrapper.find(".
|
|
49
|
+
expect(wrapper.find(".dashboard-quad-grid-wrapper")).toBeTruthy();
|
|
50
|
+
expect(wrapper.find(".dashboard-quad-grid")).toBeTruthy();
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
it("renders all four slot containers", async () => {
|
|
54
54
|
await createWrapper();
|
|
55
55
|
|
|
56
|
-
const slots = wrapper.findAll(".
|
|
56
|
+
const slots = wrapper.findAll(".dashboard-quad-grid > div");
|
|
57
57
|
expect(slots).toHaveLength(4);
|
|
58
58
|
|
|
59
59
|
expect(wrapper.find(".slot1")).toBeTruthy();
|
|
@@ -65,12 +65,12 @@ describe("LayoutGridA", () => {
|
|
|
65
65
|
it("has correct grid container structure", async () => {
|
|
66
66
|
await createWrapper();
|
|
67
67
|
|
|
68
|
-
const wrapper_element = wrapper.find(".
|
|
69
|
-
const grid = wrapper.find(".
|
|
68
|
+
const wrapper_element = wrapper.find(".dashboard-quad-grid-wrapper");
|
|
69
|
+
const grid = wrapper.find(".dashboard-quad-grid");
|
|
70
70
|
|
|
71
71
|
expect(wrapper_element.exists()).toBe(true);
|
|
72
72
|
expect(grid.exists()).toBe(true);
|
|
73
|
-
expect(grid.classes()).toContain("
|
|
73
|
+
expect(grid.classes()).toContain("dashboard-quad-grid");
|
|
74
74
|
});
|
|
75
75
|
});
|
|
76
76
|
|
|
@@ -156,7 +156,7 @@ describe("LayoutGridA", () => {
|
|
|
156
156
|
}
|
|
157
157
|
);
|
|
158
158
|
|
|
159
|
-
const slots = wrapper.findAll(".
|
|
159
|
+
const slots = wrapper.findAll(".dashboard-quad-grid > div");
|
|
160
160
|
expect(slots).toHaveLength(4);
|
|
161
161
|
|
|
162
162
|
slots.forEach((slot: DOMWrapper<Element>) => {
|
|
@@ -171,7 +171,7 @@ describe("LayoutGridA", () => {
|
|
|
171
171
|
await createWrapper({ styleClassPassthrough: className });
|
|
172
172
|
|
|
173
173
|
// Component should mount without error with string class
|
|
174
|
-
expect(wrapper.find(".
|
|
174
|
+
expect(wrapper.find(".dashboard-quad-grid")).toBeTruthy();
|
|
175
175
|
});
|
|
176
176
|
|
|
177
177
|
it("accepts array styleClassPassthrough", async () => {
|
|
@@ -179,22 +179,22 @@ describe("LayoutGridA", () => {
|
|
|
179
179
|
await createWrapper({ styleClassPassthrough: classNames });
|
|
180
180
|
|
|
181
181
|
// Component should mount without error with array of classes
|
|
182
|
-
expect(wrapper.find(".
|
|
182
|
+
expect(wrapper.find(".dashboard-quad-grid")).toBeTruthy();
|
|
183
183
|
});
|
|
184
184
|
|
|
185
185
|
it("uses default empty array for styleClassPassthrough", async () => {
|
|
186
186
|
await createWrapper();
|
|
187
187
|
|
|
188
188
|
// Component should mount without error with default props
|
|
189
|
-
expect(wrapper.find(".
|
|
189
|
+
expect(wrapper.find(".dashboard-quad-grid")).toBeTruthy();
|
|
190
190
|
});
|
|
191
191
|
|
|
192
192
|
it("renders correctly with various props", async () => {
|
|
193
193
|
await createWrapper({ styleClassPassthrough: ["test-class", "another-class"] });
|
|
194
194
|
|
|
195
|
-
const wrapper_element = wrapper.find(".
|
|
195
|
+
const wrapper_element = wrapper.find(".dashboard-quad-grid-wrapper");
|
|
196
196
|
expect(wrapper_element.exists()).toBe(true);
|
|
197
|
-
expect(wrapper_element.classes()).toContain("
|
|
197
|
+
expect(wrapper_element.classes()).toContain("dashboard-quad-grid-wrapper");
|
|
198
198
|
});
|
|
199
199
|
});
|
|
200
200
|
|
|
@@ -203,9 +203,9 @@ describe("LayoutGridA", () => {
|
|
|
203
203
|
await createWrapper({ styleClassPassthrough: ["initial"] });
|
|
204
204
|
|
|
205
205
|
// Component should maintain its structure
|
|
206
|
-
expect(wrapper.find(".
|
|
207
|
-
expect(wrapper.find(".
|
|
208
|
-
expect(wrapper.findAll(".
|
|
206
|
+
expect(wrapper.find(".dashboard-quad-grid-wrapper")).toBeTruthy();
|
|
207
|
+
expect(wrapper.find(".dashboard-quad-grid")).toBeTruthy();
|
|
208
|
+
expect(wrapper.findAll(".dashboard-quad-grid > div")).toHaveLength(4);
|
|
209
209
|
});
|
|
210
210
|
});
|
|
211
211
|
|
|
@@ -213,15 +213,15 @@ describe("LayoutGridA", () => {
|
|
|
213
213
|
it("has correct CSS classes applied", async () => {
|
|
214
214
|
await createWrapper();
|
|
215
215
|
|
|
216
|
-
const wrapper_element = wrapper.find(".
|
|
217
|
-
const grid = wrapper.find(".
|
|
216
|
+
const wrapper_element = wrapper.find(".dashboard-quad-grid-wrapper");
|
|
217
|
+
const grid = wrapper.find(".dashboard-quad-grid");
|
|
218
218
|
const slot1 = wrapper.find(".slot1");
|
|
219
219
|
const slot2 = wrapper.find(".slot2");
|
|
220
220
|
const slot3 = wrapper.find(".slot3");
|
|
221
221
|
const slot4 = wrapper.find(".slot4");
|
|
222
222
|
|
|
223
|
-
expect(wrapper_element.classes()).toContain("
|
|
224
|
-
expect(grid.classes()).toContain("
|
|
223
|
+
expect(wrapper_element.classes()).toContain("dashboard-quad-grid-wrapper");
|
|
224
|
+
expect(grid.classes()).toContain("dashboard-quad-grid");
|
|
225
225
|
expect(slot1.classes()).toContain("slot1");
|
|
226
226
|
expect(slot2.classes()).toContain("slot2");
|
|
227
227
|
expect(slot3.classes()).toContain("slot3");
|
|
@@ -231,8 +231,8 @@ describe("LayoutGridA", () => {
|
|
|
231
231
|
it("maintains correct DOM hierarchy", async () => {
|
|
232
232
|
await createWrapper();
|
|
233
233
|
|
|
234
|
-
const wrapper_element = wrapper.find(".
|
|
235
|
-
const grid = wrapper_element.find(".
|
|
234
|
+
const wrapper_element = wrapper.find(".dashboard-quad-grid-wrapper");
|
|
235
|
+
const grid = wrapper_element.find(".dashboard-quad-grid");
|
|
236
236
|
|
|
237
237
|
expect(grid.exists()).toBe(true);
|
|
238
238
|
|
|
@@ -341,7 +341,7 @@ describe("LayoutGridA", () => {
|
|
|
341
341
|
}
|
|
342
342
|
);
|
|
343
343
|
|
|
344
|
-
const slots = wrapper.findAll(".
|
|
344
|
+
const slots = wrapper.findAll(".dashboard-quad-grid > div");
|
|
345
345
|
expect(slots).toHaveLength(4);
|
|
346
346
|
|
|
347
347
|
slots.forEach((slot: DOMWrapper<Element>) => {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# DashboardStatsGrid — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
## Public token API
|
|
4
|
+
|
|
5
|
+
| Token | Default | Controls |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `--dashboard-stats-grid-gap` | `1.2rem` | Gap between panels, in every row |
|
|
8
|
+
| `--dashboard-stats-grid-panel-padding` | `1.2rem` | Padding inside each panel |
|
|
9
|
+
| `--dashboard-stats-grid-border-width` | `0.1rem` | Panel border width |
|
|
10
|
+
| `--dashboard-stats-grid-border-colour` | `light-dark(hsl(0, 29%, 3%), hsl(0, 0%, 92%))` | Panel border colour |
|
|
11
|
+
| `--dashboard-stats-grid-border-radius` | `1.2rem` | Panel corner radius |
|
|
12
|
+
| `--dashboard-stats-grid-rule-width` | `0.1rem` | Width of the grid gap rule line (CSS Gap Decorations — see Notes) |
|
|
13
|
+
| `--dashboard-stats-grid-rule-colour` | `transparent` | Colour of the grid gap rule line, invisible by default |
|
|
14
|
+
| `--dashboard-stats-grid-rule-inset` | `0` | Inset of the grid gap rule line from the panel edges |
|
|
15
|
+
|
|
16
|
+
```css
|
|
17
|
+
.my-page {
|
|
18
|
+
--dashboard-stats-grid-gap: 1.6rem;
|
|
19
|
+
--dashboard-stats-grid-border-colour: var(--theme-border);
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or scope to a single instance via `styleClassPassthrough`:
|
|
24
|
+
|
|
25
|
+
```vue
|
|
26
|
+
<DashboardStatsGrid style-class-passthrough="quarterly-stats">...</DashboardStatsGrid>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Notes
|
|
30
|
+
|
|
31
|
+
- Layout shape (top-row split into a multi-panel cluster + 2 static panels, bottom-row panel
|
|
32
|
+
strip) is structural, driven by fixed container-query breakpoints (`680px`, `1024px`) rather
|
|
33
|
+
than tokens/props.
|
|
34
|
+
- `topRowSlot1ItemCount` (default `6`) and `bottomRowItemCount` (default `4`) control how many
|
|
35
|
+
dynamic slots are rendered in the top-row cluster and bottom row, not layout shape.
|
|
36
|
+
- Responds to its own container width (`container-type: inline-size`), not the viewport.
|
|
37
|
+
- Also declares a `rule`/`rule-break`/`rule-inset` gap-decoration line between grid cells (the
|
|
38
|
+
CSS Gap Decorations spec) via `--dashboard-stats-grid-rule-*` tokens, on the top-level grid
|
|
39
|
+
between the top row and bottom row. Browser support is limited as of 2026, so it's transparent
|
|
40
|
+
by default and safe to ignore — set `--dashboard-stats-grid-rule-colour` to opt in where
|
|
41
|
+
supported.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="dashboard-stats-grid" :class="[elementClasses]">
|
|
3
|
+
<section class="top-row">
|
|
4
|
+
<div class="top-row-slot-1">
|
|
5
|
+
<div class="top-row-slot-1-inner">
|
|
6
|
+
<div v-for="key in topRowSlot1ItemCount" :key="key" class="panel">
|
|
7
|
+
<slot :name="`top-row-slot1-${key}-content`"></slot>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="top-row-slot-2">
|
|
13
|
+
<div class="panel">
|
|
14
|
+
<slot name="top-row-slot-2"></slot>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="top-row-slot-3">
|
|
18
|
+
<div class="panel">
|
|
19
|
+
<slot name="top-row-slot-3"></slot>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</section>
|
|
23
|
+
|
|
24
|
+
<section class="bottom-row">
|
|
25
|
+
<div v-for="key in bottomRowItemCount" :key="key" class="panel">
|
|
26
|
+
<slot :name="`bottom-row-${key}-content`"></slot>
|
|
27
|
+
</div>
|
|
28
|
+
</section>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script setup lang="ts">
|
|
33
|
+
interface Props {
|
|
34
|
+
topRowSlot1ItemCount?: number;
|
|
35
|
+
bottomRowItemCount?: number;
|
|
36
|
+
styleClassPassthrough?: string | string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
40
|
+
topRowSlot1ItemCount: 6,
|
|
41
|
+
bottomRowItemCount: 4,
|
|
42
|
+
styleClassPassthrough: () => [],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
46
|
+
|
|
47
|
+
watch(
|
|
48
|
+
() => props.styleClassPassthrough,
|
|
49
|
+
() => {
|
|
50
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<style lang="css">
|
|
56
|
+
@layer components {
|
|
57
|
+
.dashboard-stats-grid {
|
|
58
|
+
.panel {
|
|
59
|
+
border: var(--dashboard-stats-grid-border-width, 0.1rem) solid
|
|
60
|
+
var(--dashboard-stats-grid-border-colour, light-dark(hsl(0, 29%, 3%), hsl(0, 0%, 92%)));
|
|
61
|
+
border-radius: var(--dashboard-stats-grid-border-radius, 1.2rem);
|
|
62
|
+
padding: var(--dashboard-stats-grid-panel-padding, 1.2rem);
|
|
63
|
+
height: auto;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
container-type: inline-size;
|
|
67
|
+
display: grid;
|
|
68
|
+
grid-template-columns: 1fr;
|
|
69
|
+
gap: var(--dashboard-stats-grid-gap, 1.2rem);
|
|
70
|
+
width: 100%;
|
|
71
|
+
margin-inline: auto;
|
|
72
|
+
|
|
73
|
+
/* CSS Gap Decorations (rule/rule-break/rule-inset) — limited browser support as of 2026, transparent by default */
|
|
74
|
+
rule: var(--dashboard-stats-grid-rule-width, 0.1rem) solid var(--dashboard-stats-grid-rule-colour, transparent);
|
|
75
|
+
rule-break: intersection;
|
|
76
|
+
rule-inset: var(--dashboard-stats-grid-rule-inset, 0);
|
|
77
|
+
|
|
78
|
+
.top-row {
|
|
79
|
+
display: grid;
|
|
80
|
+
gap: var(--dashboard-stats-grid-gap, 1.2rem);
|
|
81
|
+
width: 100%;
|
|
82
|
+
|
|
83
|
+
grid-template-columns: 1fr;
|
|
84
|
+
grid-template-areas:
|
|
85
|
+
"slot1"
|
|
86
|
+
"slot2"
|
|
87
|
+
"slot3";
|
|
88
|
+
|
|
89
|
+
@container (min-width: 1024px) {
|
|
90
|
+
grid-template-columns: 1fr minmax(46rem, 33%);
|
|
91
|
+
grid-template-areas:
|
|
92
|
+
"slot1 slot2"
|
|
93
|
+
"slot3 slot2";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.top-row-slot-1 {
|
|
98
|
+
grid-area: slot1;
|
|
99
|
+
container-type: inline-size;
|
|
100
|
+
|
|
101
|
+
.top-row-slot-1-inner {
|
|
102
|
+
display: grid;
|
|
103
|
+
grid-template-columns: repeat(2, 1fr);
|
|
104
|
+
|
|
105
|
+
gap: var(--dashboard-stats-grid-gap, 1.2rem);
|
|
106
|
+
|
|
107
|
+
@container (min-width: 680px) {
|
|
108
|
+
grid-template-columns: repeat(3, 1fr);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.panel {
|
|
112
|
+
display: grid;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.top-row-slot-2 {
|
|
118
|
+
grid-area: slot2;
|
|
119
|
+
display: grid;
|
|
120
|
+
}
|
|
121
|
+
.top-row-slot-3 {
|
|
122
|
+
grid-area: slot3;
|
|
123
|
+
display: grid;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.bottom-row {
|
|
127
|
+
display: grid;
|
|
128
|
+
grid-template-columns: repeat(2, 1fr);
|
|
129
|
+
gap: var(--dashboard-stats-grid-gap, 1.2rem);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
</style>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
-
import
|
|
3
|
-
import type { MediaCanvas } from "
|
|
2
|
+
import DashboardStatsGridComponent from "../DashboardStatsGrid.vue";
|
|
3
|
+
import type { MediaCanvas } from "~/types/components";
|
|
4
4
|
|
|
5
5
|
// Define the args interface
|
|
6
|
-
interface
|
|
6
|
+
interface DashboardStatsGridArgs {
|
|
7
7
|
topRowSlot1ItemCount: number;
|
|
8
8
|
bottomRowItemCount: number;
|
|
9
9
|
mediaCanvas: MediaCanvas;
|
|
@@ -11,8 +11,8 @@ interface LayoutGridBArgs {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export default {
|
|
14
|
-
title: "
|
|
15
|
-
component:
|
|
14
|
+
title: "Atoms/Content Wrappers/Dashboard Stats Grid",
|
|
15
|
+
component: DashboardStatsGridComponent,
|
|
16
16
|
argTypes: {
|
|
17
17
|
// Configuration
|
|
18
18
|
topRowSlot1ItemCount: {
|
|
@@ -59,10 +59,10 @@ export default {
|
|
|
59
59
|
},
|
|
60
60
|
},
|
|
61
61
|
},
|
|
62
|
-
} as Meta<
|
|
62
|
+
} as Meta<DashboardStatsGridArgs>;
|
|
63
63
|
|
|
64
|
-
const Template: StoryFn<
|
|
65
|
-
components: {
|
|
64
|
+
const Template: StoryFn<DashboardStatsGridArgs> = (args) => ({
|
|
65
|
+
components: { DashboardStatsGridComponent },
|
|
66
66
|
setup() {
|
|
67
67
|
const sampleContent = {
|
|
68
68
|
shortText: "Sample content panel",
|
|
@@ -76,9 +76,9 @@ const Template: StoryFn<LayoutGridBArgs> = (args) => ({
|
|
|
76
76
|
},
|
|
77
77
|
template: `
|
|
78
78
|
<div style="padding: 20px;" :class="args.mediaCanvas">
|
|
79
|
-
<
|
|
80
|
-
:
|
|
81
|
-
:
|
|
79
|
+
<DashboardStatsGridComponent
|
|
80
|
+
:top-row-slot1-item-count="args.topRowSlot1ItemCount"
|
|
81
|
+
:bottom-row-item-count="args.bottomRowItemCount"
|
|
82
82
|
:style-class-passthrough="[args.mediaCanvas, ...args.styleClassPassthrough]"
|
|
83
83
|
>
|
|
84
84
|
<!-- Dynamic top row slot 1 content -->
|
|
@@ -113,7 +113,7 @@ const Template: StoryFn<LayoutGridBArgs> = (args) => ({
|
|
|
113
113
|
<p style="margin: 0; color: #b45309; font-size: 14px; line-height: 1.4;">{{ sampleContent.mediumText }}</p>
|
|
114
114
|
</div>
|
|
115
115
|
</template>
|
|
116
|
-
</
|
|
116
|
+
</DashboardStatsGridComponent>
|
|
117
117
|
</div>
|
|
118
118
|
`,
|
|
119
119
|
});
|
|
@@ -209,16 +209,16 @@ FullWidthView.parameters = {
|
|
|
209
209
|
|
|
210
210
|
// ===== CONTENT VARIATION STORIES =====
|
|
211
211
|
|
|
212
|
-
const ContentVariationsTemplate: StoryFn<
|
|
213
|
-
components: {
|
|
212
|
+
const ContentVariationsTemplate: StoryFn<DashboardStatsGridArgs> = (args) => ({
|
|
213
|
+
components: { DashboardStatsGridComponent },
|
|
214
214
|
setup() {
|
|
215
215
|
return { args };
|
|
216
216
|
},
|
|
217
217
|
template: `
|
|
218
218
|
<div style="padding: 20px;" :class="args.mediaCanvas">
|
|
219
|
-
<
|
|
220
|
-
:
|
|
221
|
-
:
|
|
219
|
+
<DashboardStatsGridComponent
|
|
220
|
+
:top-row-slot1-item-count="args.topRowSlot1ItemCount"
|
|
221
|
+
:bottom-row-item-count="args.bottomRowItemCount"
|
|
222
222
|
:style-class-passthrough="[args.mediaCanvas]"
|
|
223
223
|
>
|
|
224
224
|
<!-- Top row slot 1 - Different content types -->
|
|
@@ -335,7 +335,7 @@ const ContentVariationsTemplate: StoryFn<LayoutGridBArgs> = (args) => ({
|
|
|
335
335
|
<button style="padding: 8px 16px; background: #7c3aed; color: white; border: none; border-radius: 6px; font-size: 14px; cursor: pointer;">Upgrade</button>
|
|
336
336
|
</div>
|
|
337
337
|
</template>
|
|
338
|
-
</
|
|
338
|
+
</DashboardStatsGridComponent>
|
|
339
339
|
</div>
|
|
340
340
|
`,
|
|
341
341
|
});
|
|
@@ -350,7 +350,7 @@ DashboardExample.parameters = {
|
|
|
350
350
|
docs: {
|
|
351
351
|
description: {
|
|
352
352
|
story:
|
|
353
|
-
"Complete dashboard example showing how
|
|
353
|
+
"Complete dashboard example showing how DashboardStatsGrid can be used for admin interfaces with statistics, main content, activity feeds, and feature cards.",
|
|
354
354
|
},
|
|
355
355
|
},
|
|
356
356
|
};
|
|
@@ -358,7 +358,7 @@ DashboardExample.parameters = {
|
|
|
358
358
|
// ===== COMPARISON STORY =====
|
|
359
359
|
|
|
360
360
|
const ComparisonTemplate: StoryFn = () => ({
|
|
361
|
-
components: {
|
|
361
|
+
components: { DashboardStatsGridComponent },
|
|
362
362
|
setup() {
|
|
363
363
|
const canvases: MediaCanvas[] = ["mobileCanvas", "tabletCanvas", "laptopCanvas", "desktopCanvas"];
|
|
364
364
|
return { canvases };
|
|
@@ -372,9 +372,9 @@ const ComparisonTemplate: StoryFn = () => ({
|
|
|
372
372
|
{{ canvas.replace('Canvas', '') }} View
|
|
373
373
|
</h3>
|
|
374
374
|
<div style="border: 2px solid #e5e7eb; border-radius: 12px; padding: 20px; background: #fafafa;">
|
|
375
|
-
<
|
|
376
|
-
:
|
|
377
|
-
:
|
|
375
|
+
<DashboardStatsGridComponent
|
|
376
|
+
:top-row-slot1-item-count="6"
|
|
377
|
+
:bottom-row-item-count="4"
|
|
378
378
|
:style-class-passthrough="[canvas]"
|
|
379
379
|
>
|
|
380
380
|
<!-- Simplified content for comparison -->
|
|
@@ -401,7 +401,7 @@ const ComparisonTemplate: StoryFn = () => ({
|
|
|
401
401
|
Bottom {{ i }}
|
|
402
402
|
</div>
|
|
403
403
|
</template>
|
|
404
|
-
</
|
|
404
|
+
</DashboardStatsGridComponent>
|
|
405
405
|
</div>
|
|
406
406
|
</div>
|
|
407
407
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
-
import
|
|
3
|
+
import DashboardStatsGrid from "../DashboardStatsGrid.vue";
|
|
4
4
|
|
|
5
5
|
// Mock useStyleClassPassthrough composable
|
|
6
6
|
const mockElementClasses = { value: "" };
|
|
@@ -15,7 +15,7 @@ vi.mock("#imports", () => ({
|
|
|
15
15
|
useStyleClassPassthrough: mockUseStyleClassPassthrough,
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
|
-
describe("
|
|
18
|
+
describe("DashboardStatsGrid", () => {
|
|
19
19
|
let wrapper: ReturnType<typeof mountSuspended>;
|
|
20
20
|
|
|
21
21
|
const createWrapper = async (props = {}, slots = {}) => {
|
|
@@ -26,7 +26,7 @@ describe("LayoutGridB", () => {
|
|
|
26
26
|
...props,
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
wrapper = await mountSuspended(
|
|
29
|
+
wrapper = await mountSuspended(DashboardStatsGrid, {
|
|
30
30
|
props: defaultProps,
|
|
31
31
|
slots,
|
|
32
32
|
});
|
|
@@ -47,8 +47,8 @@ describe("LayoutGridB", () => {
|
|
|
47
47
|
it("mounts without error", async () => {
|
|
48
48
|
await createWrapper();
|
|
49
49
|
|
|
50
|
-
expect(wrapper.find(".
|
|
51
|
-
expect(wrapper.find(".
|
|
50
|
+
expect(wrapper.find(".dashboard-stats-grid-wrapper")).toBeTruthy();
|
|
51
|
+
expect(wrapper.find(".dashboard-stats-grid")).toBeTruthy();
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
it("renders top row structure correctly", async () => {
|
|
@@ -69,10 +69,10 @@ describe("LayoutGridB", () => {
|
|
|
69
69
|
it("has correct grid container structure", async () => {
|
|
70
70
|
await createWrapper();
|
|
71
71
|
|
|
72
|
-
const grid = wrapper.find(".
|
|
72
|
+
const grid = wrapper.find(".dashboard-stats-grid");
|
|
73
73
|
|
|
74
74
|
expect(grid.exists()).toBe(true);
|
|
75
|
-
expect(grid.classes()).toContain("
|
|
75
|
+
expect(grid.classes()).toContain("dashboard-stats-grid");
|
|
76
76
|
});
|
|
77
77
|
});
|
|
78
78
|
|
|
@@ -82,7 +82,7 @@ describe("LayoutGridB", () => {
|
|
|
82
82
|
await createWrapper({ topRowSlot1ItemCount: itemCount });
|
|
83
83
|
|
|
84
84
|
// Component should render without error with custom item count
|
|
85
|
-
expect(wrapper.find(".
|
|
85
|
+
expect(wrapper.find(".dashboard-stats-grid")).toBeTruthy();
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
it("accepts bottomRowItemCount prop", async () => {
|
|
@@ -90,7 +90,7 @@ describe("LayoutGridB", () => {
|
|
|
90
90
|
await createWrapper({ bottomRowItemCount: itemCount });
|
|
91
91
|
|
|
92
92
|
// Component should render without error with custom item count
|
|
93
|
-
expect(wrapper.find(".
|
|
93
|
+
expect(wrapper.find(".dashboard-stats-grid")).toBeTruthy();
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
it("uses default props when not provided", async () => {
|
|
@@ -100,7 +100,7 @@ describe("LayoutGridB", () => {
|
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
// Should use default values (6 and 4)
|
|
103
|
-
expect(wrapper.find(".
|
|
103
|
+
expect(wrapper.find(".dashboard-stats-grid")).toBeTruthy();
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
it("accepts string styleClassPassthrough", async () => {
|
|
@@ -108,7 +108,7 @@ describe("LayoutGridB", () => {
|
|
|
108
108
|
await createWrapper({ styleClassPassthrough: className });
|
|
109
109
|
|
|
110
110
|
// Component should mount without error with string class
|
|
111
|
-
expect(wrapper.find(".
|
|
111
|
+
expect(wrapper.find(".dashboard-stats-grid")).toBeTruthy();
|
|
112
112
|
});
|
|
113
113
|
|
|
114
114
|
it("accepts array styleClassPassthrough", async () => {
|
|
@@ -116,7 +116,7 @@ describe("LayoutGridB", () => {
|
|
|
116
116
|
await createWrapper({ styleClassPassthrough: classNames });
|
|
117
117
|
|
|
118
118
|
// Component should mount without error with array of classes
|
|
119
|
-
expect(wrapper.find(".
|
|
119
|
+
expect(wrapper.find(".dashboard-stats-grid")).toBeTruthy();
|
|
120
120
|
});
|
|
121
121
|
|
|
122
122
|
it("applies element classes to wrapper", async () => {
|
|
@@ -125,7 +125,7 @@ describe("LayoutGridB", () => {
|
|
|
125
125
|
await createWrapper();
|
|
126
126
|
|
|
127
127
|
// Check that the component renders
|
|
128
|
-
expect(wrapper.find(".
|
|
128
|
+
expect(wrapper.find(".dashboard-stats-grid")).toBeTruthy();
|
|
129
129
|
});
|
|
130
130
|
});
|
|
131
131
|
|
|
@@ -157,7 +157,7 @@ describe("LayoutGridB", () => {
|
|
|
157
157
|
const { unmount } = await createWrapper({ topRowSlot1ItemCount: count }, slots);
|
|
158
158
|
|
|
159
159
|
// Should render without error
|
|
160
|
-
expect(wrapper.find(".
|
|
160
|
+
expect(wrapper.find(".dashboard-stats-grid")).toBeTruthy();
|
|
161
161
|
|
|
162
162
|
// Check that content is rendered
|
|
163
163
|
for (let i = 1; i <= count; i++) {
|
|
@@ -239,7 +239,7 @@ describe("LayoutGridB", () => {
|
|
|
239
239
|
const { unmount } = await createWrapper({ bottomRowItemCount: count }, slots);
|
|
240
240
|
|
|
241
241
|
// Should render without error
|
|
242
|
-
expect(wrapper.find(".
|
|
242
|
+
expect(wrapper.find(".dashboard-stats-grid")).toBeTruthy();
|
|
243
243
|
|
|
244
244
|
// Check that content is rendered
|
|
245
245
|
for (let i = 1; i <= count; i++) {
|
|
@@ -294,7 +294,7 @@ describe("LayoutGridB", () => {
|
|
|
294
294
|
await createWrapper({ styleClassPassthrough: ["initial"] });
|
|
295
295
|
|
|
296
296
|
// Component should maintain structure with different props
|
|
297
|
-
expect(wrapper.find(".
|
|
297
|
+
expect(wrapper.find(".dashboard-stats-grid")).toBeTruthy();
|
|
298
298
|
expect(wrapper.find(".top-row")).toBeTruthy();
|
|
299
299
|
expect(wrapper.find(".bottom-row")).toBeTruthy();
|
|
300
300
|
});
|
|
@@ -315,7 +315,7 @@ describe("LayoutGridB", () => {
|
|
|
315
315
|
expect(wrapper.html()).toContain("Bottom 1");
|
|
316
316
|
|
|
317
317
|
// Component should render properly
|
|
318
|
-
expect(wrapper.find(".
|
|
318
|
+
expect(wrapper.find(".dashboard-stats-grid")).toBeTruthy();
|
|
319
319
|
|
|
320
320
|
unmount();
|
|
321
321
|
});
|
|
@@ -325,11 +325,11 @@ describe("LayoutGridB", () => {
|
|
|
325
325
|
it("has correct CSS classes applied", async () => {
|
|
326
326
|
await createWrapper();
|
|
327
327
|
|
|
328
|
-
const grid = wrapper.find(".
|
|
328
|
+
const grid = wrapper.find(".dashboard-stats-grid");
|
|
329
329
|
const topRow = wrapper.find(".top-row");
|
|
330
330
|
const bottomRow = wrapper.find(".bottom-row");
|
|
331
331
|
|
|
332
|
-
expect(grid.classes()).toContain("
|
|
332
|
+
expect(grid.classes()).toContain("dashboard-stats-grid");
|
|
333
333
|
expect(topRow.classes()).toContain("top-row");
|
|
334
334
|
expect(bottomRow.classes()).toContain("bottom-row");
|
|
335
335
|
});
|
|
@@ -337,7 +337,7 @@ describe("LayoutGridB", () => {
|
|
|
337
337
|
it("maintains correct DOM hierarchy", async () => {
|
|
338
338
|
await createWrapper();
|
|
339
339
|
|
|
340
|
-
const grid = wrapper.find(".
|
|
340
|
+
const grid = wrapper.find(".dashboard-stats-grid");
|
|
341
341
|
const topRow = grid.find(".top-row");
|
|
342
342
|
const bottomRow = grid.find(".bottom-row");
|
|
343
343
|
|
|
@@ -359,7 +359,7 @@ describe("LayoutGridB", () => {
|
|
|
359
359
|
bottomRowItemCount: 0,
|
|
360
360
|
});
|
|
361
361
|
|
|
362
|
-
expect(wrapper.find(".
|
|
362
|
+
expect(wrapper.find(".dashboard-stats-grid")).toBeTruthy();
|
|
363
363
|
expect(wrapper.find(".top-row")).toBeTruthy();
|
|
364
364
|
expect(wrapper.find(".bottom-row")).toBeTruthy();
|
|
365
365
|
});
|
|
@@ -382,7 +382,7 @@ describe("LayoutGridB", () => {
|
|
|
382
382
|
slots
|
|
383
383
|
);
|
|
384
384
|
|
|
385
|
-
expect(wrapper.find(".
|
|
385
|
+
expect(wrapper.find(".dashboard-stats-grid")).toBeTruthy();
|
|
386
386
|
expect(wrapper.html()).toContain("Large Panel 1");
|
|
387
387
|
expect(wrapper.html()).toContain("Large Bottom 1");
|
|
388
388
|
});
|
|
@@ -436,7 +436,7 @@ describe("LayoutGridB", () => {
|
|
|
436
436
|
}
|
|
437
437
|
);
|
|
438
438
|
|
|
439
|
-
expect(wrapper.find(".
|
|
439
|
+
expect(wrapper.find(".dashboard-stats-grid")).toBeTruthy();
|
|
440
440
|
expect(wrapper.find(".top-row")).toBeTruthy();
|
|
441
441
|
expect(wrapper.find(".bottom-row")).toBeTruthy();
|
|
442
442
|
});
|