srcdev-nuxt-components 9.6.18 → 9.6.20
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/rules/consumer/css-relative-colour-calc.md +15 -0
- package/.claude/rules/consumer/ids-from-useid.md +3 -2
- package/.claude/rules/consumer/vue-dynamic-arg-no-spaces.md +16 -0
- package/.claude/skills/component-compliance-checklist.md +11 -1
- package/.claude/skills/component-dynamic-slots.md +2 -2
- package/.claude/skills/component-naming.md +1 -1
- package/.claude/skills/components/accordion-chat.md +99 -0
- package/.claude/skills/components/accordion-group.md +98 -0
- package/.claude/skills/components/action-menu.md +11 -5
- package/.claude/skills/components/auto-grid.md +3 -1
- package/.claude/skills/components/capture-qr-code.md +9 -5
- package/.claude/skills/components/carousel-flip.md +41 -30
- package/.claude/skills/components/column-flow-grid.md +2 -0
- package/.claude/skills/components/dashboard-quad-grid.md +7 -2
- package/.claude/skills/components/dashboard-stats-grid.md +4 -0
- package/.claude/skills/components/decode-qr-code.md +7 -2
- package/.claude/skills/components/deep-expanding-menu-classic.md +31 -30
- package/.claude/skills/components/deep-expanding-menu.md +22 -21
- package/.claude/skills/components/expanding-panel-classic.md +9 -7
- package/.claude/skills/components/expanding-panel.md +9 -8
- package/.claude/skills/components/grid-stack.md +2 -0
- package/.claude/skills/components/layout-grid-by-cols.md +2 -0
- package/.claude/skills/components/masonry-grid.md +9 -1
- package/.claude/skills/components/page-row.md +3 -0
- package/.claude/skills/components/samaritan-prompt.md +24 -8
- package/.claude/skills/components/site-header.md +16 -2
- package/.claude/skills/components/sparkle-effect.md +79 -0
- package/.claude/skills/index.md +16 -14
- package/.claude/skills/storybook-add-story.md +33 -0
- package/.claude/skills/testing-add-unit-test.md +26 -0
- package/.claude/skills/using-component-skills.md +1 -1
- package/.vscode/srcdev-component-accordion-chat.code-snippets +42 -0
- package/.vscode/srcdev-component-accordion-group.code-snippets +38 -0
- package/.vscode/srcdev-component-action-menu.code-snippets +3 -3
- package/.vscode/srcdev-component-capture-decode-qr-code.code-snippets +11 -2
- package/.vscode/srcdev-component-carousel-flip.code-snippets +22 -4
- package/.vscode/srcdev-component-deep-expanding-menu-classic.code-snippets +32 -2
- package/.vscode/srcdev-component-deep-expanding-menu.code-snippets +31 -1
- package/.vscode/srcdev-component-samaritan-prompt.code-snippets +5 -4
- package/.vscode/srcdev-component-site-header.code-snippets +13 -1
- package/.vscode/srcdev-component-sparkle-effect.code-snippets +33 -0
- package/app/components/01.atoms/animations/sparkle-effect/CONSUMER-STYLING.md +75 -0
- package/app/components/01.atoms/animations/sparkle-effect/SparkleEffect.vue +213 -0
- package/app/components/01.atoms/animations/sparkle-effect/stories/SparkleEffect.stories.ts +303 -0
- package/app/components/01.atoms/animations/sparkle-effect/tests/SparkleEffect.spec.ts +150 -0
- package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +1 -1
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +12 -2
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue +19 -17
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts +42 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/tests/DashboardQuadGrid.spec.ts +31 -44
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +13 -1
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +40 -31
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/stories/DashboardStatsGrid.stories.ts +47 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/tests/DashboardStatsGrid.spec.ts +51 -53
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/CONSUMER-STYLING.md +3 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/LayoutGridByCols.vue +4 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/stories/LayoutGridByCols.stories.ts +47 -2
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/LayoutGridByCols.spec.ts +36 -23
- package/app/components/01.atoms/grids/auto-grid/AutoGrid.vue +7 -3
- package/app/components/01.atoms/grids/auto-grid/CONSUMER-STYLING.md +1 -1
- package/app/components/01.atoms/grids/auto-grid/stories/AutoGrid.stories.ts +46 -1
- package/app/components/01.atoms/grids/auto-grid/tests/AutoGrid.spec.ts +40 -27
- package/app/components/01.atoms/grids/auto-grid/tests/__snapshots__/AutoGrid.spec.ts.snap +1 -1
- package/app/components/01.atoms/grids/column-flow-grid/CONSUMER-STYLING.md +9 -0
- package/app/components/01.atoms/grids/column-flow-grid/ColumnFlowGrid.vue +3 -2
- package/app/components/01.atoms/grids/column-flow-grid/stories/ColumnFlowGrid.stories.ts +46 -0
- package/app/components/01.atoms/grids/column-flow-grid/tests/ColumnFlowGrid.spec.ts +24 -11
- package/app/components/01.atoms/grids/grid-stack/CONSUMER-STYLING.md +3 -0
- package/app/components/01.atoms/grids/grid-stack/GridStack.vue +2 -0
- package/app/components/01.atoms/grids/grid-stack/stories/GridStack.stories.ts +41 -0
- package/app/components/01.atoms/grids/grid-stack/tests/GridStack.spec.ts +26 -13
- package/app/components/01.atoms/grids/masonry-grid/CONSUMER-STYLING.md +15 -0
- package/app/components/01.atoms/grids/masonry-grid/MasonryGrid.vue +51 -31
- package/app/components/01.atoms/grids/masonry-grid/stories/MasonryGrid.stories.ts +48 -0
- package/app/components/01.atoms/grids/masonry-grid/tests/MasonryGrid.spec.ts +84 -29
- package/app/components/01.atoms/page-row/CONSUMER-STYLING.md +6 -1
- package/app/components/01.atoms/page-row/PageRow.vue +16 -15
- package/app/components/01.atoms/page-row/stories/PageRow.stories.ts +23 -0
- package/app/components/01.atoms/page-row/tests/PageRow.spec.ts +6 -5
- package/app/components/02.molecules/action-menu/ActionMenu.vue +6 -2
- package/app/components/02.molecules/action-menu/ActionMenuItem.vue +6 -3
- package/app/components/02.molecules/action-menu/CONSUMER-STYLING.md +11 -4
- package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +130 -18
- package/app/components/02.molecules/expandable/accordion-chat/AccordionChat.vue +227 -0
- package/app/components/02.molecules/expandable/accordion-chat/CONSUMER-STYLING.md +150 -0
- package/app/components/02.molecules/expandable/accordion-chat/stories/AccordionChat.stories.ts +347 -0
- package/app/components/02.molecules/expandable/accordion-chat/tests/AccordionChat.spec.ts +205 -0
- package/app/components/02.molecules/expandable/accordion-chat/tests/__snapshots__/AccordionChat.spec.ts.snap +40 -0
- package/app/components/02.molecules/expandable/{accordian/AccordianCore.vue → accordion-group/AccordionGroup.vue} +32 -20
- package/app/components/02.molecules/expandable/accordion-group/CONSUMER-STYLING.md +66 -0
- package/app/components/02.molecules/expandable/accordion-group/stories/AccordionGroup.stories.ts +238 -0
- package/app/components/02.molecules/expandable/accordion-group/tests/AccordionGroup.spec.ts +172 -0
- package/app/components/02.molecules/expandable/accordion-group/tests/__snapshots__/AccordionGroup.spec.ts.snap +45 -0
- package/app/components/02.molecules/expandable/expanding-panel/CONSUMER-STYLING.md +1 -0
- package/app/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue +11 -6
- package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts +82 -1
- package/app/components/02.molecules/expandable/expanding-panel/tests/ExpandingPanel.spec.ts +24 -30
- package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +8 -8
- package/app/components/02.molecules/expandable/expanding-panel-classic/CONSUMER-STYLING.md +1 -0
- package/app/components/02.molecules/expandable/expanding-panel-classic/ExpandingPanelClassic.vue +10 -5
- package/app/components/02.molecules/expandable/expanding-panel-classic/stories/ExpandingPanelClassic.stories.ts +83 -1
- package/app/components/02.molecules/expandable/expanding-panel-classic/tests/ExpandingPanelClassic.spec.ts +24 -30
- package/app/components/02.molecules/expandable/expanding-panel-classic/tests/__snapshots__/ExpandingPanelClassic.spec.ts.snap +8 -8
- package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +63 -44
- package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +74 -37
- package/app/components/02.molecules/navigation/deep-expanding-menu/stories/DeepExpandingMenu.stories.ts +139 -19
- package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts +90 -27
- package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +8 -8
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +58 -26
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +113 -57
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/stories/DeepExpandingMenuClassic.stories.ts +139 -19
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/DeepExpandingMenuClassic.spec.ts +140 -22
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/__snapshots__/DeepExpandingMenuClassic.spec.ts.snap +9 -9
- package/app/components/02.molecules/qr-code/CONSUMER-STYLING.md +22 -7
- package/app/components/02.molecules/qr-code/CaptureQrCode.vue +63 -71
- package/app/components/02.molecules/qr-code/DecodeQrCode.vue +47 -10
- package/app/components/02.molecules/qr-code/stories/CaptureQrCode.stories.ts +114 -26
- package/app/components/02.molecules/qr-code/stories/DecodeQrCode.stories.ts +115 -24
- package/app/components/02.molecules/qr-code/tests/CaptureQrCode.spec.ts +124 -51
- package/app/components/02.molecules/qr-code/tests/DecodeQrCode.spec.ts +74 -25
- package/app/components/02.molecules/samaritan-prompt/CONSUMER-STYLING.md +22 -10
- package/app/components/02.molecules/samaritan-prompt/SamaritanPrompt.vue +30 -12
- package/app/components/02.molecules/samaritan-prompt/SamaritanPromptMixed.vue +34 -10
- package/app/components/02.molecules/samaritan-prompt/stories/SamaritanPrompt.stories.ts +55 -4
- package/app/components/02.molecules/samaritan-prompt/stories/SamaritanPromptMixed.stories.ts +67 -7
- package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPrompt.spec.ts +113 -29
- package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPromptMixed.spec.ts +92 -45
- package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +82 -29
- package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +176 -113
- package/app/components/03.organisms/image-galleries/carousel-flip/stories/CarouselFlip.stories.ts +153 -23
- package/app/components/03.organisms/image-galleries/carousel-flip/tests/CarouselFlip.spec.ts +119 -63
- package/app/components/03.organisms/responsive-header/tests/__snapshots__/NavigationItems.spec.ts.snap +2 -2
- package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +2 -2
- package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +8 -8
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +4 -4
- package/app/components/03.organisms/site-header/CONSUMER-STYLING.md +41 -23
- package/app/components/03.organisms/site-header/SiteHeader.vue +16 -21
- package/app/components/03.organisms/site-header/stories/SiteHeader.stories.ts +138 -0
- package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +53 -26
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +2 -2
- package/app/types/components/qr-code.d.ts +12 -0
- package/app/utils/graphemes.ts +8 -0
- package/package.json +1 -1
- package/.claude/skills/components/accordian-core.md +0 -180
- package/.vscode/srcdev-component-accordian-core.code-snippets +0 -74
- package/app/components/01.atoms/animations/entry/.nuxt/app.config.mjs +0 -21
- package/app/components/01.atoms/animations/entry/.nuxt/components.d.ts +0 -66
- package/app/components/01.atoms/animations/entry/.nuxt/imports.d.ts +0 -30
- package/app/components/01.atoms/animations/entry/.nuxt/manifest/meta/test.json +0 -1
- package/app/components/01.atoms/animations/entry/.nuxt/types/app.config.d.ts +0 -35
- package/app/components/01.atoms/animations/entry/.nuxt/types/build.d.ts +0 -23
- package/app/components/01.atoms/animations/entry/.nuxt/types/builder-env.d.ts +0 -1
- package/app/components/01.atoms/animations/entry/.nuxt/types/components.d.ts +0 -71
- package/app/components/01.atoms/animations/entry/.nuxt/types/imports.d.ts +0 -386
- package/app/components/01.atoms/animations/entry/.nuxt/types/middleware.d.ts +0 -11
- package/app/components/01.atoms/animations/entry/.nuxt/types/modules.d.ts +0 -79
- package/app/components/01.atoms/animations/entry/.nuxt/types/nitro-layouts.d.ts +0 -17
- package/app/components/01.atoms/animations/entry/.nuxt/types/nitro-nuxt.d.ts +0 -64
- package/app/components/01.atoms/animations/entry/.nuxt/types/plugins.d.ts +0 -29
- package/app/components/01.atoms/animations/entry/.nuxt/types/runtime-config.d.ts +0 -32
- package/app/components/01.atoms/animations/entry/.nuxt/types/shared-imports.d.ts +0 -10
- package/app/components/01.atoms/animations/entry/.nuxt/types/vue-shim.d.ts +0 -0
- package/app/components/01.atoms/animations/entry/.nuxtrc +0 -1
- package/app/components/01.atoms/animations/entry/node_modules/.cache/nuxt/chrome-workspace.json +0 -1
- package/app/components/01.atoms/animations/entry/node_modules/.cache/vite/client/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/app/components/02.molecules/expandable/accordian/CONSUMER-STYLING.md +0 -100
- package/app/components/02.molecules/expandable/accordian/stories/AccordianCore.stories.ts +0 -272
- package/app/components/02.molecules/expandable/accordian/tests/AccordianCore.spec.ts +0 -257
- package/app/components/02.molecules/expandable/accordian/tests/__snapshots__/AccordianCore.spec.ts.snap +0 -95
- package/app/components/03.organisms/responsive-header/tests/.nuxt/app.config.mjs +0 -21
- package/app/components/03.organisms/responsive-header/tests/.nuxt/components.d.ts +0 -66
- package/app/components/03.organisms/responsive-header/tests/.nuxt/imports.d.ts +0 -30
- package/app/components/03.organisms/responsive-header/tests/.nuxt/manifest/meta/test.json +0 -1
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/app.config.d.ts +0 -35
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/build.d.ts +0 -23
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/builder-env.d.ts +0 -1
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/components.d.ts +0 -71
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/imports.d.ts +0 -386
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/middleware.d.ts +0 -11
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/modules.d.ts +0 -79
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-layouts.d.ts +0 -17
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-nuxt.d.ts +0 -64
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/plugins.d.ts +0 -29
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/runtime-config.d.ts +0 -32
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/shared-imports.d.ts +0 -10
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/vue-shim.d.ts +0 -0
- package/app/components/03.organisms/responsive-header/tests/node_modules/.cache/nuxt/chrome-workspace.json +0 -1
|
@@ -2,19 +2,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
|
2
2
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
3
|
import DashboardStatsGrid from "../DashboardStatsGrid.vue";
|
|
4
4
|
|
|
5
|
-
// Mock useStyleClassPassthrough composable
|
|
6
|
-
const mockElementClasses = { value: "" };
|
|
7
|
-
const mockResetElementClasses = vi.fn();
|
|
8
|
-
|
|
9
|
-
const mockUseStyleClassPassthrough = vi.fn(() => ({
|
|
10
|
-
elementClasses: mockElementClasses,
|
|
11
|
-
resetElementClasses: mockResetElementClasses,
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
|
-
vi.mock("#imports", () => ({
|
|
15
|
-
useStyleClassPassthrough: mockUseStyleClassPassthrough,
|
|
16
|
-
}));
|
|
17
|
-
|
|
18
5
|
describe("DashboardStatsGrid", () => {
|
|
19
6
|
let wrapper: ReturnType<typeof mountSuspended>;
|
|
20
7
|
|
|
@@ -36,34 +23,34 @@ describe("DashboardStatsGrid", () => {
|
|
|
36
23
|
|
|
37
24
|
beforeEach(() => {
|
|
38
25
|
vi.clearAllMocks();
|
|
39
|
-
mockElementClasses.value = "";
|
|
40
26
|
});
|
|
41
27
|
|
|
42
28
|
afterEach(() => {
|
|
43
29
|
wrapper?.unmount();
|
|
30
|
+
vi.restoreAllMocks();
|
|
44
31
|
});
|
|
45
32
|
|
|
46
33
|
describe("Component Rendering", () => {
|
|
47
34
|
it("mounts without error", async () => {
|
|
48
35
|
await createWrapper();
|
|
49
36
|
|
|
50
|
-
expect(wrapper.
|
|
51
|
-
expect(wrapper.find(".dashboard-stats-grid")).
|
|
37
|
+
expect(wrapper.classes()).toContain("dashboard-stats-grid");
|
|
38
|
+
expect(wrapper.find(".dashboard-stats-grid").exists()).toBe(true);
|
|
52
39
|
});
|
|
53
40
|
|
|
54
41
|
it("renders top row structure correctly", async () => {
|
|
55
42
|
await createWrapper();
|
|
56
43
|
|
|
57
|
-
expect(wrapper.find(".top-row")).
|
|
58
|
-
expect(wrapper.find(".top-row-slot-1")).
|
|
59
|
-
expect(wrapper.find(".top-row-slot-2")).
|
|
60
|
-
expect(wrapper.find(".top-row-slot-3")).
|
|
44
|
+
expect(wrapper.find(".dashboard-stats-grid-top-row").exists()).toBe(true);
|
|
45
|
+
expect(wrapper.find(".dashboard-stats-grid-top-row-slot-1").exists()).toBe(true);
|
|
46
|
+
expect(wrapper.find(".dashboard-stats-grid-top-row-slot-2").exists()).toBe(true);
|
|
47
|
+
expect(wrapper.find(".dashboard-stats-grid-top-row-slot-3").exists()).toBe(true);
|
|
61
48
|
});
|
|
62
49
|
|
|
63
50
|
it("renders bottom row correctly", async () => {
|
|
64
51
|
await createWrapper();
|
|
65
52
|
|
|
66
|
-
expect(wrapper.find(".bottom-row")).
|
|
53
|
+
expect(wrapper.find(".dashboard-stats-grid-bottom-row").exists()).toBe(true);
|
|
67
54
|
});
|
|
68
55
|
|
|
69
56
|
it("has correct grid container structure", async () => {
|
|
@@ -82,7 +69,7 @@ describe("DashboardStatsGrid", () => {
|
|
|
82
69
|
await createWrapper({ topRowSlot1ItemCount: itemCount });
|
|
83
70
|
|
|
84
71
|
// Component should render without error with custom item count
|
|
85
|
-
expect(wrapper.find(".dashboard-stats-grid")).
|
|
72
|
+
expect(wrapper.find(".dashboard-stats-grid").exists()).toBe(true);
|
|
86
73
|
});
|
|
87
74
|
|
|
88
75
|
it("accepts bottomRowItemCount prop", async () => {
|
|
@@ -90,7 +77,7 @@ describe("DashboardStatsGrid", () => {
|
|
|
90
77
|
await createWrapper({ bottomRowItemCount: itemCount });
|
|
91
78
|
|
|
92
79
|
// Component should render without error with custom item count
|
|
93
|
-
expect(wrapper.find(".dashboard-stats-grid")).
|
|
80
|
+
expect(wrapper.find(".dashboard-stats-grid").exists()).toBe(true);
|
|
94
81
|
});
|
|
95
82
|
|
|
96
83
|
it("uses default props when not provided", async () => {
|
|
@@ -100,7 +87,7 @@ describe("DashboardStatsGrid", () => {
|
|
|
100
87
|
});
|
|
101
88
|
|
|
102
89
|
// Should use default values (6 and 4)
|
|
103
|
-
expect(wrapper.find(".dashboard-stats-grid")).
|
|
90
|
+
expect(wrapper.find(".dashboard-stats-grid").exists()).toBe(true);
|
|
104
91
|
});
|
|
105
92
|
|
|
106
93
|
it("accepts string styleClassPassthrough", async () => {
|
|
@@ -108,7 +95,7 @@ describe("DashboardStatsGrid", () => {
|
|
|
108
95
|
await createWrapper({ styleClassPassthrough: className });
|
|
109
96
|
|
|
110
97
|
// Component should mount without error with string class
|
|
111
|
-
expect(wrapper.find(".dashboard-stats-grid")).
|
|
98
|
+
expect(wrapper.find(".dashboard-stats-grid").exists()).toBe(true);
|
|
112
99
|
});
|
|
113
100
|
|
|
114
101
|
it("accepts array styleClassPassthrough", async () => {
|
|
@@ -116,16 +103,27 @@ describe("DashboardStatsGrid", () => {
|
|
|
116
103
|
await createWrapper({ styleClassPassthrough: classNames });
|
|
117
104
|
|
|
118
105
|
// Component should mount without error with array of classes
|
|
119
|
-
expect(wrapper.find(".dashboard-stats-grid")).
|
|
106
|
+
expect(wrapper.find(".dashboard-stats-grid").exists()).toBe(true);
|
|
120
107
|
});
|
|
121
108
|
|
|
122
|
-
it("applies
|
|
123
|
-
|
|
109
|
+
it("applies styleClassPassthrough classes to the root", async () => {
|
|
110
|
+
await createWrapper({ styleClassPassthrough: ["test-class", "another-class"] });
|
|
111
|
+
expect(wrapper.classes()).toContain("test-class");
|
|
112
|
+
expect(wrapper.classes()).toContain("another-class");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
124
115
|
|
|
125
|
-
|
|
116
|
+
describe("Item counts", () => {
|
|
117
|
+
it("floors fractional counts and treats negative or non-finite counts as zero", async () => {
|
|
118
|
+
const count = (w: typeof wrapper, selector: string) => w.findAll(`${selector} > .dashboard-stats-grid-panel`).length;
|
|
119
|
+
|
|
120
|
+
await createWrapper({ topRowSlot1ItemCount: 2.7, bottomRowItemCount: -3 });
|
|
121
|
+
expect(count(wrapper, ".dashboard-stats-grid-top-row-slot-1-inner")).toBe(2);
|
|
122
|
+
expect(count(wrapper, ".dashboard-stats-grid-bottom-row")).toBe(0);
|
|
126
123
|
|
|
127
|
-
|
|
128
|
-
expect(wrapper
|
|
124
|
+
await wrapper.setProps({ topRowSlot1ItemCount: Number.NaN, bottomRowItemCount: 3 });
|
|
125
|
+
expect(count(wrapper, ".dashboard-stats-grid-top-row-slot-1-inner")).toBe(0);
|
|
126
|
+
expect(count(wrapper, ".dashboard-stats-grid-bottom-row")).toBe(3);
|
|
129
127
|
});
|
|
130
128
|
});
|
|
131
129
|
|
|
@@ -157,7 +155,7 @@ describe("DashboardStatsGrid", () => {
|
|
|
157
155
|
const { unmount } = await createWrapper({ topRowSlot1ItemCount: count }, slots);
|
|
158
156
|
|
|
159
157
|
// Should render without error
|
|
160
|
-
expect(wrapper.find(".dashboard-stats-grid")).
|
|
158
|
+
expect(wrapper.find(".dashboard-stats-grid").exists()).toBe(true);
|
|
161
159
|
|
|
162
160
|
// Check that content is rendered
|
|
163
161
|
for (let i = 1; i <= count; i++) {
|
|
@@ -239,7 +237,7 @@ describe("DashboardStatsGrid", () => {
|
|
|
239
237
|
const { unmount } = await createWrapper({ bottomRowItemCount: count }, slots);
|
|
240
238
|
|
|
241
239
|
// Should render without error
|
|
242
|
-
expect(wrapper.find(".dashboard-stats-grid")).
|
|
240
|
+
expect(wrapper.find(".dashboard-stats-grid").exists()).toBe(true);
|
|
243
241
|
|
|
244
242
|
// Check that content is rendered
|
|
245
243
|
for (let i = 1; i <= count; i++) {
|
|
@@ -294,9 +292,9 @@ describe("DashboardStatsGrid", () => {
|
|
|
294
292
|
await createWrapper({ styleClassPassthrough: ["initial"] });
|
|
295
293
|
|
|
296
294
|
// Component should maintain structure with different props
|
|
297
|
-
expect(wrapper.find(".dashboard-stats-grid")).
|
|
298
|
-
expect(wrapper.find(".top-row")).
|
|
299
|
-
expect(wrapper.find(".bottom-row")).
|
|
295
|
+
expect(wrapper.find(".dashboard-stats-grid").exists()).toBe(true);
|
|
296
|
+
expect(wrapper.find(".dashboard-stats-grid-top-row").exists()).toBe(true);
|
|
297
|
+
expect(wrapper.find(".dashboard-stats-grid-bottom-row").exists()).toBe(true);
|
|
300
298
|
});
|
|
301
299
|
|
|
302
300
|
it("handles different prop configurations", async () => {
|
|
@@ -315,7 +313,7 @@ describe("DashboardStatsGrid", () => {
|
|
|
315
313
|
expect(wrapper.html()).toContain("Bottom 1");
|
|
316
314
|
|
|
317
315
|
// Component should render properly
|
|
318
|
-
expect(wrapper.find(".dashboard-stats-grid")).
|
|
316
|
+
expect(wrapper.find(".dashboard-stats-grid").exists()).toBe(true);
|
|
319
317
|
|
|
320
318
|
unmount();
|
|
321
319
|
});
|
|
@@ -326,29 +324,29 @@ describe("DashboardStatsGrid", () => {
|
|
|
326
324
|
await createWrapper();
|
|
327
325
|
|
|
328
326
|
const grid = wrapper.find(".dashboard-stats-grid");
|
|
329
|
-
const topRow = wrapper.find(".top-row");
|
|
330
|
-
const bottomRow = wrapper.find(".bottom-row");
|
|
327
|
+
const topRow = wrapper.find(".dashboard-stats-grid-top-row");
|
|
328
|
+
const bottomRow = wrapper.find(".dashboard-stats-grid-bottom-row");
|
|
331
329
|
|
|
332
330
|
expect(grid.classes()).toContain("dashboard-stats-grid");
|
|
333
|
-
expect(topRow.classes()).toContain("top-row");
|
|
334
|
-
expect(bottomRow.classes()).toContain("bottom-row");
|
|
331
|
+
expect(topRow.classes()).toContain("dashboard-stats-grid-top-row");
|
|
332
|
+
expect(bottomRow.classes()).toContain("dashboard-stats-grid-bottom-row");
|
|
335
333
|
});
|
|
336
334
|
|
|
337
335
|
it("maintains correct DOM hierarchy", async () => {
|
|
338
336
|
await createWrapper();
|
|
339
337
|
|
|
340
338
|
const grid = wrapper.find(".dashboard-stats-grid");
|
|
341
|
-
const topRow = grid.find(".top-row");
|
|
342
|
-
const bottomRow = grid.find(".bottom-row");
|
|
339
|
+
const topRow = grid.find(".dashboard-stats-grid-top-row");
|
|
340
|
+
const bottomRow = grid.find(".dashboard-stats-grid-bottom-row");
|
|
343
341
|
|
|
344
342
|
expect(grid.exists()).toBe(true);
|
|
345
343
|
expect(topRow.exists()).toBe(true);
|
|
346
344
|
expect(bottomRow.exists()).toBe(true);
|
|
347
345
|
|
|
348
346
|
// Check top row slots exist
|
|
349
|
-
expect(topRow.find(".top-row-slot-1").exists()).toBe(true);
|
|
350
|
-
expect(topRow.find(".top-row-slot-2").exists()).toBe(true);
|
|
351
|
-
expect(topRow.find(".top-row-slot-3").exists()).toBe(true);
|
|
347
|
+
expect(topRow.find(".dashboard-stats-grid-top-row-slot-1").exists()).toBe(true);
|
|
348
|
+
expect(topRow.find(".dashboard-stats-grid-top-row-slot-2").exists()).toBe(true);
|
|
349
|
+
expect(topRow.find(".dashboard-stats-grid-top-row-slot-3").exists()).toBe(true);
|
|
352
350
|
});
|
|
353
351
|
});
|
|
354
352
|
|
|
@@ -359,9 +357,9 @@ describe("DashboardStatsGrid", () => {
|
|
|
359
357
|
bottomRowItemCount: 0,
|
|
360
358
|
});
|
|
361
359
|
|
|
362
|
-
expect(wrapper.find(".dashboard-stats-grid")).
|
|
363
|
-
expect(wrapper.find(".top-row")).
|
|
364
|
-
expect(wrapper.find(".bottom-row")).
|
|
360
|
+
expect(wrapper.find(".dashboard-stats-grid").exists()).toBe(true);
|
|
361
|
+
expect(wrapper.find(".dashboard-stats-grid-top-row").exists()).toBe(true);
|
|
362
|
+
expect(wrapper.find(".dashboard-stats-grid-bottom-row").exists()).toBe(true);
|
|
365
363
|
});
|
|
366
364
|
|
|
367
365
|
it("handles large item counts", async () => {
|
|
@@ -382,7 +380,7 @@ describe("DashboardStatsGrid", () => {
|
|
|
382
380
|
slots
|
|
383
381
|
);
|
|
384
382
|
|
|
385
|
-
expect(wrapper.find(".dashboard-stats-grid")).
|
|
383
|
+
expect(wrapper.find(".dashboard-stats-grid").exists()).toBe(true);
|
|
386
384
|
expect(wrapper.html()).toContain("Large Panel 1");
|
|
387
385
|
expect(wrapper.html()).toContain("Large Bottom 1");
|
|
388
386
|
});
|
|
@@ -436,9 +434,9 @@ describe("DashboardStatsGrid", () => {
|
|
|
436
434
|
}
|
|
437
435
|
);
|
|
438
436
|
|
|
439
|
-
expect(wrapper.find(".dashboard-stats-grid")).
|
|
440
|
-
expect(wrapper.find(".top-row")).
|
|
441
|
-
expect(wrapper.find(".bottom-row")).
|
|
437
|
+
expect(wrapper.find(".dashboard-stats-grid").exists()).toBe(true);
|
|
438
|
+
expect(wrapper.find(".dashboard-stats-grid-top-row").exists()).toBe(true);
|
|
439
|
+
expect(wrapper.find(".dashboard-stats-grid-bottom-row").exists()).toBe(true);
|
|
442
440
|
});
|
|
443
441
|
});
|
|
444
442
|
|
package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/CONSUMER-STYLING.md
CHANGED
|
@@ -42,6 +42,9 @@ old container query. Fewer items than columns leave empty tracks, as before.
|
|
|
42
42
|
The column-gap feeds that calculation, so it must be a single length (`gap: "1rem 2rem"` style
|
|
43
43
|
shorthand isn't supported; use the row/column gap tokens instead).
|
|
44
44
|
|
|
45
|
+
Each slotted item gets `min-inline-size: 0` (since 2026-10-21), so an item with an unbroken string
|
|
46
|
+
shrinks to its column instead of overflowing it; wrapping the text is up to your own markup.
|
|
47
|
+
|
|
45
48
|
## Local overrides
|
|
46
49
|
|
|
47
50
|
Set the tokens above on an element you own (a page or section class, or a class added with
|
|
@@ -66,6 +66,10 @@ watch(
|
|
|
66
66
|
grid-template-columns: repeat(auto-fill, minmax(min(100%, max(var(--_collapse), var(--_column-min))), 1fr));
|
|
67
67
|
row-gap: var(--layout-grid-by-cols-row-gap, var(--layout-grid-by-cols-gap, 1rem));
|
|
68
68
|
column-gap: var(--_column-gap);
|
|
69
|
+
|
|
70
|
+
> * {
|
|
71
|
+
min-inline-size: 0;
|
|
72
|
+
}
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { computed } from "vue";
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import type { MediaCanvas } from "~/types/components";
|
|
3
|
+
import CanvasSwitcher from "../../../../canvas-switcher/CanvasSwitcher.vue";
|
|
2
4
|
import LayoutGridByCols from "../LayoutGridByCols.vue";
|
|
3
|
-
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
5
|
+
import type { Meta, StoryObj, StoryFn } from "@nuxtjs/storybook";
|
|
4
6
|
|
|
5
7
|
type StoryArgs = InstanceType<typeof LayoutGridByCols>["$props"] & { itemCount?: number };
|
|
6
8
|
|
|
@@ -201,3 +203,46 @@ export const ZeroItems: Story = {
|
|
|
201
203
|
},
|
|
202
204
|
render: (args) => renderGrid(args),
|
|
203
205
|
};
|
|
206
|
+
|
|
207
|
+
export const StressTest: StoryFn = () => ({
|
|
208
|
+
components: { LayoutGridByCols },
|
|
209
|
+
template: `
|
|
210
|
+
<LayoutGridByCols :column-count="3">
|
|
211
|
+
<template #item-1><div style="padding: 1.2rem; background: var(--theme-surface-subtle); outline: 1px dashed currentColor;"><h3 style="margin: 0;">Supercalifragilisticexpialidociousandthensomemoretextwithoutanyspaces</h3><p style="margin: 0;">https://example.com/a/very/long/unbroken/path/that/never/wraps/on/its/own</p></div></template>
|
|
212
|
+
<template #item-2><div style="padding: 1.2rem; background: var(--theme-surface-subtle); outline: 1px dashed currentColor;"><p style="margin: 0;">Ein sehr langer Beschreibungstext aus Northtown, der auf schmalen Bildschirmen mehrfach umbrechen muss, ohne das Raster zu sprengen oder die Nachbarspalten zusammenzudrücken.</p></div></template>
|
|
213
|
+
<template #item-3><div style="padding: 1.2rem; background: var(--theme-surface-subtle); outline: 1px dashed currentColor;"><img src="/images/banners/banner-1.webp" width="1600" height="400" alt="" style="display: block; max-inline-size: 100%; block-size: auto;"></div></template>
|
|
214
|
+
<template #item-4><div style="padding: 1.2rem; background: var(--theme-surface-subtle); outline: 1px dashed currentColor;"><p style="margin: 0;">🙂 👍🏽 👨👩👧 <b>Not bold</b></p></div></template>
|
|
215
|
+
<template #item-5><div style="padding: 1.2rem; background: var(--theme-surface-subtle); outline: 1px dashed currentColor;"><p style="margin: 0;" dir="rtl">مرحبا بالعالم، نص طويل من اليمين إلى اليسار</p></div></template>
|
|
216
|
+
<template #item-6><div style="padding: 1.2rem; background: var(--theme-surface-subtle); outline: 1px dashed currentColor;"></div></template>
|
|
217
|
+
<template #item-7><div style="padding: 1.2rem; background: var(--theme-surface-subtle); outline: 1px dashed currentColor;"><p style="margin: 0;">x</p></div></template>
|
|
218
|
+
</LayoutGridByCols>
|
|
219
|
+
`,
|
|
220
|
+
});
|
|
221
|
+
StressTest.storyName = "Stress Test (Worst-Case Data)";
|
|
222
|
+
StressTest.decorators = [
|
|
223
|
+
(story, context) => ({
|
|
224
|
+
components: { story, CanvasSwitcher },
|
|
225
|
+
setup() {
|
|
226
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "mobileCanvas");
|
|
227
|
+
return { canvasName };
|
|
228
|
+
},
|
|
229
|
+
template: `
|
|
230
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
231
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
232
|
+
</div>
|
|
233
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
234
|
+
<story />
|
|
235
|
+
</div>
|
|
236
|
+
`,
|
|
237
|
+
}),
|
|
238
|
+
];
|
|
239
|
+
StressTest.parameters = {
|
|
240
|
+
layout: "fullscreen",
|
|
241
|
+
docs: {
|
|
242
|
+
story: { inline: false, height: "720px" },
|
|
243
|
+
description: {
|
|
244
|
+
story:
|
|
245
|
+
"Oversized and hostile slot content at every `CanvasSwitcher` width. Check that: the unbroken heading and URL wrap inside their cell instead of widening it; the German paragraph wraps; the 1600px image scales down to its cell; emoji, `<b>` and right-to-left text render as text; the empty and one-character cells keep their place; no neighbouring cell is squeezed to nothing and nothing scrolls sideways.",
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
};
|
|
@@ -1,19 +1,32 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
2
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
3
|
import LayoutGridByCols from "../LayoutGridByCols.vue";
|
|
4
4
|
|
|
5
|
+
const mounted: Awaited<ReturnType<typeof mountSuspended>>[] = [];
|
|
6
|
+
|
|
7
|
+
const mountComponent = async (options: Parameters<typeof mountSuspended<typeof LayoutGridByCols>>[1] = {}) => {
|
|
8
|
+
const wrapper = await mountSuspended(LayoutGridByCols, options);
|
|
9
|
+
mounted.push(wrapper);
|
|
10
|
+
return wrapper;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
mounted.splice(0).forEach((wrapper) => wrapper.unmount());
|
|
15
|
+
vi.restoreAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
5
18
|
describe("LayoutGridByCols", () => {
|
|
6
19
|
// ─── Mount ───────────────────────────────────────────────────────────────
|
|
7
20
|
|
|
8
21
|
it("mounts without error", async () => {
|
|
9
|
-
const wrapper = await
|
|
22
|
+
const wrapper = await mountComponent();
|
|
10
23
|
expect(wrapper.vm).toBeTruthy();
|
|
11
24
|
});
|
|
12
25
|
|
|
13
26
|
// ─── Snapshots ───────────────────────────────────────────────────────────
|
|
14
27
|
|
|
15
28
|
it("renders correct HTML structure (div, 2 items)", async () => {
|
|
16
|
-
const wrapper = await
|
|
29
|
+
const wrapper = await mountComponent({
|
|
17
30
|
slots: {
|
|
18
31
|
"item-0": "<p>First</p>",
|
|
19
32
|
"item-1": "<p>Second</p>",
|
|
@@ -23,14 +36,14 @@ describe("LayoutGridByCols", () => {
|
|
|
23
36
|
});
|
|
24
37
|
|
|
25
38
|
it("renders correct HTML structure (section with label)", async () => {
|
|
26
|
-
const wrapper = await
|
|
39
|
+
const wrapper = await mountComponent({
|
|
27
40
|
props: { tag: "section", label: "Feature grid" },
|
|
28
41
|
});
|
|
29
42
|
expect(wrapper.html()).toMatchSnapshot();
|
|
30
43
|
});
|
|
31
44
|
|
|
32
45
|
it("renders correct HTML structure with styleClassPassthrough", async () => {
|
|
33
|
-
const wrapper = await
|
|
46
|
+
const wrapper = await mountComponent({
|
|
34
47
|
props: { styleClassPassthrough: ["custom-class", "another-class"] },
|
|
35
48
|
});
|
|
36
49
|
expect(wrapper.html()).toMatchSnapshot();
|
|
@@ -39,12 +52,12 @@ describe("LayoutGridByCols", () => {
|
|
|
39
52
|
// ─── Tag rendering ───────────────────────────────────────────────────────
|
|
40
53
|
|
|
41
54
|
it("renders as <div> by default", async () => {
|
|
42
|
-
const wrapper = await
|
|
55
|
+
const wrapper = await mountComponent();
|
|
43
56
|
expect(wrapper.element.tagName).toBe("DIV");
|
|
44
57
|
});
|
|
45
58
|
|
|
46
59
|
it("renders as <section> when tag='section'", async () => {
|
|
47
|
-
const wrapper = await
|
|
60
|
+
const wrapper = await mountComponent({
|
|
48
61
|
props: { tag: "section" },
|
|
49
62
|
});
|
|
50
63
|
expect(wrapper.element.tagName).toBe("SECTION");
|
|
@@ -53,21 +66,21 @@ describe("LayoutGridByCols", () => {
|
|
|
53
66
|
// ─── Base class ──────────────────────────────────────────────────────────
|
|
54
67
|
|
|
55
68
|
it("always has the layout-grid-by-cols class", async () => {
|
|
56
|
-
const wrapper = await
|
|
69
|
+
const wrapper = await mountComponent();
|
|
57
70
|
expect(wrapper.classes()).toContain("layout-grid-by-cols");
|
|
58
71
|
});
|
|
59
72
|
|
|
60
73
|
// ─── Inner div ───────────────────────────────────────────────────────────
|
|
61
74
|
|
|
62
75
|
it("renders a .layout-grid-by-cols-inner div", async () => {
|
|
63
|
-
const wrapper = await
|
|
76
|
+
const wrapper = await mountComponent();
|
|
64
77
|
expect(wrapper.find(".layout-grid-by-cols-inner").exists()).toBe(true);
|
|
65
78
|
});
|
|
66
79
|
|
|
67
80
|
// ─── Accessibility ───────────────────────────────────────────────────────
|
|
68
81
|
|
|
69
82
|
it("renders sr-only label and aria-labelledby when tag is section", async () => {
|
|
70
|
-
const wrapper = await
|
|
83
|
+
const wrapper = await mountComponent({
|
|
71
84
|
props: { tag: "section", label: "Card grid" },
|
|
72
85
|
});
|
|
73
86
|
const srOnly = wrapper.find(".sr-only");
|
|
@@ -77,7 +90,7 @@ describe("LayoutGridByCols", () => {
|
|
|
77
90
|
});
|
|
78
91
|
|
|
79
92
|
it("does not render sr-only label or aria-labelledby when tag is div", async () => {
|
|
80
|
-
const wrapper = await
|
|
93
|
+
const wrapper = await mountComponent({
|
|
81
94
|
props: { tag: "div", label: "Ignored" },
|
|
82
95
|
});
|
|
83
96
|
expect(wrapper.find(".sr-only").exists()).toBe(false);
|
|
@@ -87,7 +100,7 @@ describe("LayoutGridByCols", () => {
|
|
|
87
100
|
// ─── Dynamic slots ───────────────────────────────────────────────────────
|
|
88
101
|
|
|
89
102
|
it("renders slot content for each item inside the inner div", async () => {
|
|
90
|
-
const wrapper = await
|
|
103
|
+
const wrapper = await mountComponent({
|
|
91
104
|
slots: {
|
|
92
105
|
"item-0": "<span>Alpha</span>",
|
|
93
106
|
"item-1": "<span>Beta</span>",
|
|
@@ -101,21 +114,21 @@ describe("LayoutGridByCols", () => {
|
|
|
101
114
|
});
|
|
102
115
|
|
|
103
116
|
it("renders no slot content when no slots are provided", async () => {
|
|
104
|
-
const wrapper = await
|
|
117
|
+
const wrapper = await mountComponent();
|
|
105
118
|
expect(wrapper.find(".layout-grid-by-cols-inner").text().trim()).toBe("");
|
|
106
119
|
});
|
|
107
120
|
|
|
108
121
|
// ─── styleClassPassthrough ───────────────────────────────────────────────
|
|
109
122
|
|
|
110
123
|
it("applies a single styleClassPassthrough string", async () => {
|
|
111
|
-
const wrapper = await
|
|
124
|
+
const wrapper = await mountComponent({
|
|
112
125
|
props: { styleClassPassthrough: "my-class" },
|
|
113
126
|
});
|
|
114
127
|
expect(wrapper.classes()).toContain("my-class");
|
|
115
128
|
});
|
|
116
129
|
|
|
117
130
|
it("applies multiple styleClassPassthrough classes from an array", async () => {
|
|
118
|
-
const wrapper = await
|
|
131
|
+
const wrapper = await mountComponent({
|
|
119
132
|
props: { styleClassPassthrough: ["class-a", "class-b"] },
|
|
120
133
|
});
|
|
121
134
|
expect(wrapper.classes()).toContain("class-a");
|
|
@@ -128,40 +141,40 @@ describe("LayoutGridByCols", () => {
|
|
|
128
141
|
(wrapper.element as HTMLElement).style.getPropertyValue(name);
|
|
129
142
|
|
|
130
143
|
it("writes no inline style when no layout props are passed", async () => {
|
|
131
|
-
const wrapper = await
|
|
144
|
+
const wrapper = await mountComponent();
|
|
132
145
|
expect(wrapper.attributes("style")).toBeUndefined();
|
|
133
146
|
});
|
|
134
147
|
|
|
135
148
|
it("writes columnCount to --layout-grid-by-cols-column-count", async () => {
|
|
136
|
-
const wrapper = await
|
|
149
|
+
const wrapper = await mountComponent({
|
|
137
150
|
props: { columnCount: 3 },
|
|
138
151
|
});
|
|
139
152
|
expect(inlineToken(wrapper, "--layout-grid-by-cols-column-count")).toBe("3");
|
|
140
153
|
});
|
|
141
154
|
|
|
142
155
|
it("clamps columnCount to a minimum of 2", async () => {
|
|
143
|
-
const wrapper = await
|
|
156
|
+
const wrapper = await mountComponent({
|
|
144
157
|
props: { columnCount: 1 as unknown as 2 },
|
|
145
158
|
});
|
|
146
159
|
expect(inlineToken(wrapper, "--layout-grid-by-cols-column-count")).toBe("2");
|
|
147
160
|
});
|
|
148
161
|
|
|
149
162
|
it("writes gap to --layout-grid-by-cols-gap", async () => {
|
|
150
|
-
const wrapper = await
|
|
163
|
+
const wrapper = await mountComponent({
|
|
151
164
|
props: { gap: "2rem" },
|
|
152
165
|
});
|
|
153
166
|
expect(inlineToken(wrapper, "--layout-grid-by-cols-gap")).toBe("2rem");
|
|
154
167
|
});
|
|
155
168
|
|
|
156
169
|
it("writes singleColBelow to --layout-grid-by-cols-single-col-below", async () => {
|
|
157
|
-
const wrapper = await
|
|
170
|
+
const wrapper = await mountComponent({
|
|
158
171
|
props: { singleColBelow: "600px" },
|
|
159
172
|
});
|
|
160
173
|
expect(inlineToken(wrapper, "--layout-grid-by-cols-single-col-below")).toBe("600px");
|
|
161
174
|
});
|
|
162
175
|
|
|
163
176
|
it("updates the inline token when a prop changes", async () => {
|
|
164
|
-
const wrapper = await
|
|
177
|
+
const wrapper = await mountComponent({
|
|
165
178
|
props: { columnCount: 3 },
|
|
166
179
|
});
|
|
167
180
|
await wrapper.setProps({ columnCount: 4 });
|
|
@@ -172,7 +185,7 @@ describe("LayoutGridByCols", () => {
|
|
|
172
185
|
|
|
173
186
|
it("does not render placeholder text or aria-labelledby for a section without a label", async () => {
|
|
174
187
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
175
|
-
const wrapper = await
|
|
188
|
+
const wrapper = await mountComponent({
|
|
176
189
|
props: { tag: "section" },
|
|
177
190
|
});
|
|
178
191
|
expect(wrapper.find(".sr-only").exists()).toBe(false);
|
|
@@ -185,7 +198,7 @@ describe("LayoutGridByCols", () => {
|
|
|
185
198
|
// ─── Combined ────────────────────────────────────────────────────────────
|
|
186
199
|
|
|
187
200
|
it("renders correctly with all props and slots combined", async () => {
|
|
188
|
-
const wrapper = await
|
|
201
|
+
const wrapper = await mountComponent({
|
|
189
202
|
props: {
|
|
190
203
|
tag: "section",
|
|
191
204
|
label: "Services",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<component :is="tag" class="auto-grid" :class="[elementClasses
|
|
2
|
+
<component :is="tag" class="auto-grid" :class="[elementClasses]" :data-responsive="isResponsive ? '' : undefined">
|
|
3
3
|
<slot v-for="(_, name) in $slots" :key="name" :name="name"></slot>
|
|
4
4
|
</component>
|
|
5
5
|
</template>
|
|
@@ -31,11 +31,15 @@ watch(
|
|
|
31
31
|
display: grid;
|
|
32
32
|
gap: var(--auto-grid-gap, 1rem);
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
> * {
|
|
35
|
+
min-inline-size: 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&:not([data-responsive]) {
|
|
35
39
|
grid-template-columns: repeat(auto-fit, minmax(min(var(--auto-grid-min-col-size-default, 300px), 100%), 1fr));
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
&[data-responsive] {
|
|
39
43
|
grid-template-columns: repeat(auto-fit, minmax(min(var(--auto-grid-min-col-size-small, 250px), 100%), 1fr));
|
|
40
44
|
|
|
41
45
|
@container (width >= 768px) {
|
|
@@ -26,7 +26,7 @@ No private `--_` tokens.
|
|
|
26
26
|
| Hook | When | Effect |
|
|
27
27
|
|---|---|---|
|
|
28
28
|
| `.auto-grid` | Always | Root element |
|
|
29
|
-
| `.auto-grid
|
|
29
|
+
| `.auto-grid[data-responsive]` | `is-responsive` prop is `true` (was the class `.is-responsive` until 2026-10-21) | Column minimum steps small → default → large via `@container` queries at 768px and 1024px |
|
|
30
30
|
|
|
31
31
|
The breakpoints are container queries, so `is-responsive` needs an ancestor with
|
|
32
32
|
`container-type: inline-size`. Without one, the small size applies at every width.
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { computed } from "vue";
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import type { MediaCanvas } from "~/types/components";
|
|
3
|
+
import CanvasSwitcher from "../../../canvas-switcher/CanvasSwitcher.vue";
|
|
2
4
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
3
5
|
import AutoGridComponent from "../AutoGrid.vue";
|
|
4
6
|
|
|
@@ -289,3 +291,46 @@ SemanticSection.parameters = {
|
|
|
289
291
|
},
|
|
290
292
|
},
|
|
291
293
|
};
|
|
294
|
+
|
|
295
|
+
export const StressTest: StoryFn = () => ({
|
|
296
|
+
components: { AutoGridComponent },
|
|
297
|
+
template: `
|
|
298
|
+
<AutoGridComponent :is-responsive="true">
|
|
299
|
+
<template #item-1><div style="padding: 1.2rem; background: var(--theme-surface-subtle); outline: 1px dashed currentColor;"><h3 style="margin: 0;">Supercalifragilisticexpialidociousandthensomemoretextwithoutanyspaces</h3><p style="margin: 0;">https://example.com/a/very/long/unbroken/path/that/never/wraps/on/its/own</p></div></template>
|
|
300
|
+
<template #item-2><div style="padding: 1.2rem; background: var(--theme-surface-subtle); outline: 1px dashed currentColor;"><p style="margin: 0;">Ein sehr langer Beschreibungstext aus Northtown, der auf schmalen Bildschirmen mehrfach umbrechen muss, ohne das Raster zu sprengen oder die Nachbarspalten zusammenzudrücken.</p></div></template>
|
|
301
|
+
<template #item-3><div style="padding: 1.2rem; background: var(--theme-surface-subtle); outline: 1px dashed currentColor;"><img src="/images/banners/banner-1.webp" width="1600" height="400" alt="" style="display: block; max-inline-size: 100%; block-size: auto;"></div></template>
|
|
302
|
+
<template #item-4><div style="padding: 1.2rem; background: var(--theme-surface-subtle); outline: 1px dashed currentColor;"><p style="margin: 0;">🙂 👍🏽 👨👩👧 <b>Not bold</b></p></div></template>
|
|
303
|
+
<template #item-5><div style="padding: 1.2rem; background: var(--theme-surface-subtle); outline: 1px dashed currentColor;"><p style="margin: 0;" dir="rtl">مرحبا بالعالم، نص طويل من اليمين إلى اليسار</p></div></template>
|
|
304
|
+
<template #item-6><div style="padding: 1.2rem; background: var(--theme-surface-subtle); outline: 1px dashed currentColor;"></div></template>
|
|
305
|
+
<template #item-7><div style="padding: 1.2rem; background: var(--theme-surface-subtle); outline: 1px dashed currentColor;"><p style="margin: 0;">x</p></div></template>
|
|
306
|
+
</AutoGridComponent>
|
|
307
|
+
`,
|
|
308
|
+
});
|
|
309
|
+
StressTest.storyName = "Stress Test (Worst-Case Data)";
|
|
310
|
+
StressTest.decorators = [
|
|
311
|
+
(story, context) => ({
|
|
312
|
+
components: { story, CanvasSwitcher },
|
|
313
|
+
setup() {
|
|
314
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "mobileCanvas");
|
|
315
|
+
return { canvasName };
|
|
316
|
+
},
|
|
317
|
+
template: `
|
|
318
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
319
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
320
|
+
</div>
|
|
321
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
322
|
+
<story />
|
|
323
|
+
</div>
|
|
324
|
+
`,
|
|
325
|
+
}),
|
|
326
|
+
];
|
|
327
|
+
StressTest.parameters = {
|
|
328
|
+
layout: "fullscreen",
|
|
329
|
+
docs: {
|
|
330
|
+
story: { inline: false, height: "720px" },
|
|
331
|
+
description: {
|
|
332
|
+
story:
|
|
333
|
+
"Oversized and hostile slot content at every `CanvasSwitcher` width. Check that: the unbroken heading and URL wrap inside their cell instead of widening it; the German paragraph wraps; the 1600px image scales down to its cell; emoji, `<b>` and right-to-left text render as text; the empty and one-character cells keep their place; no neighbouring cell is squeezed to nothing and nothing scrolls sideways.",
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
};
|