@wordpress/grid 0.1.1-next.v.202606191442.0
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/CHANGELOG.md +33 -0
- package/LICENSE.md +788 -0
- package/README.md +534 -0
- package/build/dashboard-grid/grid-item.cjs +308 -0
- package/build/dashboard-grid/grid-item.cjs.map +7 -0
- package/build/dashboard-grid/index.cjs +591 -0
- package/build/dashboard-grid/index.cjs.map +7 -0
- package/build/dashboard-grid/resolve-fill-widths.cjs +189 -0
- package/build/dashboard-grid/resolve-fill-widths.cjs.map +7 -0
- package/build/dashboard-grid/types.cjs +19 -0
- package/build/dashboard-grid/types.cjs.map +7 -0
- package/build/dashboard-lanes/index.cjs +558 -0
- package/build/dashboard-lanes/index.cjs.map +7 -0
- package/build/dashboard-lanes/lane-placement.cjs +110 -0
- package/build/dashboard-lanes/lane-placement.cjs.map +7 -0
- package/build/dashboard-lanes/lanes-item.cjs +295 -0
- package/build/dashboard-lanes/lanes-item.cjs.map +7 -0
- package/build/dashboard-lanes/types.cjs +19 -0
- package/build/dashboard-lanes/types.cjs.map +7 -0
- package/build/dashboard-lanes/use-lane-placement.cjs +206 -0
- package/build/dashboard-lanes/use-lane-placement.cjs.map +7 -0
- package/build/index.cjs +34 -0
- package/build/index.cjs.map +7 -0
- package/build/shared/drag-overlay-drop-animation.cjs +70 -0
- package/build/shared/drag-overlay-drop-animation.cjs.map +7 -0
- package/build/shared/grid-item-key.cjs +31 -0
- package/build/shared/grid-item-key.cjs.map +7 -0
- package/build/shared/grid-overlay.cjs +187 -0
- package/build/shared/grid-overlay.cjs.map +7 -0
- package/build/shared/item-exit-overlay.cjs +150 -0
- package/build/shared/item-exit-overlay.cjs.map +7 -0
- package/build/shared/resize-handle.cjs +224 -0
- package/build/shared/resize-handle.cjs.map +7 -0
- package/build/shared/resize-snap.cjs +47 -0
- package/build/shared/resize-snap.cjs.map +7 -0
- package/build/shared/types.cjs +19 -0
- package/build/shared/types.cjs.map +7 -0
- package/build/shared/use-item-exit-animation.cjs +148 -0
- package/build/shared/use-item-exit-animation.cjs.map +7 -0
- package/build/shared/use-layout-shift-animation.cjs +167 -0
- package/build/shared/use-layout-shift-animation.cjs.map +7 -0
- package/build-module/dashboard-grid/grid-item.mjs +273 -0
- package/build-module/dashboard-grid/grid-item.mjs.map +7 -0
- package/build-module/dashboard-grid/index.mjs +579 -0
- package/build-module/dashboard-grid/index.mjs.map +7 -0
- package/build-module/dashboard-grid/resolve-fill-widths.mjs +164 -0
- package/build-module/dashboard-grid/resolve-fill-widths.mjs.map +7 -0
- package/build-module/dashboard-grid/types.mjs +1 -0
- package/build-module/dashboard-grid/types.mjs.map +7 -0
- package/build-module/dashboard-lanes/index.mjs +547 -0
- package/build-module/dashboard-lanes/index.mjs.map +7 -0
- package/build-module/dashboard-lanes/lane-placement.mjs +85 -0
- package/build-module/dashboard-lanes/lane-placement.mjs.map +7 -0
- package/build-module/dashboard-lanes/lanes-item.mjs +260 -0
- package/build-module/dashboard-lanes/lanes-item.mjs.map +7 -0
- package/build-module/dashboard-lanes/types.mjs +1 -0
- package/build-module/dashboard-lanes/types.mjs.map +7 -0
- package/build-module/dashboard-lanes/use-lane-placement.mjs +181 -0
- package/build-module/dashboard-lanes/use-lane-placement.mjs.map +7 -0
- package/build-module/index.mjs +8 -0
- package/build-module/index.mjs.map +7 -0
- package/build-module/shared/drag-overlay-drop-animation.mjs +47 -0
- package/build-module/shared/drag-overlay-drop-animation.mjs.map +7 -0
- package/build-module/shared/grid-item-key.mjs +6 -0
- package/build-module/shared/grid-item-key.mjs.map +7 -0
- package/build-module/shared/grid-overlay.mjs +152 -0
- package/build-module/shared/grid-overlay.mjs.map +7 -0
- package/build-module/shared/item-exit-overlay.mjs +125 -0
- package/build-module/shared/item-exit-overlay.mjs.map +7 -0
- package/build-module/shared/resize-handle.mjs +193 -0
- package/build-module/shared/resize-handle.mjs.map +7 -0
- package/build-module/shared/resize-snap.mjs +21 -0
- package/build-module/shared/resize-snap.mjs.map +7 -0
- package/build-module/shared/types.mjs +1 -0
- package/build-module/shared/types.mjs.map +7 -0
- package/build-module/shared/use-item-exit-animation.mjs +128 -0
- package/build-module/shared/use-item-exit-animation.mjs.map +7 -0
- package/build-module/shared/use-layout-shift-animation.mjs +140 -0
- package/build-module/shared/use-layout-shift-animation.mjs.map +7 -0
- package/build-types/dashboard-grid/grid-item.d.ts +3 -0
- package/build-types/dashboard-grid/grid-item.d.ts.map +1 -0
- package/build-types/dashboard-grid/index.d.ts +35 -0
- package/build-types/dashboard-grid/index.d.ts.map +1 -0
- package/build-types/dashboard-grid/resolve-fill-widths.d.ts +26 -0
- package/build-types/dashboard-grid/resolve-fill-widths.d.ts.map +1 -0
- package/build-types/dashboard-grid/stories/index.story.d.ts +98 -0
- package/build-types/dashboard-grid/stories/index.story.d.ts.map +1 -0
- package/build-types/dashboard-grid/types.d.ts +232 -0
- package/build-types/dashboard-grid/types.d.ts.map +1 -0
- package/build-types/dashboard-lanes/index.d.ts +40 -0
- package/build-types/dashboard-lanes/index.d.ts.map +1 -0
- package/build-types/dashboard-lanes/lane-placement.d.ts +126 -0
- package/build-types/dashboard-lanes/lane-placement.d.ts.map +1 -0
- package/build-types/dashboard-lanes/lanes-item.d.ts +52 -0
- package/build-types/dashboard-lanes/lanes-item.d.ts.map +1 -0
- package/build-types/dashboard-lanes/stories/index.story.d.ts +64 -0
- package/build-types/dashboard-lanes/stories/index.story.d.ts.map +1 -0
- package/build-types/dashboard-lanes/types.d.ts +151 -0
- package/build-types/dashboard-lanes/types.d.ts.map +1 -0
- package/build-types/dashboard-lanes/use-lane-placement.d.ts +74 -0
- package/build-types/dashboard-lanes/use-lane-placement.d.ts.map +1 -0
- package/build-types/index.d.ts +6 -0
- package/build-types/index.d.ts.map +1 -0
- package/build-types/shared/drag-overlay-drop-animation.d.ts +13 -0
- package/build-types/shared/drag-overlay-drop-animation.d.ts.map +1 -0
- package/build-types/shared/grid-item-key.d.ts +6 -0
- package/build-types/shared/grid-item-key.d.ts.map +1 -0
- package/build-types/shared/grid-overlay.d.ts +19 -0
- package/build-types/shared/grid-overlay.d.ts.map +1 -0
- package/build-types/shared/item-exit-overlay.d.ts +20 -0
- package/build-types/shared/item-exit-overlay.d.ts.map +1 -0
- package/build-types/shared/resize-handle.d.ts +23 -0
- package/build-types/shared/resize-handle.d.ts.map +1 -0
- package/build-types/shared/resize-snap.d.ts +41 -0
- package/build-types/shared/resize-snap.d.ts.map +1 -0
- package/build-types/shared/types.d.ts +144 -0
- package/build-types/shared/types.d.ts.map +1 -0
- package/build-types/shared/use-item-exit-animation.d.ts +37 -0
- package/build-types/shared/use-item-exit-animation.d.ts.map +1 -0
- package/build-types/shared/use-layout-shift-animation.d.ts +77 -0
- package/build-types/shared/use-layout-shift-animation.d.ts.map +1 -0
- package/package.json +80 -0
- package/src/dashboard-grid/grid-item.module.css +94 -0
- package/src/dashboard-grid/grid-item.tsx +205 -0
- package/src/dashboard-grid/grid.module.css +134 -0
- package/src/dashboard-grid/index.tsx +713 -0
- package/src/dashboard-grid/resolve-fill-widths.ts +224 -0
- package/src/dashboard-grid/stories/index.story.tsx +930 -0
- package/src/dashboard-grid/test/keyboard-activation.test.tsx +76 -0
- package/src/dashboard-grid/test/resolve-fill-widths.test.ts +250 -0
- package/src/dashboard-grid/types.ts +271 -0
- package/src/dashboard-lanes/index.tsx +629 -0
- package/src/dashboard-lanes/lane-placement.ts +245 -0
- package/src/dashboard-lanes/lanes-item.module.css +93 -0
- package/src/dashboard-lanes/lanes-item.tsx +236 -0
- package/src/dashboard-lanes/lanes.module.css +152 -0
- package/src/dashboard-lanes/stories/index.story.tsx +518 -0
- package/src/dashboard-lanes/test/keyboard-activation.test.tsx +71 -0
- package/src/dashboard-lanes/test/lane-placement.test.ts +442 -0
- package/src/dashboard-lanes/test/use-lane-placement.test.tsx +358 -0
- package/src/dashboard-lanes/types.ts +176 -0
- package/src/dashboard-lanes/use-lane-placement.ts +313 -0
- package/src/index.ts +17 -0
- package/src/shared/actionable-area-slot.module.css +16 -0
- package/src/shared/drag-overlay-drop-animation.ts +66 -0
- package/src/shared/grid-item-key.ts +5 -0
- package/src/shared/grid-overlay.module.css +82 -0
- package/src/shared/grid-overlay.tsx +93 -0
- package/src/shared/item-exit-animation.module.css +49 -0
- package/src/shared/item-exit-overlay.tsx +57 -0
- package/src/shared/layout-shift-animation.module.css +16 -0
- package/src/shared/resize-handle.module.css +88 -0
- package/src/shared/resize-handle.tsx +163 -0
- package/src/shared/resize-snap.ts +63 -0
- package/src/shared/test/resize-snap.test.ts +35 -0
- package/src/shared/types.ts +164 -0
- package/src/shared/use-item-exit-animation.ts +199 -0
- package/src/shared/use-layout-shift-animation.ts +284 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { computeLanePlacements } from '../lane-placement';
|
|
5
|
+
import type { LanePlacementItem } from '../lane-placement';
|
|
6
|
+
|
|
7
|
+
function items(
|
|
8
|
+
defs: Array< {
|
|
9
|
+
key: string;
|
|
10
|
+
span?: number;
|
|
11
|
+
height: number;
|
|
12
|
+
lane?: number;
|
|
13
|
+
} >
|
|
14
|
+
): LanePlacementItem[] {
|
|
15
|
+
return defs.map( ( def ) => ( {
|
|
16
|
+
key: def.key,
|
|
17
|
+
span: def.span ?? 1,
|
|
18
|
+
height: def.height,
|
|
19
|
+
...( def.lane !== undefined ? { lane: def.lane } : {} ),
|
|
20
|
+
} ) );
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe( 'computeLanePlacements', () => {
|
|
24
|
+
describe( 'empty and trivial inputs', () => {
|
|
25
|
+
it( 'returns an empty map and zero height for no items', () => {
|
|
26
|
+
const result = computeLanePlacements( {
|
|
27
|
+
items: [],
|
|
28
|
+
lanes: 4,
|
|
29
|
+
gap: 16,
|
|
30
|
+
flowTolerance: 0,
|
|
31
|
+
} );
|
|
32
|
+
expect( result.placements.size ).toBe( 0 );
|
|
33
|
+
expect( result.totalHeight ).toBe( 0 );
|
|
34
|
+
} );
|
|
35
|
+
|
|
36
|
+
it( 'places a single item at lane 0, top 0', () => {
|
|
37
|
+
const result = computeLanePlacements( {
|
|
38
|
+
items: items( [ { key: 'a', height: 100 } ] ),
|
|
39
|
+
lanes: 4,
|
|
40
|
+
gap: 16,
|
|
41
|
+
flowTolerance: 0,
|
|
42
|
+
} );
|
|
43
|
+
expect( result.placements.get( 'a' ) ).toEqual( {
|
|
44
|
+
key: 'a',
|
|
45
|
+
lane: 0,
|
|
46
|
+
top: 0,
|
|
47
|
+
span: 1,
|
|
48
|
+
} );
|
|
49
|
+
expect( result.totalHeight ).toBe( 100 );
|
|
50
|
+
} );
|
|
51
|
+
} );
|
|
52
|
+
|
|
53
|
+
describe( 'shortest-lane skyline', () => {
|
|
54
|
+
it( 'fills empty lanes left to right when all are zero', () => {
|
|
55
|
+
const result = computeLanePlacements( {
|
|
56
|
+
items: items( [
|
|
57
|
+
{ key: 'a', height: 100 },
|
|
58
|
+
{ key: 'b', height: 100 },
|
|
59
|
+
{ key: 'c', height: 100 },
|
|
60
|
+
{ key: 'd', height: 100 },
|
|
61
|
+
] ),
|
|
62
|
+
lanes: 4,
|
|
63
|
+
gap: 0,
|
|
64
|
+
flowTolerance: 0,
|
|
65
|
+
} );
|
|
66
|
+
expect( result.placements.get( 'a' )?.lane ).toBe( 0 );
|
|
67
|
+
expect( result.placements.get( 'b' )?.lane ).toBe( 1 );
|
|
68
|
+
expect( result.placements.get( 'c' )?.lane ).toBe( 2 );
|
|
69
|
+
expect( result.placements.get( 'd' )?.lane ).toBe( 3 );
|
|
70
|
+
expect( result.totalHeight ).toBe( 100 );
|
|
71
|
+
} );
|
|
72
|
+
|
|
73
|
+
it( 'sends the next item to the shortest lane', () => {
|
|
74
|
+
// After placing tall A in lane 0 and short B in lane 1, the
|
|
75
|
+
// next item must land in lane 1 (the shortest).
|
|
76
|
+
const result = computeLanePlacements( {
|
|
77
|
+
items: items( [
|
|
78
|
+
{ key: 'a', height: 200 },
|
|
79
|
+
{ key: 'b', height: 50 },
|
|
80
|
+
{ key: 'c', height: 80 },
|
|
81
|
+
] ),
|
|
82
|
+
lanes: 2,
|
|
83
|
+
gap: 0,
|
|
84
|
+
flowTolerance: 0,
|
|
85
|
+
} );
|
|
86
|
+
expect( result.placements.get( 'a' )?.lane ).toBe( 0 );
|
|
87
|
+
expect( result.placements.get( 'b' )?.lane ).toBe( 1 );
|
|
88
|
+
expect( result.placements.get( 'c' )?.lane ).toBe( 1 );
|
|
89
|
+
expect( result.placements.get( 'c' )?.top ).toBe( 50 );
|
|
90
|
+
} );
|
|
91
|
+
|
|
92
|
+
it( 'continues to balance lanes across many items', () => {
|
|
93
|
+
const result = computeLanePlacements( {
|
|
94
|
+
items: items( [
|
|
95
|
+
{ key: 'a', height: 100 },
|
|
96
|
+
{ key: 'b', height: 50 },
|
|
97
|
+
{ key: 'c', height: 30 },
|
|
98
|
+
{ key: 'd', height: 30 },
|
|
99
|
+
{ key: 'e', height: 30 },
|
|
100
|
+
] ),
|
|
101
|
+
lanes: 3,
|
|
102
|
+
gap: 0,
|
|
103
|
+
flowTolerance: 0,
|
|
104
|
+
} );
|
|
105
|
+
// Lanes after each step: a → [100, 0, 0]; b → [100, 50, 0];
|
|
106
|
+
// c → [100, 50, 30]; d → [100, 50, 60]; e → [100, 80, 60].
|
|
107
|
+
expect( result.placements.get( 'a' )?.lane ).toBe( 0 );
|
|
108
|
+
expect( result.placements.get( 'b' )?.lane ).toBe( 1 );
|
|
109
|
+
expect( result.placements.get( 'c' )?.lane ).toBe( 2 );
|
|
110
|
+
expect( result.placements.get( 'd' )?.lane ).toBe( 2 );
|
|
111
|
+
expect( result.placements.get( 'e' )?.lane ).toBe( 1 );
|
|
112
|
+
expect( result.totalHeight ).toBe( 100 );
|
|
113
|
+
} );
|
|
114
|
+
} );
|
|
115
|
+
|
|
116
|
+
describe( 'gap accounting', () => {
|
|
117
|
+
it( 'does not add a gap before the first item in a lane', () => {
|
|
118
|
+
const result = computeLanePlacements( {
|
|
119
|
+
items: items( [ { key: 'a', height: 100 } ] ),
|
|
120
|
+
lanes: 1,
|
|
121
|
+
gap: 16,
|
|
122
|
+
flowTolerance: 0,
|
|
123
|
+
} );
|
|
124
|
+
expect( result.placements.get( 'a' )?.top ).toBe( 0 );
|
|
125
|
+
} );
|
|
126
|
+
|
|
127
|
+
it( 'adds a single gap between stacked items', () => {
|
|
128
|
+
const result = computeLanePlacements( {
|
|
129
|
+
items: items( [
|
|
130
|
+
{ key: 'a', height: 100 },
|
|
131
|
+
{ key: 'b', height: 50 },
|
|
132
|
+
] ),
|
|
133
|
+
lanes: 1,
|
|
134
|
+
gap: 16,
|
|
135
|
+
flowTolerance: 0,
|
|
136
|
+
} );
|
|
137
|
+
expect( result.placements.get( 'a' )?.top ).toBe( 0 );
|
|
138
|
+
expect( result.placements.get( 'b' )?.top ).toBe( 116 );
|
|
139
|
+
expect( result.totalHeight ).toBe( 166 );
|
|
140
|
+
} );
|
|
141
|
+
|
|
142
|
+
it( 'tracks gaps independently per lane', () => {
|
|
143
|
+
const result = computeLanePlacements( {
|
|
144
|
+
items: items( [
|
|
145
|
+
{ key: 'a', height: 100 },
|
|
146
|
+
{ key: 'b', height: 100 },
|
|
147
|
+
{ key: 'c', height: 50 },
|
|
148
|
+
] ),
|
|
149
|
+
lanes: 2,
|
|
150
|
+
gap: 8,
|
|
151
|
+
flowTolerance: 0,
|
|
152
|
+
} );
|
|
153
|
+
expect( result.placements.get( 'a' )?.top ).toBe( 0 );
|
|
154
|
+
expect( result.placements.get( 'b' )?.top ).toBe( 0 );
|
|
155
|
+
// c stacks on the shorter lane; both started at 100 so
|
|
156
|
+
// either is acceptable, but with tolerance 0 the lane with
|
|
157
|
+
// the lowest baseline wins (tied → earliest lane keeps).
|
|
158
|
+
expect( result.placements.get( 'c' )?.top ).toBe( 108 );
|
|
159
|
+
} );
|
|
160
|
+
} );
|
|
161
|
+
|
|
162
|
+
describe( 'spanning', () => {
|
|
163
|
+
it( 'spans contiguous lanes and uses the tallest one as baseline', () => {
|
|
164
|
+
// A in lane 0 (h=100), B in lane 1 (h=50). C spans 2:
|
|
165
|
+
// must start where lane 0 (100) is clear → top=100+gap.
|
|
166
|
+
const result = computeLanePlacements( {
|
|
167
|
+
items: items( [
|
|
168
|
+
{ key: 'a', height: 100 },
|
|
169
|
+
{ key: 'b', height: 50 },
|
|
170
|
+
{ key: 'c', span: 2, height: 80 },
|
|
171
|
+
] ),
|
|
172
|
+
lanes: 2,
|
|
173
|
+
gap: 10,
|
|
174
|
+
flowTolerance: 0,
|
|
175
|
+
} );
|
|
176
|
+
expect( result.placements.get( 'c' ) ).toEqual( {
|
|
177
|
+
key: 'c',
|
|
178
|
+
lane: 0,
|
|
179
|
+
top: 110,
|
|
180
|
+
span: 2,
|
|
181
|
+
} );
|
|
182
|
+
expect( result.totalHeight ).toBe( 190 );
|
|
183
|
+
} );
|
|
184
|
+
|
|
185
|
+
it( 'finds the shortest run when multiple span positions exist', () => {
|
|
186
|
+
// Three lanes, baselines [100, 0, 100] after seeding.
|
|
187
|
+
// A span-2 item should pick lanes 1-2 (max=100) over
|
|
188
|
+
// lanes 0-1 (max=100); ties broken by earliest, so 0-1.
|
|
189
|
+
const result = computeLanePlacements( {
|
|
190
|
+
items: items( [
|
|
191
|
+
{ key: 'seed-l', height: 100 },
|
|
192
|
+
{ key: 'seed-r', lane: 2, height: 100 },
|
|
193
|
+
{ key: 'span', span: 2, height: 50 },
|
|
194
|
+
] ),
|
|
195
|
+
lanes: 3,
|
|
196
|
+
gap: 0,
|
|
197
|
+
flowTolerance: 0,
|
|
198
|
+
} );
|
|
199
|
+
expect( result.placements.get( 'span' )?.lane ).toBe( 0 );
|
|
200
|
+
} );
|
|
201
|
+
|
|
202
|
+
it( 'clamps span to the lane count', () => {
|
|
203
|
+
const result = computeLanePlacements( {
|
|
204
|
+
items: items( [ { key: 'a', span: 99, height: 100 } ] ),
|
|
205
|
+
lanes: 4,
|
|
206
|
+
gap: 0,
|
|
207
|
+
flowTolerance: 0,
|
|
208
|
+
} );
|
|
209
|
+
expect( result.placements.get( 'a' )?.span ).toBe( 4 );
|
|
210
|
+
expect( result.placements.get( 'a' )?.lane ).toBe( 0 );
|
|
211
|
+
} );
|
|
212
|
+
|
|
213
|
+
it( 'clamps span to a minimum of 1', () => {
|
|
214
|
+
const result = computeLanePlacements( {
|
|
215
|
+
items: items( [ { key: 'a', span: 0, height: 100 } ] ),
|
|
216
|
+
lanes: 4,
|
|
217
|
+
gap: 0,
|
|
218
|
+
flowTolerance: 0,
|
|
219
|
+
} );
|
|
220
|
+
expect( result.placements.get( 'a' )?.span ).toBe( 1 );
|
|
221
|
+
} );
|
|
222
|
+
} );
|
|
223
|
+
|
|
224
|
+
describe( 'flow tolerance', () => {
|
|
225
|
+
it( 'with tolerance 0, picks the strictly shortest lane', () => {
|
|
226
|
+
// Lanes after seeding: [100, 99]. Item should pick lane 1
|
|
227
|
+
// because 99 < 100 by 1, exceeding tolerance 0.
|
|
228
|
+
const result = computeLanePlacements( {
|
|
229
|
+
items: items( [
|
|
230
|
+
{ key: 's0', height: 100 },
|
|
231
|
+
{ key: 's1', lane: 1, height: 99 },
|
|
232
|
+
{ key: 'next', height: 30 },
|
|
233
|
+
] ),
|
|
234
|
+
lanes: 2,
|
|
235
|
+
gap: 0,
|
|
236
|
+
flowTolerance: 0,
|
|
237
|
+
} );
|
|
238
|
+
expect( result.placements.get( 'next' )?.lane ).toBe( 1 );
|
|
239
|
+
} );
|
|
240
|
+
|
|
241
|
+
it( 'with a generous tolerance, prefers the earlier lane', () => {
|
|
242
|
+
// Lanes [100, 99]. Tolerance 5 absorbs the 1px difference;
|
|
243
|
+
// reading order wins → lane 0.
|
|
244
|
+
const result = computeLanePlacements( {
|
|
245
|
+
items: items( [
|
|
246
|
+
{ key: 's0', height: 100 },
|
|
247
|
+
{ key: 's1', lane: 1, height: 99 },
|
|
248
|
+
{ key: 'next', height: 30 },
|
|
249
|
+
] ),
|
|
250
|
+
lanes: 2,
|
|
251
|
+
gap: 0,
|
|
252
|
+
flowTolerance: 5,
|
|
253
|
+
} );
|
|
254
|
+
expect( result.placements.get( 'next' )?.lane ).toBe( 0 );
|
|
255
|
+
} );
|
|
256
|
+
|
|
257
|
+
it( 'tolerance does not override a clearly shorter lane', () => {
|
|
258
|
+
// Lanes [100, 30]. Even with tolerance 5, the 70px gap
|
|
259
|
+
// between baselines is decisive → lane 1.
|
|
260
|
+
const result = computeLanePlacements( {
|
|
261
|
+
items: items( [
|
|
262
|
+
{ key: 's0', height: 100 },
|
|
263
|
+
{ key: 's1', lane: 1, height: 30 },
|
|
264
|
+
{ key: 'next', height: 30 },
|
|
265
|
+
] ),
|
|
266
|
+
lanes: 2,
|
|
267
|
+
gap: 0,
|
|
268
|
+
flowTolerance: 5,
|
|
269
|
+
} );
|
|
270
|
+
expect( result.placements.get( 'next' )?.lane ).toBe( 1 );
|
|
271
|
+
} );
|
|
272
|
+
} );
|
|
273
|
+
|
|
274
|
+
describe( 'explicit placement', () => {
|
|
275
|
+
it( 'honors an explicit lane index', () => {
|
|
276
|
+
const result = computeLanePlacements( {
|
|
277
|
+
items: items( [ { key: 'a', lane: 2, height: 50 } ] ),
|
|
278
|
+
lanes: 4,
|
|
279
|
+
gap: 0,
|
|
280
|
+
flowTolerance: 0,
|
|
281
|
+
} );
|
|
282
|
+
expect( result.placements.get( 'a' )?.lane ).toBe( 2 );
|
|
283
|
+
expect( result.placements.get( 'a' )?.top ).toBe( 0 );
|
|
284
|
+
} );
|
|
285
|
+
|
|
286
|
+
it( 'places explicit items first regardless of source order', () => {
|
|
287
|
+
// Source order: auto, explicit. Explicit must be placed
|
|
288
|
+
// first so the auto item flows around it.
|
|
289
|
+
const result = computeLanePlacements( {
|
|
290
|
+
items: items( [
|
|
291
|
+
{ key: 'auto', height: 100 },
|
|
292
|
+
{ key: 'pinned', lane: 0, height: 50 },
|
|
293
|
+
] ),
|
|
294
|
+
lanes: 2,
|
|
295
|
+
gap: 0,
|
|
296
|
+
flowTolerance: 0,
|
|
297
|
+
} );
|
|
298
|
+
expect( result.placements.get( 'pinned' )?.lane ).toBe( 0 );
|
|
299
|
+
expect( result.placements.get( 'pinned' )?.top ).toBe( 0 );
|
|
300
|
+
// auto can't take lane 0 (occupied at top) — it lands in
|
|
301
|
+
// lane 1, the only zero-baseline lane available.
|
|
302
|
+
expect( result.placements.get( 'auto' )?.lane ).toBe( 1 );
|
|
303
|
+
expect( result.placements.get( 'auto' )?.top ).toBe( 0 );
|
|
304
|
+
} );
|
|
305
|
+
|
|
306
|
+
it( 'clamps an out-of-range explicit lane', () => {
|
|
307
|
+
const result = computeLanePlacements( {
|
|
308
|
+
items: items( [ { key: 'a', lane: 99, height: 50 } ] ),
|
|
309
|
+
lanes: 4,
|
|
310
|
+
gap: 0,
|
|
311
|
+
flowTolerance: 0,
|
|
312
|
+
} );
|
|
313
|
+
// `lane: 99` clamps to `lanes - span = 4 - 1 = 3`.
|
|
314
|
+
expect( result.placements.get( 'a' )?.lane ).toBe( 3 );
|
|
315
|
+
} );
|
|
316
|
+
|
|
317
|
+
it( 'clamps a negative explicit lane to 0', () => {
|
|
318
|
+
const result = computeLanePlacements( {
|
|
319
|
+
items: items( [ { key: 'a', lane: -5, height: 50 } ] ),
|
|
320
|
+
lanes: 4,
|
|
321
|
+
gap: 0,
|
|
322
|
+
flowTolerance: 0,
|
|
323
|
+
} );
|
|
324
|
+
expect( result.placements.get( 'a' )?.lane ).toBe( 0 );
|
|
325
|
+
} );
|
|
326
|
+
|
|
327
|
+
it( 'clamps an explicit lane that would push the span off-grid', () => {
|
|
328
|
+
// span 3 cannot start at lane 2 of a 4-lane grid (would
|
|
329
|
+
// occupy lanes 2-3-4 but lane 4 is out of range).
|
|
330
|
+
const result = computeLanePlacements( {
|
|
331
|
+
items: items( [ { key: 'a', lane: 2, span: 3, height: 50 } ] ),
|
|
332
|
+
lanes: 4,
|
|
333
|
+
gap: 0,
|
|
334
|
+
flowTolerance: 0,
|
|
335
|
+
} );
|
|
336
|
+
expect( result.placements.get( 'a' )?.lane ).toBe( 1 );
|
|
337
|
+
expect( result.placements.get( 'a' )?.span ).toBe( 3 );
|
|
338
|
+
} );
|
|
339
|
+
} );
|
|
340
|
+
|
|
341
|
+
describe( 'input clamping and edge cases', () => {
|
|
342
|
+
it( 'clamps lanes to a minimum of 1', () => {
|
|
343
|
+
const result = computeLanePlacements( {
|
|
344
|
+
items: items( [
|
|
345
|
+
{ key: 'a', height: 50 },
|
|
346
|
+
{ key: 'b', height: 50 },
|
|
347
|
+
] ),
|
|
348
|
+
lanes: 0,
|
|
349
|
+
gap: 0,
|
|
350
|
+
flowTolerance: 0,
|
|
351
|
+
} );
|
|
352
|
+
expect( result.placements.get( 'a' )?.lane ).toBe( 0 );
|
|
353
|
+
expect( result.placements.get( 'b' )?.lane ).toBe( 0 );
|
|
354
|
+
expect( result.totalHeight ).toBe( 100 );
|
|
355
|
+
} );
|
|
356
|
+
|
|
357
|
+
it( 'treats a negative gap as zero', () => {
|
|
358
|
+
const result = computeLanePlacements( {
|
|
359
|
+
items: items( [
|
|
360
|
+
{ key: 'a', height: 50 },
|
|
361
|
+
{ key: 'b', height: 50 },
|
|
362
|
+
] ),
|
|
363
|
+
lanes: 1,
|
|
364
|
+
gap: -10,
|
|
365
|
+
flowTolerance: 0,
|
|
366
|
+
} );
|
|
367
|
+
expect( result.placements.get( 'b' )?.top ).toBe( 50 );
|
|
368
|
+
} );
|
|
369
|
+
|
|
370
|
+
it( 'treats a negative tolerance as zero', () => {
|
|
371
|
+
// With tolerance treated as 0, the lane with the strictly
|
|
372
|
+
// lower baseline always wins.
|
|
373
|
+
const result = computeLanePlacements( {
|
|
374
|
+
items: items( [
|
|
375
|
+
{ key: 's0', height: 100 },
|
|
376
|
+
{ key: 's1', lane: 1, height: 99 },
|
|
377
|
+
{ key: 'next', height: 30 },
|
|
378
|
+
] ),
|
|
379
|
+
lanes: 2,
|
|
380
|
+
gap: 0,
|
|
381
|
+
flowTolerance: -50,
|
|
382
|
+
} );
|
|
383
|
+
expect( result.placements.get( 'next' )?.lane ).toBe( 1 );
|
|
384
|
+
} );
|
|
385
|
+
|
|
386
|
+
it( 'treats negative item heights as zero', () => {
|
|
387
|
+
const result = computeLanePlacements( {
|
|
388
|
+
items: items( [
|
|
389
|
+
{ key: 'a', height: -50 },
|
|
390
|
+
{ key: 'b', height: 30 },
|
|
391
|
+
] ),
|
|
392
|
+
lanes: 1,
|
|
393
|
+
gap: 0,
|
|
394
|
+
flowTolerance: 0,
|
|
395
|
+
} );
|
|
396
|
+
expect( result.placements.get( 'b' )?.top ).toBe( 0 );
|
|
397
|
+
expect( result.totalHeight ).toBe( 30 );
|
|
398
|
+
} );
|
|
399
|
+
} );
|
|
400
|
+
|
|
401
|
+
describe( 'realistic scenarios', () => {
|
|
402
|
+
it( 'packs a mixed dashboard layout into 3 lanes', () => {
|
|
403
|
+
const result = computeLanePlacements( {
|
|
404
|
+
items: items( [
|
|
405
|
+
{ key: 'kpi', height: 120 },
|
|
406
|
+
{ key: 'chart', span: 2, height: 240 },
|
|
407
|
+
{ key: 'note', height: 80 },
|
|
408
|
+
{ key: 'feed', height: 300 },
|
|
409
|
+
{ key: 'tasks', height: 160 },
|
|
410
|
+
] ),
|
|
411
|
+
lanes: 3,
|
|
412
|
+
gap: 16,
|
|
413
|
+
flowTolerance: 16,
|
|
414
|
+
} );
|
|
415
|
+
expect( result.placements.size ).toBe( 5 );
|
|
416
|
+
expect( result.placements.get( 'kpi' )?.top ).toBe( 0 );
|
|
417
|
+
expect( result.placements.get( 'chart' )?.lane ).toBe( 1 );
|
|
418
|
+
expect( result.placements.get( 'chart' )?.span ).toBe( 2 );
|
|
419
|
+
expect( result.totalHeight ).toBeGreaterThan( 0 );
|
|
420
|
+
} );
|
|
421
|
+
|
|
422
|
+
it( 'is deterministic across calls with identical input', () => {
|
|
423
|
+
const input = {
|
|
424
|
+
items: items( [
|
|
425
|
+
{ key: 'a', height: 100 },
|
|
426
|
+
{ key: 'b', height: 50 },
|
|
427
|
+
{ key: 'c', span: 2, height: 80 },
|
|
428
|
+
{ key: 'd', height: 30 },
|
|
429
|
+
] ),
|
|
430
|
+
lanes: 3,
|
|
431
|
+
gap: 8,
|
|
432
|
+
flowTolerance: 4,
|
|
433
|
+
};
|
|
434
|
+
const a = computeLanePlacements( input );
|
|
435
|
+
const b = computeLanePlacements( input );
|
|
436
|
+
expect( Array.from( a.placements.entries() ) ).toEqual(
|
|
437
|
+
Array.from( b.placements.entries() )
|
|
438
|
+
);
|
|
439
|
+
expect( a.totalHeight ).toBe( b.totalHeight );
|
|
440
|
+
} );
|
|
441
|
+
} );
|
|
442
|
+
} );
|