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.
- package/.claude/rules/consumer/ids-from-useid.md +3 -2
- package/.claude/skills/component-compliance-checklist.md +5 -0
- 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/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/index.md +12 -12
- package/.claude/skills/storybook-add-story.md +25 -0
- package/.claude/skills/testing-add-unit-test.md +26 -0
- 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/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/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/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/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/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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<component :is="tag" class="masonry-grid" :class="[elementClasses]">
|
|
3
|
-
<div ref="gridWrapper" class="masonry-grid-wrapper" :
|
|
4
|
-
<div v-for="name in slotNames" :key="name"
|
|
3
|
+
<div ref="gridWrapper" class="masonry-grid-wrapper" :data-multiple-cols="isSingleColumn ? undefined : ''">
|
|
4
|
+
<div v-for="name in slotNames()" :key="name" class="masonry-grid-item">
|
|
5
5
|
<slot :name="name"></slot>
|
|
6
6
|
</div>
|
|
7
7
|
</div>
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
</template>
|
|
10
10
|
|
|
11
11
|
<script setup lang="ts">
|
|
12
|
-
import { useElementSize } from "@vueuse/core";
|
|
12
|
+
import { useElementSize, useResizeObserver } from "@vueuse/core";
|
|
13
13
|
|
|
14
14
|
interface Props {
|
|
15
15
|
tag?: "div" | "section" | "article" | "main";
|
|
@@ -37,14 +37,15 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
37
37
|
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
38
38
|
|
|
39
39
|
const slots = useSlots();
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
// measured and packed in, so reading order is always preserved: unlike a CSS `columns`-based
|
|
43
|
-
// masonry, this never needs to reorder the DOM to keep visual and reading order in sync.
|
|
44
|
-
const slotNames = computed(() => Object.keys(slots));
|
|
40
|
+
// Slot names in authoring order; a function, not a computed, because slots aren't reactive.
|
|
41
|
+
const slotNames = () => Object.keys(slots);
|
|
45
42
|
|
|
46
43
|
const gridWrapper = ref<HTMLElement | null>(null);
|
|
47
|
-
|
|
44
|
+
// Read from the DOM: a v-for ref array isn't guaranteed to follow DOM order, and packing depends on it.
|
|
45
|
+
const gridItems = shallowRef<HTMLElement[]>([]);
|
|
46
|
+
const readItems = () => {
|
|
47
|
+
gridItems.value = Array.from(gridWrapper.value?.children ?? []) as HTMLElement[];
|
|
48
|
+
};
|
|
48
49
|
// Resize-reactive — useElementSize wires up its own ResizeObserver internally.
|
|
49
50
|
const { width } = useElementSize(gridWrapper);
|
|
50
51
|
|
|
@@ -63,13 +64,13 @@ const itemMinWidthPx = computed(() => `${props.itemMinWidth}px`);
|
|
|
63
64
|
// order. Because placement follows DOM order directly, no reordering is ever needed here.
|
|
64
65
|
function updateGrid() {
|
|
65
66
|
if (gridWrapper.value === null || columnCount.value <= 1) {
|
|
66
|
-
|
|
67
|
-
item?.style.removeProperty("--
|
|
68
|
-
item?.style.removeProperty("--
|
|
69
|
-
item?.style.removeProperty("--
|
|
70
|
-
item?.style.removeProperty("--
|
|
67
|
+
gridItems.value.forEach((item) => {
|
|
68
|
+
item?.style.removeProperty("--_masonry-grid-item-position");
|
|
69
|
+
item?.style.removeProperty("--_masonry-grid-item-top");
|
|
70
|
+
item?.style.removeProperty("--_masonry-grid-item-left");
|
|
71
|
+
item?.style.removeProperty("--_masonry-grid-item-width");
|
|
71
72
|
});
|
|
72
|
-
gridWrapper.value?.style.removeProperty("--
|
|
73
|
+
gridWrapper.value?.style.removeProperty("--_masonry-grid-height");
|
|
73
74
|
return;
|
|
74
75
|
}
|
|
75
76
|
|
|
@@ -92,30 +93,47 @@ function updateGrid() {
|
|
|
92
93
|
|
|
93
94
|
const colHeights = Array(columnCount.value).fill(0);
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
gridItems.value.forEach((item) => {
|
|
96
97
|
const minHeight = Math.min(...colHeights);
|
|
97
98
|
const minIndex = colHeights.indexOf(minHeight);
|
|
98
99
|
|
|
99
|
-
item?.style.setProperty("--
|
|
100
|
-
item?.style.setProperty("--
|
|
101
|
-
item?.style.setProperty("--
|
|
102
|
-
item?.style.setProperty("--
|
|
100
|
+
item?.style.setProperty("--_masonry-grid-item-position", "absolute");
|
|
101
|
+
item?.style.setProperty("--_masonry-grid-item-top", `${minHeight}px`);
|
|
102
|
+
item?.style.setProperty("--_masonry-grid-item-left", `${startX + minIndex * (itemWidth + props.gap)}px`);
|
|
103
|
+
item?.style.setProperty("--_masonry-grid-item-width", `${itemWidth}px`);
|
|
103
104
|
|
|
104
105
|
colHeights[minIndex] += Math.floor((item?.offsetHeight ?? 0) + props.gap);
|
|
105
106
|
});
|
|
106
107
|
|
|
107
108
|
const maxHeight = Math.max(...colHeights);
|
|
108
|
-
gridWrapper.value.style.setProperty("--
|
|
109
|
+
gridWrapper.value.style.setProperty("--_masonry-grid-height", `${maxHeight}px`);
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
// Re-pack whenever the measured width changes (useElementSize already wires up its own
|
|
112
113
|
// ResizeObserver — a second explicit useResizeObserver call here would just duplicate it).
|
|
114
|
+
let frame = 0;
|
|
115
|
+
const scheduleUpdate = () => {
|
|
116
|
+
cancelAnimationFrame(frame);
|
|
117
|
+
frame = requestAnimationFrame(() => updateGrid());
|
|
118
|
+
};
|
|
119
|
+
|
|
113
120
|
watch(width, () => nextTick(() => updateGrid()));
|
|
114
121
|
|
|
122
|
+
// A tile that changes height (an image loading, text wrapping) re-packs without a width change.
|
|
123
|
+
useResizeObserver(() => gridItems.value, scheduleUpdate);
|
|
124
|
+
|
|
115
125
|
onMounted(() => {
|
|
126
|
+
readItems();
|
|
116
127
|
nextTick(() => updateGrid());
|
|
117
128
|
});
|
|
118
129
|
|
|
130
|
+
onUpdated(() => {
|
|
131
|
+
readItems();
|
|
132
|
+
scheduleUpdate();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
onBeforeUnmount(() => cancelAnimationFrame(frame));
|
|
136
|
+
|
|
119
137
|
watch(
|
|
120
138
|
() => [props.fixedWidth, props.justify, props.itemMinWidth, props.gap],
|
|
121
139
|
() => updateGrid()
|
|
@@ -142,30 +160,32 @@ watch(
|
|
|
142
160
|
(minmax(itemMinWidth, 1fr)) only ever hurt: on a narrow container it fights the container
|
|
143
161
|
for space instead of just filling it. itemMinWidth starts governing column width once
|
|
144
162
|
there's an actual choice to make, from two columns up (below). */
|
|
145
|
-
grid-template-columns: 1fr;
|
|
163
|
+
grid-template-columns: minmax(0, 1fr);
|
|
146
164
|
|
|
147
|
-
height: var(--
|
|
165
|
+
height: var(--_masonry-grid-height, auto);
|
|
148
166
|
|
|
149
167
|
@container (min-width: 768px) {
|
|
150
|
-
grid-template-columns: repeat(2, minmax(v-bind(itemMinWidthPx), 1fr));
|
|
168
|
+
grid-template-columns: repeat(2, minmax(min(100%, v-bind(itemMinWidthPx)), 1fr));
|
|
151
169
|
}
|
|
152
170
|
@container (min-width: 1024px) {
|
|
153
|
-
grid-template-columns: repeat(3, minmax(v-bind(itemMinWidthPx), 1fr));
|
|
171
|
+
grid-template-columns: repeat(3, minmax(min(100%, v-bind(itemMinWidthPx)), 1fr));
|
|
154
172
|
}
|
|
155
173
|
@container (min-width: 1280px) {
|
|
156
|
-
grid-template-columns: repeat(4, minmax(v-bind(itemMinWidthPx), 1fr));
|
|
174
|
+
grid-template-columns: repeat(4, minmax(min(100%, v-bind(itemMinWidthPx)), 1fr));
|
|
157
175
|
}
|
|
158
176
|
|
|
159
177
|
.masonry-grid-item {
|
|
178
|
+
min-inline-size: 0;
|
|
179
|
+
overflow-wrap: anywhere;
|
|
160
180
|
outline: 0.1rem solid var(--masonry-grid-item-border-colour, var(--theme-border));
|
|
161
181
|
padding: var(--masonry-grid-item-padding, 1.2rem);
|
|
162
182
|
}
|
|
163
183
|
|
|
164
|
-
|
|
165
|
-
position: var(--
|
|
166
|
-
top: var(--
|
|
167
|
-
left: var(--
|
|
168
|
-
width: var(--
|
|
184
|
+
&[data-multiple-cols] .masonry-grid-item {
|
|
185
|
+
position: var(--_masonry-grid-item-position, static);
|
|
186
|
+
top: var(--_masonry-grid-item-top, auto);
|
|
187
|
+
left: var(--_masonry-grid-item-left, auto);
|
|
188
|
+
width: var(--_masonry-grid-item-width, auto);
|
|
169
189
|
|
|
170
190
|
@media (prefers-reduced-motion: no-preference) {
|
|
171
191
|
transition:
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { ref } from "vue";
|
|
1
2
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
3
|
+
import type { MediaCanvas } from "~/types/components";
|
|
4
|
+
import CanvasSwitcher from "../../../canvas-switcher/CanvasSwitcher.vue";
|
|
2
5
|
import MasonryGrid from "../MasonryGrid.vue";
|
|
3
6
|
|
|
4
7
|
interface MasonryGridArgs {
|
|
@@ -197,3 +200,48 @@ export const TwoItems: StoryFn<MasonryGridArgs> = (args) => ({
|
|
|
197
200
|
`,
|
|
198
201
|
});
|
|
199
202
|
TwoItems.storyName = "Two Items (no count prop needed)";
|
|
203
|
+
|
|
204
|
+
export const StressTest: StoryFn = () => ({
|
|
205
|
+
components: { MasonryGrid },
|
|
206
|
+
template: `
|
|
207
|
+
<MasonryGrid :item-min-width="220" :gap="12">
|
|
208
|
+
<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>
|
|
209
|
+
<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>
|
|
210
|
+
<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>
|
|
211
|
+
<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>
|
|
212
|
+
<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>
|
|
213
|
+
<template #item-6><div style="padding: 1.2rem; background: var(--theme-surface-subtle); outline: 1px dashed currentColor;"></div></template>
|
|
214
|
+
<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>
|
|
215
|
+
<template #item-8><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>
|
|
216
|
+
<template #item-9><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>
|
|
217
|
+
</MasonryGrid>
|
|
218
|
+
`,
|
|
219
|
+
});
|
|
220
|
+
StressTest.storyName = "Stress Test (Worst-Case Data)";
|
|
221
|
+
StressTest.decorators = [
|
|
222
|
+
(story, context) => ({
|
|
223
|
+
components: { story, CanvasSwitcher },
|
|
224
|
+
setup() {
|
|
225
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "mobileCanvas");
|
|
226
|
+
return { canvasName };
|
|
227
|
+
},
|
|
228
|
+
template: `
|
|
229
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
230
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
231
|
+
</div>
|
|
232
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
233
|
+
<story />
|
|
234
|
+
</div>
|
|
235
|
+
`,
|
|
236
|
+
}),
|
|
237
|
+
];
|
|
238
|
+
StressTest.parameters = {
|
|
239
|
+
layout: "fullscreen",
|
|
240
|
+
docs: {
|
|
241
|
+
story: { inline: false, height: "720px" },
|
|
242
|
+
description: {
|
|
243
|
+
story:
|
|
244
|
+
"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. When a tile changes height after the first layout (a late-loading font or image, or text re-wrapping), the tiles below it re-pack instead of overlapping; until 2026-10-21 only a container width change re-packed.",
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
-
import { nextTick } from "vue";
|
|
3
|
+
import { defineComponent, h, nextTick, ref } from "vue";
|
|
4
4
|
import MasonryGrid from "../MasonryGrid.vue";
|
|
5
5
|
|
|
6
6
|
// useElementSize/useResizeObserver (from @vueuse/core) require ResizeObserver, which jsdom
|
|
@@ -9,9 +9,9 @@ import MasonryGrid from "../MasonryGrid.vue";
|
|
|
9
9
|
// way to feed the component a non-zero measured width.
|
|
10
10
|
// Vitest 4: a mock's implementation must be a regular function (not an arrow function)
|
|
11
11
|
// to remain constructible via `new`.
|
|
12
|
-
|
|
12
|
+
const resizeCallbacks: ResizeObserverCallback[] = [];
|
|
13
13
|
const mockResizeObserver = vi.fn(function (callback: ResizeObserverCallback) {
|
|
14
|
-
|
|
14
|
+
resizeCallbacks.push(callback);
|
|
15
15
|
return {
|
|
16
16
|
observe: vi.fn(),
|
|
17
17
|
unobserve: vi.fn(),
|
|
@@ -20,16 +20,18 @@ const mockResizeObserver = vi.fn(function (callback: ResizeObserverCallback) {
|
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
beforeEach(() => {
|
|
23
|
-
|
|
23
|
+
resizeCallbacks.length = 0;
|
|
24
24
|
vi.stubGlobal("ResizeObserver", mockResizeObserver);
|
|
25
25
|
// ⚠️ Do NOT call vi.unstubAllGlobals() in afterEach — it removes global stubs from
|
|
26
26
|
// vitest.setup.ts ($fetch, etc.)
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
function simulateResizeTo(width: number) {
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
resizeCallbacks.forEach((callback) =>
|
|
31
|
+
callback(
|
|
32
|
+
[{ contentBoxSize: [{ inlineSize: width, blockSize: 0 }], contentRect: { width } } as unknown as ResizeObserverEntry],
|
|
32
33
|
{} as ResizeObserver
|
|
34
|
+
)
|
|
33
35
|
);
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -37,26 +39,39 @@ function stubOffsetHeight(el: Element, height: number) {
|
|
|
37
39
|
Object.defineProperty(el, "offsetHeight", { value: height, configurable: true });
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
const mounted: Awaited<ReturnType<typeof mountSuspended>>[] = [];
|
|
43
|
+
|
|
44
|
+
const mountGrid = async (options: Parameters<typeof mountSuspended<typeof MasonryGrid>>[1] = {}) => {
|
|
45
|
+
const wrapper = await mountSuspended(MasonryGrid, options);
|
|
46
|
+
mounted.push(wrapper);
|
|
47
|
+
return wrapper;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
mounted.splice(0).forEach((wrapper) => wrapper.unmount());
|
|
52
|
+
vi.restoreAllMocks();
|
|
53
|
+
});
|
|
54
|
+
|
|
40
55
|
describe("MasonryGrid", () => {
|
|
41
56
|
it("mounts without error", async () => {
|
|
42
|
-
const wrapper = await
|
|
57
|
+
const wrapper = await mountGrid();
|
|
43
58
|
expect(wrapper.vm).toBeTruthy();
|
|
44
59
|
});
|
|
45
60
|
|
|
46
61
|
it("renders as div by default", async () => {
|
|
47
|
-
const wrapper = await
|
|
62
|
+
const wrapper = await mountGrid();
|
|
48
63
|
expect(wrapper.element.tagName).toBe("DIV");
|
|
49
64
|
});
|
|
50
65
|
|
|
51
66
|
it("renders a different tag when provided", async () => {
|
|
52
|
-
const wrapper = await
|
|
67
|
+
const wrapper = await mountGrid({
|
|
53
68
|
props: { tag: "section" },
|
|
54
69
|
});
|
|
55
70
|
expect(wrapper.element.tagName).toBe("SECTION");
|
|
56
71
|
});
|
|
57
72
|
|
|
58
73
|
it("renders slot content in natural authoring order", async () => {
|
|
59
|
-
const wrapper = await
|
|
74
|
+
const wrapper = await mountGrid({
|
|
60
75
|
slots: {
|
|
61
76
|
"item-1": "<div>Item 1</div>",
|
|
62
77
|
"item-2": "<div>Item 2</div>",
|
|
@@ -67,23 +82,23 @@ describe("MasonryGrid", () => {
|
|
|
67
82
|
});
|
|
68
83
|
|
|
69
84
|
it("renders no items when no slots are provided", async () => {
|
|
70
|
-
const wrapper = await
|
|
85
|
+
const wrapper = await mountGrid();
|
|
71
86
|
expect(wrapper.findAll(".masonry-grid-item")).toHaveLength(0);
|
|
72
87
|
});
|
|
73
88
|
|
|
74
89
|
it("does not absolutely position items when only one column fits (single-column fallback)", async () => {
|
|
75
|
-
const wrapper = await
|
|
90
|
+
const wrapper = await mountGrid({
|
|
76
91
|
slots: { "item-1": "<div>Item 1</div>", "item-2": "<div>Item 2</div>" },
|
|
77
92
|
});
|
|
78
93
|
await nextTick();
|
|
79
94
|
await nextTick();
|
|
80
|
-
expect(wrapper.find(".masonry-grid-wrapper").
|
|
95
|
+
expect(wrapper.find(".masonry-grid-wrapper").attributes("data-multiple-cols")).toBeUndefined();
|
|
81
96
|
const item = wrapper.find(".masonry-grid-item").element as HTMLElement;
|
|
82
|
-
expect(item.style.getPropertyValue("--
|
|
97
|
+
expect(item.style.getPropertyValue("--_masonry-grid-item-position")).toBe("");
|
|
83
98
|
});
|
|
84
99
|
|
|
85
100
|
it("packs items into whichever column is currently shortest", async () => {
|
|
86
|
-
const wrapper = await
|
|
101
|
+
const wrapper = await mountGrid({
|
|
87
102
|
// itemMinWidth (300, default) + gap (12, default) = 312 -> floor(700/312) = 2 columns
|
|
88
103
|
slots: {
|
|
89
104
|
"item-1": "<div>Item 1</div>",
|
|
@@ -107,16 +122,16 @@ describe("MasonryGrid", () => {
|
|
|
107
122
|
const el1 = items[0]!.element as HTMLElement;
|
|
108
123
|
const el2 = items[1]!.element as HTMLElement;
|
|
109
124
|
const el3 = items[2]!.element as HTMLElement;
|
|
110
|
-
expect(el1.style.getPropertyValue("--
|
|
111
|
-
expect(el1.style.getPropertyValue("--
|
|
112
|
-
expect(el2.style.getPropertyValue("--
|
|
113
|
-
expect(el2.style.getPropertyValue("--
|
|
114
|
-
expect(el3.style.getPropertyValue("--
|
|
115
|
-
expect(el3.style.getPropertyValue("--
|
|
125
|
+
expect(el1.style.getPropertyValue("--_masonry-grid-item-top")).toBe("0px");
|
|
126
|
+
expect(el1.style.getPropertyValue("--_masonry-grid-item-left")).toBe("0px");
|
|
127
|
+
expect(el2.style.getPropertyValue("--_masonry-grid-item-top")).toBe("0px");
|
|
128
|
+
expect(el2.style.getPropertyValue("--_masonry-grid-item-left")).toBe("356px");
|
|
129
|
+
expect(el3.style.getPropertyValue("--_masonry-grid-item-top")).toBe("62px");
|
|
130
|
+
expect(el3.style.getPropertyValue("--_masonry-grid-item-left")).toBe("356px");
|
|
116
131
|
});
|
|
117
132
|
|
|
118
133
|
it("uses a fixed item width when fixedWidth is set, ignoring stretch sizing", async () => {
|
|
119
|
-
const wrapper = await
|
|
134
|
+
const wrapper = await mountGrid({
|
|
120
135
|
props: { fixedWidth: true, itemMinWidth: 200 },
|
|
121
136
|
slots: { "item-1": "<div>Item 1</div>", "item-2": "<div>Item 2</div>" },
|
|
122
137
|
});
|
|
@@ -124,11 +139,11 @@ describe("MasonryGrid", () => {
|
|
|
124
139
|
await nextTick();
|
|
125
140
|
await nextTick();
|
|
126
141
|
const item = wrapper.find(".masonry-grid-item").element as HTMLElement;
|
|
127
|
-
expect(item.style.getPropertyValue("--
|
|
142
|
+
expect(item.style.getPropertyValue("--_masonry-grid-item-width")).toBe("200px");
|
|
128
143
|
});
|
|
129
144
|
|
|
130
145
|
it("spaces fixed-width columns by exactly itemWidth + gap, and centers the block via justify", async () => {
|
|
131
|
-
const wrapper = await
|
|
146
|
+
const wrapper = await mountGrid({
|
|
132
147
|
props: { fixedWidth: true, itemMinWidth: 200, gap: 20, justify: "center" },
|
|
133
148
|
slots: {
|
|
134
149
|
"item-1": "<div>Item 1</div>",
|
|
@@ -144,15 +159,55 @@ describe("MasonryGrid", () => {
|
|
|
144
159
|
// available -> 30px leftover on each side.
|
|
145
160
|
const items = wrapper.findAll(".masonry-grid-item");
|
|
146
161
|
const [el1, el2, el3] = items.map((item) => item.element as HTMLElement);
|
|
147
|
-
expect(el1!.style.getPropertyValue("--
|
|
148
|
-
expect(el2!.style.getPropertyValue("--
|
|
149
|
-
expect(el3!.style.getPropertyValue("--
|
|
162
|
+
expect(el1!.style.getPropertyValue("--_masonry-grid-item-left")).toBe("30px");
|
|
163
|
+
expect(el2!.style.getPropertyValue("--_masonry-grid-item-left")).toBe("250px");
|
|
164
|
+
expect(el3!.style.getPropertyValue("--_masonry-grid-item-left")).toBe("470px");
|
|
150
165
|
});
|
|
151
166
|
|
|
152
167
|
it("applies styleClassPassthrough", async () => {
|
|
153
|
-
const wrapper = await
|
|
168
|
+
const wrapper = await mountGrid({
|
|
154
169
|
props: { styleClassPassthrough: "custom-class" },
|
|
155
170
|
});
|
|
156
171
|
expect(wrapper.classes()).toContain("custom-class");
|
|
157
172
|
});
|
|
173
|
+
|
|
174
|
+
it("re-packs when a tile changes height without a width change", async () => {
|
|
175
|
+
const wrapper = await mountGrid({
|
|
176
|
+
slots: { "item-1": "<div>1</div>", "item-2": "<div>2</div>", "item-3": "<div>3</div>" },
|
|
177
|
+
});
|
|
178
|
+
const items = wrapper.findAll(".masonry-grid-item");
|
|
179
|
+
stubOffsetHeight(items[0]!.element, 100);
|
|
180
|
+
stubOffsetHeight(items[1]!.element, 50);
|
|
181
|
+
stubOffsetHeight(items[2]!.element, 80);
|
|
182
|
+
simulateResizeTo(700);
|
|
183
|
+
await nextTick();
|
|
184
|
+
await nextTick();
|
|
185
|
+
const el3 = items[2]!.element as HTMLElement;
|
|
186
|
+
expect(el3.style.getPropertyValue("--_masonry-grid-item-left")).toBe("356px");
|
|
187
|
+
|
|
188
|
+
stubOffsetHeight(items[0]!.element, 10);
|
|
189
|
+
resizeCallbacks.at(-1)!([], {} as ResizeObserver);
|
|
190
|
+
vi.advanceTimersByTime(20);
|
|
191
|
+
await nextTick();
|
|
192
|
+
expect(el3.style.getPropertyValue("--_masonry-grid-item-top")).toBe("22px");
|
|
193
|
+
expect(el3.style.getPropertyValue("--_masonry-grid-item-left")).toBe("0px");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("renders a slot that appears after mount", async () => {
|
|
197
|
+
const showSecond = ref(false);
|
|
198
|
+
const Host = defineComponent({
|
|
199
|
+
setup: () => () =>
|
|
200
|
+
h(MasonryGrid, null, {
|
|
201
|
+
"item-1": () => h("div", "First"),
|
|
202
|
+
...(showSecond.value ? { "item-2": () => h("div", "Second") } : {}),
|
|
203
|
+
}),
|
|
204
|
+
});
|
|
205
|
+
const wrapper = await mountSuspended(Host);
|
|
206
|
+
mounted.push(wrapper);
|
|
207
|
+
expect(wrapper.findAll(".masonry-grid-item")).toHaveLength(1);
|
|
208
|
+
|
|
209
|
+
showSecond.value = true;
|
|
210
|
+
await nextTick();
|
|
211
|
+
expect(wrapper.findAll(".masonry-grid-item").map((item) => item.text())).toEqual(["First", "Second"]);
|
|
212
|
+
});
|
|
158
213
|
});
|
|
@@ -27,7 +27,7 @@ computed tracks `--_full-track-min`, `--_full-track`, `--_popout-track`, `--_con
|
|
|
27
27
|
|
|
28
28
|
| Hook | When |
|
|
29
29
|
|---|---|
|
|
30
|
-
| `.page-row
|
|
30
|
+
| `.page-row[data-variant="full"]`, `"popout"`, `"content"`, `"inset-content"` | The `variant` prop (default `content`). Sets the row's own track when nested, and the default track for its direct non-PageRow children |
|
|
31
31
|
| `[data-align="start"]`, `[data-align="end"]` | The `align` prop on a nested row: bleeds to the left or right edge instead of centring |
|
|
32
32
|
|
|
33
33
|
Direct children of a `.page-row` are grid items placed on a named track (`full`, `popout`,
|
|
@@ -35,6 +35,11 @@ Direct children of a `.page-row` are grid items placed on a named track (`full`,
|
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
38
|
+
> Changed 2026-10-21: the variant was a bare class on the root (`.page-row.full`, `.content`, ...);
|
|
39
|
+
> it is now the `data-variant` attribute. A consumer's own `.content` or `.full` CSS no longer
|
|
40
|
+
> matches page rows, and selectors like `.page-row.full` need updating to
|
|
41
|
+
> `.page-row[data-variant="full"]`.
|
|
42
|
+
|
|
38
43
|
## Global theming
|
|
39
44
|
|
|
40
45
|
Create `assets/styles/setup/07.components/page-row.css` in the consuming app and set tokens on
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
:is="tag"
|
|
4
4
|
:id
|
|
5
5
|
class="page-row"
|
|
6
|
-
:class="[elementClasses
|
|
6
|
+
:class="[elementClasses]"
|
|
7
|
+
:data-variant="variant"
|
|
7
8
|
:data-align="align || undefined"
|
|
8
9
|
:aria-labelledby="ariaLabelledby"
|
|
9
10
|
>
|
|
@@ -42,7 +43,7 @@ watch(
|
|
|
42
43
|
/*
|
|
43
44
|
* Page layout grid — https://layout-breakouts-builder.vercel.app
|
|
44
45
|
* Each .page-row is both a CSS grid container and a grid item when nested.
|
|
45
|
-
* Direct children default to the content column; nest a .page-row
|
|
46
|
+
* Direct children default to the content column; nest a .page-row[data-variant]
|
|
46
47
|
* to break out to a wider or narrower track.
|
|
47
48
|
*/
|
|
48
49
|
.page-row {
|
|
@@ -99,13 +100,13 @@ watch(
|
|
|
99
100
|
* its own children span full width, not just control placement when nested.
|
|
100
101
|
* Specificity (0,2,0) — nested .page-row variant overrides below win at (0,3,0).
|
|
101
102
|
*/
|
|
102
|
-
.page-row
|
|
103
|
+
.page-row[data-variant="full"] > * {
|
|
103
104
|
grid-column: full;
|
|
104
105
|
}
|
|
105
|
-
.page-row
|
|
106
|
+
.page-row[data-variant="popout"] > * {
|
|
106
107
|
grid-column: popout;
|
|
107
108
|
}
|
|
108
|
-
.page-row
|
|
109
|
+
.page-row[data-variant="inset-content"] > * {
|
|
109
110
|
grid-column: inset-content;
|
|
110
111
|
}
|
|
111
112
|
|
|
@@ -118,16 +119,16 @@ watch(
|
|
|
118
119
|
.page-row > .page-row {
|
|
119
120
|
align-content: start;
|
|
120
121
|
|
|
121
|
-
|
|
122
|
+
&[data-variant="full"] {
|
|
122
123
|
grid-column: full;
|
|
123
124
|
}
|
|
124
|
-
|
|
125
|
+
&[data-variant="popout"] {
|
|
125
126
|
grid-column: popout;
|
|
126
127
|
}
|
|
127
|
-
|
|
128
|
+
&[data-variant="content"] {
|
|
128
129
|
grid-column: content;
|
|
129
130
|
}
|
|
130
|
-
|
|
131
|
+
&[data-variant="inset-content"] {
|
|
131
132
|
grid-column: inset-content;
|
|
132
133
|
}
|
|
133
134
|
}
|
|
@@ -138,25 +139,25 @@ watch(
|
|
|
138
139
|
* Specificity (0,4,0) beats the symmetric rules above at (0,3,0).
|
|
139
140
|
*/
|
|
140
141
|
.page-row > .page-row[data-align="start"] {
|
|
141
|
-
|
|
142
|
+
&[data-variant="popout"] {
|
|
142
143
|
grid-column: full-start / popout-end;
|
|
143
144
|
}
|
|
144
|
-
|
|
145
|
+
&[data-variant="content"] {
|
|
145
146
|
grid-column: full-start / content-end;
|
|
146
147
|
}
|
|
147
|
-
|
|
148
|
+
&[data-variant="inset-content"] {
|
|
148
149
|
grid-column: full-start / inset-content-end;
|
|
149
150
|
}
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
.page-row > .page-row[data-align="end"] {
|
|
153
|
-
|
|
154
|
+
&[data-variant="popout"] {
|
|
154
155
|
grid-column: popout-start / full-end;
|
|
155
156
|
}
|
|
156
|
-
|
|
157
|
+
&[data-variant="content"] {
|
|
157
158
|
grid-column: content-start / full-end;
|
|
158
159
|
}
|
|
159
|
-
|
|
160
|
+
&[data-variant="inset-content"] {
|
|
160
161
|
grid-column: inset-content-start / full-end;
|
|
161
162
|
}
|
|
162
163
|
}
|
|
@@ -351,3 +351,26 @@ AriaLabelledBy.parameters = {
|
|
|
351
351
|
},
|
|
352
352
|
},
|
|
353
353
|
};
|
|
354
|
+
|
|
355
|
+
export const StressTest: StoryFn = () => ({
|
|
356
|
+
components: { PageRowComponent },
|
|
357
|
+
template: `
|
|
358
|
+
<PageRowComponent variant="full">
|
|
359
|
+
<PageRowComponent variant="popout"><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></PageRowComponent>
|
|
360
|
+
<PageRowComponent variant="content"><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><img src="/images/banners/banner-1.webp" width="1600" height="400" alt="" style="display: block; max-inline-size: 100%; block-size: auto;"></div></PageRowComponent>
|
|
361
|
+
<PageRowComponent variant="inset-content" align="end"><div style="padding: 1.2rem; background: var(--theme-surface-subtle); outline: 1px dashed currentColor;"><p style="margin: 0;">🙂 👍🏽 👨👩👧 <b>Not bold</b></p><p style="margin: 0;" dir="rtl">مرحبا بالعالم، نص طويل من اليمين إلى اليسار</p></div></PageRowComponent>
|
|
362
|
+
<PageRowComponent variant="full"><div style="padding: 1.2rem; background: var(--theme-surface-subtle); outline: 1px dashed currentColor;"><p style="margin: 0;">x</p></div></PageRowComponent>
|
|
363
|
+
</PageRowComponent>
|
|
364
|
+
`,
|
|
365
|
+
});
|
|
366
|
+
StressTest.storyName = "Stress Test (Worst-Case Data)";
|
|
367
|
+
StressTest.parameters = {
|
|
368
|
+
layout: "fullscreen",
|
|
369
|
+
docs: {
|
|
370
|
+
story: { inline: false, height: "720px" },
|
|
371
|
+
description: {
|
|
372
|
+
story:
|
|
373
|
+
"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. Each nested row uses a different variant; every one keeps its track instead of pushing the page wider.",
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
};
|
|
@@ -23,6 +23,7 @@ describe("PageRow", () => {
|
|
|
23
23
|
|
|
24
24
|
afterEach(() => {
|
|
25
25
|
wrapper?.unmount();
|
|
26
|
+
vi.restoreAllMocks();
|
|
26
27
|
});
|
|
27
28
|
|
|
28
29
|
describe("Rendering", () => {
|
|
@@ -62,12 +63,12 @@ describe("PageRow", () => {
|
|
|
62
63
|
describe("Props — variant", () => {
|
|
63
64
|
it("applies content class by default", async () => {
|
|
64
65
|
await createWrapper();
|
|
65
|
-
expect(wrapper.
|
|
66
|
+
expect(wrapper.attributes("data-variant")).toBe("content");
|
|
66
67
|
});
|
|
67
68
|
|
|
68
69
|
it.each(["full", "popout", "content", "inset-content"])("applies variant class %s", async (variant) => {
|
|
69
70
|
await createWrapper({ variant });
|
|
70
|
-
expect(wrapper.
|
|
71
|
+
expect(wrapper.attributes("data-variant")).toBe(variant);
|
|
71
72
|
});
|
|
72
73
|
});
|
|
73
74
|
|
|
@@ -121,7 +122,7 @@ describe("PageRow", () => {
|
|
|
121
122
|
|
|
122
123
|
it("combines variant and align on the same element", async () => {
|
|
123
124
|
await createWrapper({ variant: "content", align: "start" });
|
|
124
|
-
expect(wrapper.
|
|
125
|
+
expect(wrapper.attributes("data-variant")).toBe("content");
|
|
125
126
|
expect(wrapper.attributes("data-align")).toBe("start");
|
|
126
127
|
});
|
|
127
128
|
});
|
|
@@ -199,13 +200,13 @@ describe("PageRow", () => {
|
|
|
199
200
|
it("includes both base class and variant class", async () => {
|
|
200
201
|
await createWrapper({ variant: "full" });
|
|
201
202
|
expect(wrapper.classes()).toContain("page-row");
|
|
202
|
-
expect(wrapper.
|
|
203
|
+
expect(wrapper.attributes("data-variant")).toBe("full");
|
|
203
204
|
});
|
|
204
205
|
|
|
205
206
|
it("includes elementClasses from styleClassPassthrough alongside other classes", async () => {
|
|
206
207
|
await createWrapper({ variant: "popout", styleClassPassthrough: ["extra-class"] });
|
|
207
208
|
expect(wrapper.classes()).toContain("page-row");
|
|
208
|
-
expect(wrapper.
|
|
209
|
+
expect(wrapper.attributes("data-variant")).toBe("popout");
|
|
209
210
|
expect(wrapper.classes()).toContain("extra-class");
|
|
210
211
|
});
|
|
211
212
|
});
|