srcdev-nuxt-components 9.6.19 → 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.
Files changed (135) hide show
  1. package/.claude/rules/consumer/ids-from-useid.md +3 -2
  2. package/.claude/skills/component-compliance-checklist.md +5 -0
  3. package/.claude/skills/components/auto-grid.md +3 -1
  4. package/.claude/skills/components/capture-qr-code.md +9 -5
  5. package/.claude/skills/components/carousel-flip.md +41 -30
  6. package/.claude/skills/components/column-flow-grid.md +2 -0
  7. package/.claude/skills/components/dashboard-quad-grid.md +7 -2
  8. package/.claude/skills/components/dashboard-stats-grid.md +4 -0
  9. package/.claude/skills/components/decode-qr-code.md +7 -2
  10. package/.claude/skills/components/deep-expanding-menu-classic.md +31 -30
  11. package/.claude/skills/components/deep-expanding-menu.md +22 -21
  12. package/.claude/skills/components/grid-stack.md +2 -0
  13. package/.claude/skills/components/layout-grid-by-cols.md +2 -0
  14. package/.claude/skills/components/masonry-grid.md +9 -1
  15. package/.claude/skills/components/page-row.md +3 -0
  16. package/.claude/skills/components/samaritan-prompt.md +24 -8
  17. package/.claude/skills/components/site-header.md +16 -2
  18. package/.claude/skills/index.md +12 -12
  19. package/.claude/skills/storybook-add-story.md +25 -0
  20. package/.claude/skills/testing-add-unit-test.md +26 -0
  21. package/.vscode/srcdev-component-capture-decode-qr-code.code-snippets +11 -2
  22. package/.vscode/srcdev-component-carousel-flip.code-snippets +22 -4
  23. package/.vscode/srcdev-component-deep-expanding-menu-classic.code-snippets +32 -2
  24. package/.vscode/srcdev-component-deep-expanding-menu.code-snippets +31 -1
  25. package/.vscode/srcdev-component-samaritan-prompt.code-snippets +5 -4
  26. package/.vscode/srcdev-component-site-header.code-snippets +13 -1
  27. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +12 -2
  28. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue +19 -17
  29. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts +42 -0
  30. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/tests/DashboardQuadGrid.spec.ts +31 -44
  31. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +13 -1
  32. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +40 -31
  33. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/stories/DashboardStatsGrid.stories.ts +47 -0
  34. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/tests/DashboardStatsGrid.spec.ts +51 -53
  35. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/CONSUMER-STYLING.md +3 -0
  36. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/LayoutGridByCols.vue +4 -0
  37. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/stories/LayoutGridByCols.stories.ts +47 -2
  38. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/LayoutGridByCols.spec.ts +36 -23
  39. package/app/components/01.atoms/grids/auto-grid/AutoGrid.vue +7 -3
  40. package/app/components/01.atoms/grids/auto-grid/CONSUMER-STYLING.md +1 -1
  41. package/app/components/01.atoms/grids/auto-grid/stories/AutoGrid.stories.ts +46 -1
  42. package/app/components/01.atoms/grids/auto-grid/tests/AutoGrid.spec.ts +40 -27
  43. package/app/components/01.atoms/grids/auto-grid/tests/__snapshots__/AutoGrid.spec.ts.snap +1 -1
  44. package/app/components/01.atoms/grids/column-flow-grid/CONSUMER-STYLING.md +9 -0
  45. package/app/components/01.atoms/grids/column-flow-grid/ColumnFlowGrid.vue +3 -2
  46. package/app/components/01.atoms/grids/column-flow-grid/stories/ColumnFlowGrid.stories.ts +46 -0
  47. package/app/components/01.atoms/grids/column-flow-grid/tests/ColumnFlowGrid.spec.ts +24 -11
  48. package/app/components/01.atoms/grids/grid-stack/CONSUMER-STYLING.md +3 -0
  49. package/app/components/01.atoms/grids/grid-stack/GridStack.vue +2 -0
  50. package/app/components/01.atoms/grids/grid-stack/stories/GridStack.stories.ts +41 -0
  51. package/app/components/01.atoms/grids/grid-stack/tests/GridStack.spec.ts +26 -13
  52. package/app/components/01.atoms/grids/masonry-grid/CONSUMER-STYLING.md +15 -0
  53. package/app/components/01.atoms/grids/masonry-grid/MasonryGrid.vue +51 -31
  54. package/app/components/01.atoms/grids/masonry-grid/stories/MasonryGrid.stories.ts +48 -0
  55. package/app/components/01.atoms/grids/masonry-grid/tests/MasonryGrid.spec.ts +84 -29
  56. package/app/components/01.atoms/page-row/CONSUMER-STYLING.md +6 -1
  57. package/app/components/01.atoms/page-row/PageRow.vue +16 -15
  58. package/app/components/01.atoms/page-row/stories/PageRow.stories.ts +23 -0
  59. package/app/components/01.atoms/page-row/tests/PageRow.spec.ts +6 -5
  60. package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +63 -44
  61. package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +74 -37
  62. package/app/components/02.molecules/navigation/deep-expanding-menu/stories/DeepExpandingMenu.stories.ts +139 -19
  63. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts +90 -27
  64. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +8 -8
  65. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +58 -26
  66. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +113 -57
  67. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/stories/DeepExpandingMenuClassic.stories.ts +139 -19
  68. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/DeepExpandingMenuClassic.spec.ts +140 -22
  69. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/__snapshots__/DeepExpandingMenuClassic.spec.ts.snap +9 -9
  70. package/app/components/02.molecules/qr-code/CONSUMER-STYLING.md +22 -7
  71. package/app/components/02.molecules/qr-code/CaptureQrCode.vue +63 -71
  72. package/app/components/02.molecules/qr-code/DecodeQrCode.vue +47 -10
  73. package/app/components/02.molecules/qr-code/stories/CaptureQrCode.stories.ts +114 -26
  74. package/app/components/02.molecules/qr-code/stories/DecodeQrCode.stories.ts +115 -24
  75. package/app/components/02.molecules/qr-code/tests/CaptureQrCode.spec.ts +124 -51
  76. package/app/components/02.molecules/qr-code/tests/DecodeQrCode.spec.ts +74 -25
  77. package/app/components/02.molecules/samaritan-prompt/CONSUMER-STYLING.md +22 -10
  78. package/app/components/02.molecules/samaritan-prompt/SamaritanPrompt.vue +30 -12
  79. package/app/components/02.molecules/samaritan-prompt/SamaritanPromptMixed.vue +34 -10
  80. package/app/components/02.molecules/samaritan-prompt/stories/SamaritanPrompt.stories.ts +55 -4
  81. package/app/components/02.molecules/samaritan-prompt/stories/SamaritanPromptMixed.stories.ts +67 -7
  82. package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPrompt.spec.ts +113 -29
  83. package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPromptMixed.spec.ts +92 -45
  84. package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +82 -29
  85. package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +176 -113
  86. package/app/components/03.organisms/image-galleries/carousel-flip/stories/CarouselFlip.stories.ts +153 -23
  87. package/app/components/03.organisms/image-galleries/carousel-flip/tests/CarouselFlip.spec.ts +119 -63
  88. package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +8 -8
  89. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +4 -4
  90. package/app/components/03.organisms/site-header/CONSUMER-STYLING.md +41 -23
  91. package/app/components/03.organisms/site-header/SiteHeader.vue +16 -21
  92. package/app/components/03.organisms/site-header/stories/SiteHeader.stories.ts +138 -0
  93. package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +53 -26
  94. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +2 -2
  95. package/app/types/components/qr-code.d.ts +12 -0
  96. package/app/utils/graphemes.ts +8 -0
  97. package/package.json +1 -1
  98. package/app/components/01.atoms/animations/entry/.nuxt/app.config.mjs +0 -21
  99. package/app/components/01.atoms/animations/entry/.nuxt/components.d.ts +0 -66
  100. package/app/components/01.atoms/animations/entry/.nuxt/imports.d.ts +0 -30
  101. package/app/components/01.atoms/animations/entry/.nuxt/manifest/meta/test.json +0 -1
  102. package/app/components/01.atoms/animations/entry/.nuxt/types/app.config.d.ts +0 -35
  103. package/app/components/01.atoms/animations/entry/.nuxt/types/build.d.ts +0 -23
  104. package/app/components/01.atoms/animations/entry/.nuxt/types/builder-env.d.ts +0 -1
  105. package/app/components/01.atoms/animations/entry/.nuxt/types/components.d.ts +0 -71
  106. package/app/components/01.atoms/animations/entry/.nuxt/types/imports.d.ts +0 -386
  107. package/app/components/01.atoms/animations/entry/.nuxt/types/middleware.d.ts +0 -11
  108. package/app/components/01.atoms/animations/entry/.nuxt/types/modules.d.ts +0 -79
  109. package/app/components/01.atoms/animations/entry/.nuxt/types/nitro-layouts.d.ts +0 -17
  110. package/app/components/01.atoms/animations/entry/.nuxt/types/nitro-nuxt.d.ts +0 -64
  111. package/app/components/01.atoms/animations/entry/.nuxt/types/plugins.d.ts +0 -29
  112. package/app/components/01.atoms/animations/entry/.nuxt/types/runtime-config.d.ts +0 -32
  113. package/app/components/01.atoms/animations/entry/.nuxt/types/shared-imports.d.ts +0 -10
  114. package/app/components/01.atoms/animations/entry/.nuxt/types/vue-shim.d.ts +0 -0
  115. package/app/components/01.atoms/animations/entry/.nuxtrc +0 -1
  116. package/app/components/01.atoms/animations/entry/node_modules/.cache/nuxt/chrome-workspace.json +0 -1
  117. package/app/components/01.atoms/animations/entry/node_modules/.cache/vite/client/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  118. package/app/components/03.organisms/responsive-header/tests/.nuxt/app.config.mjs +0 -21
  119. package/app/components/03.organisms/responsive-header/tests/.nuxt/components.d.ts +0 -66
  120. package/app/components/03.organisms/responsive-header/tests/.nuxt/imports.d.ts +0 -30
  121. package/app/components/03.organisms/responsive-header/tests/.nuxt/manifest/meta/test.json +0 -1
  122. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/app.config.d.ts +0 -35
  123. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/build.d.ts +0 -23
  124. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/builder-env.d.ts +0 -1
  125. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/components.d.ts +0 -71
  126. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/imports.d.ts +0 -386
  127. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/middleware.d.ts +0 -11
  128. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/modules.d.ts +0 -79
  129. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-layouts.d.ts +0 -17
  130. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-nuxt.d.ts +0 -64
  131. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/plugins.d.ts +0 -29
  132. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/runtime-config.d.ts +0 -32
  133. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/shared-imports.d.ts +0 -10
  134. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/vue-shim.d.ts +0 -0
  135. 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.find(".dashboard-stats-grid-wrapper")).toBeTruthy();
51
- expect(wrapper.find(".dashboard-stats-grid")).toBeTruthy();
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")).toBeTruthy();
58
- expect(wrapper.find(".top-row-slot-1")).toBeTruthy();
59
- expect(wrapper.find(".top-row-slot-2")).toBeTruthy();
60
- expect(wrapper.find(".top-row-slot-3")).toBeTruthy();
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")).toBeTruthy();
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")).toBeTruthy();
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")).toBeTruthy();
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")).toBeTruthy();
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")).toBeTruthy();
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")).toBeTruthy();
106
+ expect(wrapper.find(".dashboard-stats-grid").exists()).toBe(true);
120
107
  });
121
108
 
122
- it("applies element classes to wrapper", async () => {
123
- mockElementClasses.value = "test-class another-class";
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
- await createWrapper();
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
- // Check that the component renders
128
- expect(wrapper.find(".dashboard-stats-grid")).toBeTruthy();
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")).toBeTruthy();
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")).toBeTruthy();
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")).toBeTruthy();
298
- expect(wrapper.find(".top-row")).toBeTruthy();
299
- expect(wrapper.find(".bottom-row")).toBeTruthy();
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")).toBeTruthy();
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")).toBeTruthy();
363
- expect(wrapper.find(".top-row")).toBeTruthy();
364
- expect(wrapper.find(".bottom-row")).toBeTruthy();
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")).toBeTruthy();
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")).toBeTruthy();
440
- expect(wrapper.find(".top-row")).toBeTruthy();
441
- expect(wrapper.find(".bottom-row")).toBeTruthy();
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
 
@@ -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;">🙂 👍🏽 👨‍👩‍👧 &lt;b&gt;Not bold&lt;/b&gt;</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 mountSuspended(LayoutGridByCols);
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 mountSuspended(LayoutGridByCols, {
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 mountSuspended(LayoutGridByCols, {
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 mountSuspended(LayoutGridByCols, {
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 mountSuspended(LayoutGridByCols);
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 mountSuspended(LayoutGridByCols, {
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 mountSuspended(LayoutGridByCols);
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 mountSuspended(LayoutGridByCols);
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 mountSuspended(LayoutGridByCols, {
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 mountSuspended(LayoutGridByCols, {
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 mountSuspended(LayoutGridByCols, {
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 mountSuspended(LayoutGridByCols);
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 mountSuspended(LayoutGridByCols, {
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 mountSuspended(LayoutGridByCols, {
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 mountSuspended(LayoutGridByCols);
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 mountSuspended(LayoutGridByCols, {
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 mountSuspended(LayoutGridByCols, {
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 mountSuspended(LayoutGridByCols, {
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 mountSuspended(LayoutGridByCols, {
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 mountSuspended(LayoutGridByCols, {
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 mountSuspended(LayoutGridByCols, {
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 mountSuspended(LayoutGridByCols, {
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, { 'is-responsive': isResponsive }]">
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
- &:not(.is-responsive) {
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
- &.is-responsive {
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.is-responsive` | `is-responsive` prop is `true` | Column minimum steps small → default → large via `@container` queries at 768px and 1024px |
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;">🙂 👍🏽 👨‍👩‍👧 &lt;b&gt;Not bold&lt;/b&gt;</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
+ };