@wordpress/grid 0.1.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.
Files changed (158) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE.md +788 -0
  3. package/README.md +534 -0
  4. package/build/dashboard-grid/grid-item.cjs +308 -0
  5. package/build/dashboard-grid/grid-item.cjs.map +7 -0
  6. package/build/dashboard-grid/index.cjs +591 -0
  7. package/build/dashboard-grid/index.cjs.map +7 -0
  8. package/build/dashboard-grid/resolve-fill-widths.cjs +189 -0
  9. package/build/dashboard-grid/resolve-fill-widths.cjs.map +7 -0
  10. package/build/dashboard-grid/types.cjs +19 -0
  11. package/build/dashboard-grid/types.cjs.map +7 -0
  12. package/build/dashboard-lanes/index.cjs +558 -0
  13. package/build/dashboard-lanes/index.cjs.map +7 -0
  14. package/build/dashboard-lanes/lane-placement.cjs +110 -0
  15. package/build/dashboard-lanes/lane-placement.cjs.map +7 -0
  16. package/build/dashboard-lanes/lanes-item.cjs +295 -0
  17. package/build/dashboard-lanes/lanes-item.cjs.map +7 -0
  18. package/build/dashboard-lanes/types.cjs +19 -0
  19. package/build/dashboard-lanes/types.cjs.map +7 -0
  20. package/build/dashboard-lanes/use-lane-placement.cjs +206 -0
  21. package/build/dashboard-lanes/use-lane-placement.cjs.map +7 -0
  22. package/build/index.cjs +34 -0
  23. package/build/index.cjs.map +7 -0
  24. package/build/shared/drag-overlay-drop-animation.cjs +70 -0
  25. package/build/shared/drag-overlay-drop-animation.cjs.map +7 -0
  26. package/build/shared/grid-item-key.cjs +31 -0
  27. package/build/shared/grid-item-key.cjs.map +7 -0
  28. package/build/shared/grid-overlay.cjs +187 -0
  29. package/build/shared/grid-overlay.cjs.map +7 -0
  30. package/build/shared/item-exit-overlay.cjs +150 -0
  31. package/build/shared/item-exit-overlay.cjs.map +7 -0
  32. package/build/shared/resize-handle.cjs +224 -0
  33. package/build/shared/resize-handle.cjs.map +7 -0
  34. package/build/shared/resize-snap.cjs +47 -0
  35. package/build/shared/resize-snap.cjs.map +7 -0
  36. package/build/shared/types.cjs +19 -0
  37. package/build/shared/types.cjs.map +7 -0
  38. package/build/shared/use-item-exit-animation.cjs +148 -0
  39. package/build/shared/use-item-exit-animation.cjs.map +7 -0
  40. package/build/shared/use-layout-shift-animation.cjs +167 -0
  41. package/build/shared/use-layout-shift-animation.cjs.map +7 -0
  42. package/build-module/dashboard-grid/grid-item.mjs +273 -0
  43. package/build-module/dashboard-grid/grid-item.mjs.map +7 -0
  44. package/build-module/dashboard-grid/index.mjs +579 -0
  45. package/build-module/dashboard-grid/index.mjs.map +7 -0
  46. package/build-module/dashboard-grid/resolve-fill-widths.mjs +164 -0
  47. package/build-module/dashboard-grid/resolve-fill-widths.mjs.map +7 -0
  48. package/build-module/dashboard-grid/types.mjs +1 -0
  49. package/build-module/dashboard-grid/types.mjs.map +7 -0
  50. package/build-module/dashboard-lanes/index.mjs +547 -0
  51. package/build-module/dashboard-lanes/index.mjs.map +7 -0
  52. package/build-module/dashboard-lanes/lane-placement.mjs +85 -0
  53. package/build-module/dashboard-lanes/lane-placement.mjs.map +7 -0
  54. package/build-module/dashboard-lanes/lanes-item.mjs +260 -0
  55. package/build-module/dashboard-lanes/lanes-item.mjs.map +7 -0
  56. package/build-module/dashboard-lanes/types.mjs +1 -0
  57. package/build-module/dashboard-lanes/types.mjs.map +7 -0
  58. package/build-module/dashboard-lanes/use-lane-placement.mjs +181 -0
  59. package/build-module/dashboard-lanes/use-lane-placement.mjs.map +7 -0
  60. package/build-module/index.mjs +8 -0
  61. package/build-module/index.mjs.map +7 -0
  62. package/build-module/shared/drag-overlay-drop-animation.mjs +47 -0
  63. package/build-module/shared/drag-overlay-drop-animation.mjs.map +7 -0
  64. package/build-module/shared/grid-item-key.mjs +6 -0
  65. package/build-module/shared/grid-item-key.mjs.map +7 -0
  66. package/build-module/shared/grid-overlay.mjs +152 -0
  67. package/build-module/shared/grid-overlay.mjs.map +7 -0
  68. package/build-module/shared/item-exit-overlay.mjs +125 -0
  69. package/build-module/shared/item-exit-overlay.mjs.map +7 -0
  70. package/build-module/shared/resize-handle.mjs +193 -0
  71. package/build-module/shared/resize-handle.mjs.map +7 -0
  72. package/build-module/shared/resize-snap.mjs +21 -0
  73. package/build-module/shared/resize-snap.mjs.map +7 -0
  74. package/build-module/shared/types.mjs +1 -0
  75. package/build-module/shared/types.mjs.map +7 -0
  76. package/build-module/shared/use-item-exit-animation.mjs +128 -0
  77. package/build-module/shared/use-item-exit-animation.mjs.map +7 -0
  78. package/build-module/shared/use-layout-shift-animation.mjs +140 -0
  79. package/build-module/shared/use-layout-shift-animation.mjs.map +7 -0
  80. package/build-types/dashboard-grid/grid-item.d.ts +3 -0
  81. package/build-types/dashboard-grid/grid-item.d.ts.map +1 -0
  82. package/build-types/dashboard-grid/index.d.ts +35 -0
  83. package/build-types/dashboard-grid/index.d.ts.map +1 -0
  84. package/build-types/dashboard-grid/resolve-fill-widths.d.ts +26 -0
  85. package/build-types/dashboard-grid/resolve-fill-widths.d.ts.map +1 -0
  86. package/build-types/dashboard-grid/stories/index.story.d.ts +98 -0
  87. package/build-types/dashboard-grid/stories/index.story.d.ts.map +1 -0
  88. package/build-types/dashboard-grid/types.d.ts +232 -0
  89. package/build-types/dashboard-grid/types.d.ts.map +1 -0
  90. package/build-types/dashboard-lanes/index.d.ts +40 -0
  91. package/build-types/dashboard-lanes/index.d.ts.map +1 -0
  92. package/build-types/dashboard-lanes/lane-placement.d.ts +126 -0
  93. package/build-types/dashboard-lanes/lane-placement.d.ts.map +1 -0
  94. package/build-types/dashboard-lanes/lanes-item.d.ts +52 -0
  95. package/build-types/dashboard-lanes/lanes-item.d.ts.map +1 -0
  96. package/build-types/dashboard-lanes/stories/index.story.d.ts +64 -0
  97. package/build-types/dashboard-lanes/stories/index.story.d.ts.map +1 -0
  98. package/build-types/dashboard-lanes/types.d.ts +151 -0
  99. package/build-types/dashboard-lanes/types.d.ts.map +1 -0
  100. package/build-types/dashboard-lanes/use-lane-placement.d.ts +74 -0
  101. package/build-types/dashboard-lanes/use-lane-placement.d.ts.map +1 -0
  102. package/build-types/index.d.ts +6 -0
  103. package/build-types/index.d.ts.map +1 -0
  104. package/build-types/shared/drag-overlay-drop-animation.d.ts +13 -0
  105. package/build-types/shared/drag-overlay-drop-animation.d.ts.map +1 -0
  106. package/build-types/shared/grid-item-key.d.ts +6 -0
  107. package/build-types/shared/grid-item-key.d.ts.map +1 -0
  108. package/build-types/shared/grid-overlay.d.ts +19 -0
  109. package/build-types/shared/grid-overlay.d.ts.map +1 -0
  110. package/build-types/shared/item-exit-overlay.d.ts +20 -0
  111. package/build-types/shared/item-exit-overlay.d.ts.map +1 -0
  112. package/build-types/shared/resize-handle.d.ts +23 -0
  113. package/build-types/shared/resize-handle.d.ts.map +1 -0
  114. package/build-types/shared/resize-snap.d.ts +41 -0
  115. package/build-types/shared/resize-snap.d.ts.map +1 -0
  116. package/build-types/shared/types.d.ts +144 -0
  117. package/build-types/shared/types.d.ts.map +1 -0
  118. package/build-types/shared/use-item-exit-animation.d.ts +37 -0
  119. package/build-types/shared/use-item-exit-animation.d.ts.map +1 -0
  120. package/build-types/shared/use-layout-shift-animation.d.ts +77 -0
  121. package/build-types/shared/use-layout-shift-animation.d.ts.map +1 -0
  122. package/package.json +80 -0
  123. package/src/dashboard-grid/grid-item.module.css +94 -0
  124. package/src/dashboard-grid/grid-item.tsx +205 -0
  125. package/src/dashboard-grid/grid.module.css +134 -0
  126. package/src/dashboard-grid/index.tsx +713 -0
  127. package/src/dashboard-grid/resolve-fill-widths.ts +224 -0
  128. package/src/dashboard-grid/stories/index.story.tsx +930 -0
  129. package/src/dashboard-grid/test/keyboard-activation.test.tsx +76 -0
  130. package/src/dashboard-grid/test/resolve-fill-widths.test.ts +250 -0
  131. package/src/dashboard-grid/types.ts +271 -0
  132. package/src/dashboard-lanes/index.tsx +629 -0
  133. package/src/dashboard-lanes/lane-placement.ts +245 -0
  134. package/src/dashboard-lanes/lanes-item.module.css +93 -0
  135. package/src/dashboard-lanes/lanes-item.tsx +236 -0
  136. package/src/dashboard-lanes/lanes.module.css +152 -0
  137. package/src/dashboard-lanes/stories/index.story.tsx +518 -0
  138. package/src/dashboard-lanes/test/keyboard-activation.test.tsx +71 -0
  139. package/src/dashboard-lanes/test/lane-placement.test.ts +442 -0
  140. package/src/dashboard-lanes/test/use-lane-placement.test.tsx +358 -0
  141. package/src/dashboard-lanes/types.ts +176 -0
  142. package/src/dashboard-lanes/use-lane-placement.ts +313 -0
  143. package/src/index.ts +17 -0
  144. package/src/shared/actionable-area-slot.module.css +16 -0
  145. package/src/shared/drag-overlay-drop-animation.ts +66 -0
  146. package/src/shared/grid-item-key.ts +5 -0
  147. package/src/shared/grid-overlay.module.css +82 -0
  148. package/src/shared/grid-overlay.tsx +93 -0
  149. package/src/shared/item-exit-animation.module.css +49 -0
  150. package/src/shared/item-exit-overlay.tsx +57 -0
  151. package/src/shared/layout-shift-animation.module.css +16 -0
  152. package/src/shared/resize-handle.module.css +88 -0
  153. package/src/shared/resize-handle.tsx +163 -0
  154. package/src/shared/resize-snap.ts +63 -0
  155. package/src/shared/test/resize-snap.test.ts +35 -0
  156. package/src/shared/types.ts +164 -0
  157. package/src/shared/use-item-exit-animation.ts +199 -0
  158. package/src/shared/use-layout-shift-animation.ts +284 -0
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ /**
6
+ * External dependencies
7
+ */
8
+ import { render } from '@testing-library/react';
9
+
10
+ /**
11
+ * Internal dependencies
12
+ */
13
+ import { DashboardGrid } from '..';
14
+
15
+ class MockResizeObserver {
16
+ observed: Set< Element > = new Set();
17
+ observe( element: Element ) {
18
+ this.observed.add( element );
19
+ }
20
+ unobserve( element: Element ) {
21
+ this.observed.delete( element );
22
+ }
23
+ disconnect() {
24
+ this.observed.clear();
25
+ }
26
+ }
27
+
28
+ let originalResizeObserver: typeof ResizeObserver;
29
+
30
+ beforeEach( () => {
31
+ originalResizeObserver = global.ResizeObserver;
32
+ ( global as unknown as { ResizeObserver: unknown } ).ResizeObserver =
33
+ MockResizeObserver;
34
+ } );
35
+
36
+ afterEach( () => {
37
+ ( global as unknown as { ResizeObserver: unknown } ).ResizeObserver =
38
+ originalResizeObserver;
39
+ } );
40
+
41
+ describe( 'DashboardGrid keyboard activation', () => {
42
+ it( 'places the dnd-kit keyboard activator on the inner wrapper, not the outer item', () => {
43
+ // Verifies the DOM hierarchy: keyboard activation needs the
44
+ // focused node and the keydown listener to share a node, so
45
+ // the activator must live nested inside the outer item.
46
+ /* eslint-disable testing-library/no-container, testing-library/no-node-access */
47
+ const { container } = render(
48
+ <DashboardGrid
49
+ layout={ [ { key: 'a', width: 1 } ] }
50
+ columns={ 2 }
51
+ editMode
52
+ >
53
+ <div key="a">A</div>
54
+ </DashboardGrid>
55
+ );
56
+
57
+ // Edit mode also renders a resize handle with `role="button"`;
58
+ // `aria-roledescription="sortable"` isolates the activator.
59
+ const activator = container.querySelector(
60
+ '[role="button"][aria-roledescription="sortable"]'
61
+ );
62
+ expect( activator ).not.toBeNull();
63
+ expect( activator ).toHaveAttribute( 'tabindex', '0' );
64
+
65
+ // Outer item is identified by its inline `grid-column-end`
66
+ // placement style; the activator must be its descendant.
67
+ const items = container.querySelectorAll(
68
+ '[style*="grid-column-end"]'
69
+ );
70
+ expect( items ).toHaveLength( 1 );
71
+ const item = items[ 0 ];
72
+ expect( activator ).not.toBe( item );
73
+ expect( item.contains( activator! ) ).toBe( true );
74
+ /* eslint-enable testing-library/no-container, testing-library/no-node-access */
75
+ } );
76
+ } );
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import { resolveFillWidths } from '../resolve-fill-widths';
5
+ import type { DashboardGridLayoutItem } from '../types';
6
+
7
+ function makeMap(
8
+ items: DashboardGridLayoutItem[]
9
+ ): Map< string, DashboardGridLayoutItem > {
10
+ const map = new Map< string, DashboardGridLayoutItem >();
11
+ items.forEach( ( item ) => map.set( item.key, item ) );
12
+ return map;
13
+ }
14
+
15
+ function keys( items: DashboardGridLayoutItem[] ): string[] {
16
+ return items.map( ( item ) => item.key );
17
+ }
18
+
19
+ describe( 'resolveFillWidths', () => {
20
+ it( 'returns empty map when no items use width: "fill"', () => {
21
+ const items: DashboardGridLayoutItem[] = [
22
+ { key: 'a', width: 2 },
23
+ { key: 'b', width: 4 },
24
+ ];
25
+ const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
26
+ expect( result.size ).toBe( 0 );
27
+ } );
28
+
29
+ it( 'fill item takes all columns when alone', () => {
30
+ const items: DashboardGridLayoutItem[] = [
31
+ { key: 'fill', width: 'fill' },
32
+ ];
33
+ const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
34
+ expect( result.get( 'fill' ) ).toBe( 6 );
35
+ } );
36
+
37
+ it( 'fill item takes remaining columns after fixed items', () => {
38
+ const items: DashboardGridLayoutItem[] = [
39
+ { key: 'sidebar', width: 1 },
40
+ { key: 'fill', width: 'fill' },
41
+ ];
42
+ const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
43
+ expect( result.get( 'fill' ) ).toBe( 5 );
44
+ } );
45
+
46
+ it( 'fill item reserves space for subsequent fixed items', () => {
47
+ const items: DashboardGridLayoutItem[] = [
48
+ { key: 'left', width: 1 },
49
+ { key: 'fill', width: 'fill' },
50
+ { key: 'right', width: 2 },
51
+ ];
52
+ const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
53
+ expect( result.get( 'fill' ) ).toBe( 3 );
54
+ } );
55
+
56
+ it( 'fill after a full-width item starts a new row', () => {
57
+ const items: DashboardGridLayoutItem[] = [
58
+ { key: 'full', width: 'full' },
59
+ { key: 'fill', width: 'fill' },
60
+ { key: 'sidebar', width: 1 },
61
+ ];
62
+ const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
63
+ expect( result.get( 'fill' ) ).toBe( 5 );
64
+ } );
65
+
66
+ it( 'consecutive fills each take a full row', () => {
67
+ const items: DashboardGridLayoutItem[] = [
68
+ { key: 'fill-1', width: 'fill' },
69
+ { key: 'fill-2', width: 'fill' },
70
+ ];
71
+ const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
72
+ expect( result.get( 'fill-1' ) ).toBe( 6 );
73
+ expect( result.get( 'fill-2' ) ).toBe( 6 );
74
+ } );
75
+
76
+ it( 'does not reserve items that overflow the row', () => {
77
+ const items: DashboardGridLayoutItem[] = [
78
+ { key: 'fill', width: 'fill' },
79
+ { key: 'a', width: 3 },
80
+ { key: 'b', width: 4 },
81
+ ];
82
+ const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
83
+ expect( result.get( 'fill' ) ).toBe( 3 );
84
+ } );
85
+
86
+ it( 'clamps item widths to maxColumns', () => {
87
+ const items: DashboardGridLayoutItem[] = [
88
+ { key: 'fill', width: 'fill' },
89
+ { key: 'wide', width: 10 },
90
+ ];
91
+ const result = resolveFillWidths( keys( items ), makeMap( items ), 4 );
92
+ expect( result.get( 'fill' ) ).toBe( 4 );
93
+ } );
94
+
95
+ it( 'fill in the middle of a row', () => {
96
+ const items: DashboardGridLayoutItem[] = [
97
+ { key: 'a', width: 1 },
98
+ { key: 'b', width: 1 },
99
+ { key: 'fill', width: 'fill' },
100
+ { key: 'c', width: 1 },
101
+ ];
102
+ const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
103
+ expect( result.get( 'fill' ) ).toBe( 3 );
104
+ } );
105
+
106
+ it( 'multiple fills in different rows', () => {
107
+ const items: DashboardGridLayoutItem[] = [
108
+ { key: 'fill-1', width: 'fill' },
109
+ { key: 'sidebar-1', width: 1 },
110
+ { key: 'full', width: 'full' },
111
+ { key: 'fill-2', width: 'fill' },
112
+ { key: 'sidebar-2', width: 2 },
113
+ ];
114
+ const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
115
+ expect( result.get( 'fill-1' ) ).toBe( 5 );
116
+ expect( result.get( 'fill-2' ) ).toBe( 4 );
117
+ } );
118
+
119
+ it( 'fill gets minimum of 1 column when row is almost full', () => {
120
+ const items: DashboardGridLayoutItem[] = [
121
+ { key: 'a', width: 3 },
122
+ { key: 'b', width: 2 },
123
+ { key: 'fill', width: 'fill' },
124
+ ];
125
+ const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
126
+ expect( result.get( 'fill' ) ).toBe( 1 );
127
+ } );
128
+
129
+ it( 'adapts to different column counts (responsive)', () => {
130
+ const items: DashboardGridLayoutItem[] = [
131
+ { key: 'fill', width: 'fill' },
132
+ { key: 'sidebar', width: 1 },
133
+ ];
134
+ expect(
135
+ resolveFillWidths( keys( items ), makeMap( items ), 6 ).get(
136
+ 'fill'
137
+ )
138
+ ).toBe( 5 );
139
+ expect(
140
+ resolveFillWidths( keys( items ), makeMap( items ), 4 ).get(
141
+ 'fill'
142
+ )
143
+ ).toBe( 3 );
144
+ expect(
145
+ resolveFillWidths( keys( items ), makeMap( items ), 2 ).get(
146
+ 'fill'
147
+ )
148
+ ).toBe( 1 );
149
+ } );
150
+
151
+ it( 'look-ahead stops at the next fill boundary', () => {
152
+ const items: DashboardGridLayoutItem[] = [
153
+ { key: 'fill-1', width: 'fill' },
154
+ { key: 'fill-2', width: 'fill' },
155
+ { key: 'sidebar', width: 1 },
156
+ ];
157
+ const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
158
+ expect( result.get( 'fill-1' ) ).toBe( 6 );
159
+ expect( result.get( 'fill-2' ) ).toBe( 5 );
160
+ } );
161
+
162
+ it( 'look-ahead stops at the next full-width boundary', () => {
163
+ const items: DashboardGridLayoutItem[] = [
164
+ { key: 'fill', width: 'fill' },
165
+ { key: 'full', width: 'full' },
166
+ { key: 'sidebar', width: 1 },
167
+ ];
168
+ const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
169
+ expect( result.get( 'fill' ) ).toBe( 6 );
170
+ } );
171
+
172
+ it( 'every item gets 1 column when maxColumns is 1', () => {
173
+ const items: DashboardGridLayoutItem[] = [
174
+ { key: 'a', width: 3 },
175
+ { key: 'fill', width: 'fill' },
176
+ { key: 'b', width: 2 },
177
+ ];
178
+ const result = resolveFillWidths( keys( items ), makeMap( items ), 1 );
179
+ expect( result.get( 'fill' ) ).toBe( 1 );
180
+ } );
181
+
182
+ it( 'returns empty map for an empty layout', () => {
183
+ const result = resolveFillWidths( [], new Map(), 6 );
184
+ expect( result.size ).toBe( 0 );
185
+ } );
186
+
187
+ describe( 'with multi-row items (height > 1)', () => {
188
+ it( 'accounts for the shadow of a tall tile on the left', () => {
189
+ const items: DashboardGridLayoutItem[] = [
190
+ { key: 'tall', width: 3, height: 2 },
191
+ { key: 'header', width: 9, height: 1 },
192
+ { key: 'sub', width: 3, height: 1 },
193
+ { key: 'fill', width: 'fill', height: 1 },
194
+ ];
195
+ const result = resolveFillWidths(
196
+ keys( items ),
197
+ makeMap( items ),
198
+ 12
199
+ );
200
+ expect( result.get( 'fill' ) ).toBe( 6 );
201
+ } );
202
+
203
+ it( 'accounts for the shadow of a tall tile in the middle', () => {
204
+ const items: DashboardGridLayoutItem[] = [
205
+ { key: 'a', width: 3, height: 1 },
206
+ { key: 'b', width: 3, height: 2 },
207
+ { key: 'c', width: 6, height: 1 },
208
+ { key: 'd', width: 3, height: 1 },
209
+ { key: 'fill', width: 'fill', height: 1 },
210
+ ];
211
+ const result = resolveFillWidths(
212
+ keys( items ),
213
+ makeMap( items ),
214
+ 12
215
+ );
216
+ expect( result.get( 'fill' ) ).toBe( 6 );
217
+ } );
218
+
219
+ it( 'accounts for the shadow of a tall tile on the right', () => {
220
+ const items: DashboardGridLayoutItem[] = [
221
+ { key: 'a', width: 3, height: 1 },
222
+ { key: 'b', width: 6, height: 1 },
223
+ { key: 'c', width: 3, height: 2 },
224
+ { key: 'd', width: 6, height: 1 },
225
+ { key: 'fill', width: 'fill', height: 1 },
226
+ ];
227
+ const result = resolveFillWidths(
228
+ keys( items ),
229
+ makeMap( items ),
230
+ 12
231
+ );
232
+ expect( result.get( 'fill' ) ).toBe( 3 );
233
+ } );
234
+
235
+ it( 'tracks shadow across multiple rows for height > 2', () => {
236
+ const items: DashboardGridLayoutItem[] = [
237
+ { key: 'tall', width: 3, height: 3 },
238
+ { key: 'a', width: 9, height: 1 },
239
+ { key: 'b', width: 9, height: 1 },
240
+ { key: 'fill', width: 'fill', height: 1 },
241
+ ];
242
+ const result = resolveFillWidths(
243
+ keys( items ),
244
+ makeMap( items ),
245
+ 12
246
+ );
247
+ expect( result.get( 'fill' ) ).toBe( 9 );
248
+ } );
249
+ } );
250
+ } );
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import type {
5
+ DragPreviewRenderProps,
6
+ GridOverlayRenderProps,
7
+ ResizeDelta,
8
+ ResizeHandleRenderProps,
9
+ } from '../shared/types';
10
+ import type { ResizeSnapSize } from '../shared/resize-snap';
11
+
12
+ /**
13
+ * Dashboard grid layout item definition.
14
+ *
15
+ * `width` accepts either a numeric column span or a discriminated string:
16
+ * - `number` spans that many columns (clamped to the grid's column count).
17
+ * - `'fill'` spans the remaining columns in the current row.
18
+ * - `'full'` spans all columns (`grid-column: 1 / -1`).
19
+ */
20
+ export type DashboardGridLayoutItem = {
21
+ /**
22
+ * Unique key that matches a child component key.
23
+ */
24
+ key: string;
25
+
26
+ /**
27
+ * Number of columns this item spans, or a string discriminator
28
+ * (`'fill'` or `'full'`).
29
+ */
30
+ width?: number | 'fill' | 'full';
31
+
32
+ /**
33
+ * Number of rows this item spans.
34
+ *
35
+ * @default 1
36
+ */
37
+ height?: number;
38
+
39
+ /**
40
+ * Display order for the item. Lower values render first. When
41
+ * omitted, the item falls back to its index in the `layout` array.
42
+ */
43
+ order?: number;
44
+ };
45
+
46
+ /**
47
+ * Props for the internal `<GridItem />` wrapper.
48
+ */
49
+ export type GridItemProps = {
50
+ /**
51
+ * The layout item containing grid positioning information.
52
+ */
53
+ item: DashboardGridLayoutItem;
54
+
55
+ /**
56
+ * The maximum number of columns in the grid.
57
+ */
58
+ maxColumns: number;
59
+
60
+ /**
61
+ * Whether drag and resize interactions are disabled.
62
+ *
63
+ * @default false
64
+ */
65
+ disabled?: boolean;
66
+
67
+ /**
68
+ * Whether the item can be resized vertically. Disabled when the
69
+ * grid uses `rowHeight: 'auto'`, where row height is driven by
70
+ * content rather than by the user.
71
+ *
72
+ * @default true
73
+ */
74
+ verticalResizable?: boolean;
75
+
76
+ /**
77
+ * Whether any tile in the grid is currently being dragged or
78
+ * resized. Drives the drag activator cursor.
79
+ *
80
+ * @default false
81
+ */
82
+ interacting?: boolean;
83
+
84
+ /**
85
+ * Whether a tile drag is in progress. Mutes each tile's
86
+ * `actionableArea` with `inert` so hovers on other tiles' controls
87
+ * do not steal the gesture.
88
+ *
89
+ * @default false
90
+ */
91
+ dragging?: boolean;
92
+
93
+ /**
94
+ * The content to be displayed within the grid item.
95
+ */
96
+ children: React.ReactNode;
97
+
98
+ /**
99
+ * Content rendered above the draggable area that stays interactive
100
+ * in edit mode, typically action buttons, menus, or links. While
101
+ * a tile drag is in progress, this content is set `inert` so hovers
102
+ * on other tiles can't steal the gesture. During resize, visibility
103
+ * is controlled by grid-level CSS hooks.
104
+ */
105
+ actionableArea?: React.ReactNode;
106
+
107
+ /**
108
+ * Callback fired while the item is being resized. Receives the
109
+ * item's `key` plus the cursor offset from the gesture start in
110
+ * pixels. The grid derives snapped spans from the delta and passes
111
+ * them back through `resizeSnapPreview`.
112
+ */
113
+ onResize: ( id: string, delta: ResizeDelta ) => void;
114
+
115
+ /**
116
+ * Snapped grid size in pixels for the resize-preview outline. The
117
+ * tile content resizes continuously with the cursor; this outline
118
+ * shows the span the layout will commit to on release.
119
+ */
120
+ resizeSnapPreview?: ResizeSnapSize | null;
121
+
122
+ /**
123
+ * Minimum tile width while resizing, in pixels (one column track).
124
+ */
125
+ minResizeWidthPx: number;
126
+
127
+ /**
128
+ * Minimum tile height while resizing, in pixels (one row track).
129
+ * Omitted when vertical resize is disabled.
130
+ */
131
+ minResizeHeightPx?: number;
132
+
133
+ /**
134
+ * Callback fired when the resize gesture ends.
135
+ */
136
+ onResizeEnd: () => void;
137
+
138
+ /**
139
+ * Component forwarded to `<ResizeHandle />` to override the default
140
+ * corner triangle. See `DashboardGridProps.renderResizeHandle`.
141
+ */
142
+ renderResizeHandle?: React.ComponentType< ResizeHandleRenderProps >;
143
+ };
144
+
145
+ /**
146
+ * Props for `DashboardGrid`. Extends the standard div props so consumers
147
+ * can pass `id`, `aria-*`, `data-*`, event handlers, etc., directly on
148
+ * the grid root.
149
+ *
150
+ * `columns` and `minColumnWidth` compose as a layered model:
151
+ * - `columns` alone: fixed N columns; each tile scales with the container.
152
+ * - `minColumnWidth` alone: column count derives from container width,
153
+ * floored by the per-tile minimum, down to 1 column.
154
+ * - Both together: `columns` caps the count, `minColumnWidth` enforces a
155
+ * per-tile width floor that can reduce the count below the cap on
156
+ * narrow containers ("up to N columns, but never narrower than W px").
157
+ */
158
+ export interface DashboardGridProps
159
+ extends Omit<
160
+ React.ComponentPropsWithoutRef< 'div' >,
161
+ 'children' | 'className' | 'style'
162
+ > {
163
+ /**
164
+ * Array of layout items.
165
+ */
166
+ layout: DashboardGridLayoutItem[];
167
+
168
+ /**
169
+ * Grid children. Each child must carry a `key` that matches an
170
+ * entry in `layout`; children without a match render at the end
171
+ * of the grid without explicit placement and fall through CSS
172
+ * Grid's auto-flow.
173
+ */
174
+ children: React.ReactNode;
175
+
176
+ /**
177
+ * Additional CSS class on the grid root.
178
+ */
179
+ className?: string;
180
+
181
+ /**
182
+ * Inline styles applied to the grid root. Merged underneath the
183
+ * grid's own layout styles, so the layout (`gridTemplateColumns`,
184
+ * `gridAutoRows`) always wins. The gap between tiles is owned by
185
+ * the design-system gap token and is not configurable per
186
+ * instance; override it via a theme or density change.
187
+ */
188
+ style?: React.CSSProperties;
189
+
190
+ /**
191
+ * Height of each row in pixels, or `'auto'` to let the tallest
192
+ * tile in the row size it.
193
+ *
194
+ * @default 'auto'
195
+ */
196
+ rowHeight?: number | 'auto';
197
+
198
+ /**
199
+ * Whether the grid is in edit mode (allows dragging and
200
+ * repositioning items).
201
+ *
202
+ * @default false
203
+ */
204
+ editMode?: boolean;
205
+
206
+ /**
207
+ * Callback fired when the user commits a drag or resize. Receives
208
+ * the resulting layout.
209
+ */
210
+ onChangeLayout?: ( newLayout: DashboardGridLayoutItem[] ) => void;
211
+
212
+ /**
213
+ * Callback fired continuously during a drag or resize interaction
214
+ * with the in-progress layout. Useful for live feedback in the
215
+ * surface (e.g., displaying the current width/position). The final
216
+ * committed layout is still emitted via `onChangeLayout`.
217
+ */
218
+ onPreviewLayout?: ( previewLayout: DashboardGridLayoutItem[] ) => void;
219
+
220
+ /**
221
+ * Override the default corner-triangle resize handle with a custom
222
+ * component. The grid still owns the gesture (dnd-kit `<DndContext>`,
223
+ * throttled delta loop) and passes the wiring to the consumer:
224
+ * spread `listeners` and `attributes` and assign `ref` on the
225
+ * element that should receive the gesture. Use `disabled` and
226
+ * `verticalResizable` to adapt the visual to context.
227
+ */
228
+ renderResizeHandle?: React.ComponentType< ResizeHandleRenderProps >;
229
+
230
+ /**
231
+ * Custom wrapper for the dragged-clone visual mounted inside
232
+ * `<DragOverlay>`. The surface always wraps the clone with a thin
233
+ * functional frame (lift scale, grabbing cursor, pointer pass-
234
+ * through) and mounts this component inside it; the consumer
235
+ * owns the visual chrome (shadow, radius, padding).
236
+ *
237
+ * When omitted, the cloned children render directly inside the
238
+ * functional frame so any chrome the consumer applied to the
239
+ * persistent tile carries through unchanged.
240
+ *
241
+ * Token-only adjustments (lift scale, placeholder opacity,
242
+ * outline color, placeholder radius) flow through CSS custom
243
+ * properties documented in the README.
244
+ */
245
+ renderDragPreview?: React.ComponentType< DragPreviewRenderProps >;
246
+
247
+ /**
248
+ * Override the default edit-mode overlay (row-marker tiles per
249
+ * column) with a custom component. The grid supplies the resolved
250
+ * column count, row height, and row count; the consumer is
251
+ * responsible for the visual.
252
+ *
253
+ * The overlay only renders when `editMode` is true. When omitted,
254
+ * the package's default visual is used.
255
+ */
256
+ renderGridOverlay?: React.ComponentType< GridOverlayRenderProps >;
257
+
258
+ /**
259
+ * Target column count, used as a cap. Defaults to six when neither
260
+ * `columns` nor `minColumnWidth` is set; with `minColumnWidth` set
261
+ * it can resolve lower on narrow containers.
262
+ */
263
+ columns?: number;
264
+
265
+ /**
266
+ * Per-tile minimum width in pixels. Enables responsive mode: the
267
+ * column count derives from container width, floored by this value,
268
+ * down to 1.
269
+ */
270
+ minColumnWidth?: number;
271
+ }