@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.
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,930 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import type { Meta, StoryObj } from '@storybook/react-vite';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { useState, useMemo } from '@wordpress/element';
10
+ import { close, justifyStretch, stretchFullWidth } from '@wordpress/icons';
11
+ // eslint-disable-next-line @wordpress/use-recommended-components -- @wordpress/grid consumes @wordpress/ui in story examples only.
12
+ import { Icon, IconButton, Stack } from '@wordpress/ui';
13
+
14
+ /**
15
+ * Internal dependencies
16
+ */
17
+ import { DashboardGrid } from '..';
18
+ import type { DashboardGridLayoutItem } from '../types';
19
+ import type {
20
+ DragPreviewRenderProps,
21
+ GridOverlayRenderProps,
22
+ ResizeHandleRenderProps,
23
+ } from '../../shared/types';
24
+
25
+ const meta: Meta< typeof DashboardGrid > = {
26
+ title: 'Grid/DashboardGrid',
27
+ component: DashboardGrid,
28
+ tags: [ 'status-experimental' ],
29
+ args: {
30
+ columns: 6,
31
+ rowHeight: 80,
32
+ editMode: false,
33
+ },
34
+ argTypes: {
35
+ children: { control: false },
36
+ columns: {
37
+ control: { type: 'number', min: 1, max: 24, step: 1 },
38
+ description: 'Total columns in fixed mode.',
39
+ },
40
+ minColumnWidth: {
41
+ control: { type: 'number', min: 80, max: 600, step: 8 },
42
+ description:
43
+ 'Enables responsive mode. Per-column lower bound in pixels.',
44
+ },
45
+ rowHeight: {
46
+ control: { type: 'number', min: 24, max: 400, step: 4 },
47
+ description: 'Row height in pixels, or `auto`.',
48
+ },
49
+ editMode: {
50
+ control: { type: 'boolean' },
51
+ description: 'Enables drag-to-reorder and resize.',
52
+ },
53
+ className: { control: { type: 'text' } },
54
+ onChangeLayout: { action: 'onChangeLayout' },
55
+ onPreviewLayout: { action: 'onPreviewLayout' },
56
+ },
57
+ parameters: {
58
+ componentStatus: {
59
+ status: 'use-with-caution',
60
+ whereUsed: 'global',
61
+ notes: 'This package is under heavy development and likely to change.',
62
+ },
63
+ },
64
+ };
65
+ export default meta;
66
+
67
+ type Story = StoryObj< typeof DashboardGrid >;
68
+
69
+ type Tone = 'brand' | 'info' | 'success' | 'warning' | 'error' | 'neutral';
70
+
71
+ // Static token maps so the build-time token fallback plugin can inject
72
+ // fallbacks into each `var()` call. Using literal strings keeps the
73
+ // `@wordpress/no-unknown-ds-tokens` lint rule happy.
74
+ const bgTokens: Record< Tone, string > = {
75
+ brand: 'var(--wpds-color-background-surface-brand)',
76
+ info: 'var(--wpds-color-background-surface-info)',
77
+ success: 'var(--wpds-color-background-surface-success)',
78
+ warning: 'var(--wpds-color-background-surface-warning)',
79
+ error: 'var(--wpds-color-background-surface-error)',
80
+ neutral: 'var(--wpds-color-background-surface-neutral-weak)',
81
+ };
82
+
83
+ const fgTokens: Record< Tone, string > = {
84
+ // `brand` has no dedicated fg-content token in the design system,
85
+ // so neutral content reads safely against the brand surface tint.
86
+ brand: 'var(--wpds-color-foreground-content-neutral)',
87
+ info: 'var(--wpds-color-foreground-content-info)',
88
+ success: 'var(--wpds-color-foreground-content-success)',
89
+ warning: 'var(--wpds-color-foreground-content-warning)',
90
+ error: 'var(--wpds-color-foreground-content-error)',
91
+ neutral: 'var(--wpds-color-foreground-content-neutral)',
92
+ };
93
+
94
+ function Tile( {
95
+ tone,
96
+ children,
97
+ actionableArea,
98
+ ...props
99
+ }: {
100
+ tone: Tone;
101
+ children: React.ReactNode;
102
+ actionableArea?: React.ReactNode;
103
+ } & React.HTMLAttributes< HTMLDivElement > ) {
104
+ return (
105
+ <div
106
+ { ...props }
107
+ style={ {
108
+ backgroundColor: bgTokens[ tone ],
109
+ color: fgTokens[ tone ],
110
+ padding: '20px',
111
+ display: 'flex',
112
+ alignItems: 'center',
113
+ justifyContent: 'center',
114
+ height: '100%',
115
+ boxSizing: 'border-box',
116
+ fontFamily: 'var(--wpds-typography-font-family-body)',
117
+ fontSize: 'var(--wpds-typography-font-size-sm)',
118
+ ...props?.style,
119
+ } }
120
+ >
121
+ { children }
122
+ </div>
123
+ );
124
+ }
125
+
126
+ function TileActions( {
127
+ isFill,
128
+ isFull,
129
+ onToggleFill,
130
+ onToggleFull,
131
+ onRemove,
132
+ }: {
133
+ isFill: boolean;
134
+ isFull: boolean;
135
+ onToggleFill: () => void;
136
+ onToggleFull: () => void;
137
+ onRemove: () => void;
138
+ } ) {
139
+ return (
140
+ <div
141
+ style={ {
142
+ position: 'absolute',
143
+ display: 'flex',
144
+ gap: 4,
145
+ top: 4,
146
+ right: 4,
147
+ zIndex: 2,
148
+ } }
149
+ >
150
+ <IconButton
151
+ size="small"
152
+ variant="solid"
153
+ tone={ isFill ? 'brand' : 'neutral' }
154
+ icon={ justifyStretch }
155
+ label="Fill available width"
156
+ aria-pressed={ isFill }
157
+ onClick={ onToggleFill }
158
+ />
159
+
160
+ <IconButton
161
+ size="small"
162
+ variant="solid"
163
+ tone={ isFull ? 'brand' : 'neutral' }
164
+ icon={ stretchFullWidth }
165
+ label="Full width"
166
+ aria-pressed={ isFull }
167
+ onClick={ onToggleFull }
168
+ />
169
+
170
+ <IconButton
171
+ size="small"
172
+ variant="solid"
173
+ tone="neutral"
174
+ icon={ close }
175
+ label="Remove"
176
+ onClick={ onRemove }
177
+ />
178
+ </div>
179
+ );
180
+ }
181
+
182
+ function formatTileLabel( item: DashboardGridLayoutItem ): string {
183
+ let width: string;
184
+ if ( item.width === 'fill' ) {
185
+ width = 'width: "fill"';
186
+ } else if ( item.width === 'full' ) {
187
+ width = 'width: "full"';
188
+ } else {
189
+ width = `width: ${ item.width ?? 1 }`;
190
+ }
191
+ const height = ( item.height ?? 1 ) > 1 ? `, height: ${ item.height }` : '';
192
+ return width + height;
193
+ }
194
+
195
+ // Static token maps so the build-time token fallback plugin can inject
196
+ // fallbacks into each `var()` call.
197
+ const panelBgTokens: Record< 'warning' | 'success', string > = {
198
+ warning: 'var(--wpds-color-background-surface-warning)',
199
+ success: 'var(--wpds-color-background-surface-success)',
200
+ };
201
+
202
+ const panelFgTokens: Record< 'warning' | 'success', string > = {
203
+ warning: 'var(--wpds-color-foreground-content-warning)',
204
+ success: 'var(--wpds-color-foreground-content-success)',
205
+ };
206
+
207
+ const panelStrokeTokens: Record< 'warning' | 'success', string > = {
208
+ warning: 'var(--wpds-color-stroke-surface-warning)',
209
+ success: 'var(--wpds-color-stroke-surface-success)',
210
+ };
211
+
212
+ function LayoutStatePanel( {
213
+ label,
214
+ layout,
215
+ tone,
216
+ }: {
217
+ label: string;
218
+ layout: DashboardGridLayoutItem[];
219
+ tone: 'warning' | 'success';
220
+ } ) {
221
+ return (
222
+ <Stack
223
+ direction="column"
224
+ gap="sm"
225
+ style={ {
226
+ width: 280,
227
+ padding: 16,
228
+ background: panelBgTokens[ tone ],
229
+ border: `1px solid ${ panelStrokeTokens[ tone ] }`,
230
+ borderRadius: 8,
231
+ fontFamily: 'var(--wpds-typography-font-family-mono)',
232
+ fontSize: 12,
233
+ color: panelFgTokens[ tone ],
234
+ } }
235
+ >
236
+ <strong
237
+ style={ {
238
+ fontFamily: 'var(--wpds-typography-font-family-body)',
239
+ fontSize: 11,
240
+ textTransform: 'uppercase',
241
+ letterSpacing: '0.04em',
242
+ } }
243
+ >
244
+ { label }
245
+ </strong>
246
+ <pre
247
+ style={ {
248
+ margin: 0,
249
+ overflow: 'auto',
250
+ lineHeight: 1.5,
251
+ } }
252
+ >
253
+ { JSON.stringify( layout, null, 2 ) }
254
+ </pre>
255
+ </Stack>
256
+ );
257
+ }
258
+
259
+ /**
260
+ * Static grid with a fixed number of columns. Each item declares its
261
+ * column span via `width`. Items flow left-to-right and wrap to new
262
+ * rows as the total exceeds `columns`.
263
+ */
264
+ export const Default: Story = {
265
+ args: {
266
+ layout: [
267
+ { key: 'a', width: 1 },
268
+ { key: 'b', width: 3 },
269
+ { key: 'c', width: 2 },
270
+ { key: 'd', width: 4 },
271
+ { key: 'e', width: 2 },
272
+ ],
273
+ columns: 6,
274
+ children: [
275
+ <Tile key="a" tone="brand">
276
+ width: 1
277
+ </Tile>,
278
+ <Tile key="b" tone="info">
279
+ width: 3
280
+ </Tile>,
281
+ <Tile key="c" tone="success">
282
+ width: 2
283
+ </Tile>,
284
+ <Tile key="d" tone="warning">
285
+ width: 4
286
+ </Tile>,
287
+ <Tile key="e" tone="error">
288
+ width: 2
289
+ </Tile>,
290
+ ],
291
+ },
292
+ };
293
+
294
+ /**
295
+ * Responsive grid: the column count is derived from the container
296
+ * width using `minColumnWidth` as the lower bound per column. A
297
+ * `ResizeObserver` recomputes the count on container resize.
298
+ */
299
+ export const Responsive: Story = {
300
+ args: {
301
+ layout: [
302
+ { key: 'a', width: 1, order: 1 },
303
+ { key: 'b', width: 2, order: 2 },
304
+ { key: 'c', width: 2, order: 3 },
305
+ { key: 'd', width: 1, order: 4 },
306
+ { key: 'e', width: 2, order: 5 },
307
+ { key: 'f', width: 2, order: 6 },
308
+ ],
309
+ rowHeight: 96,
310
+ minColumnWidth: 192,
311
+ children: [
312
+ <Tile key="a" tone="brand">
313
+ width: 1
314
+ </Tile>,
315
+ <Tile key="b" tone="info">
316
+ width: 2
317
+ </Tile>,
318
+ <Tile key="c" tone="success">
319
+ width: 2
320
+ </Tile>,
321
+ <Tile key="d" tone="warning">
322
+ width: 1
323
+ </Tile>,
324
+ <Tile key="e" tone="error">
325
+ width: 2
326
+ </Tile>,
327
+ <Tile key="f" tone="neutral">
328
+ width: 2
329
+ </Tile>,
330
+ ],
331
+ },
332
+ };
333
+
334
+ /**
335
+ * Layered configuration: `columns` caps the count and
336
+ * `minColumnWidth` enforces a per-tile width floor. The grid renders
337
+ * up to `columns` columns on wide containers and reduces the count
338
+ * on narrow ones whenever fitting all of them would push tiles
339
+ * below `minColumnWidth`. Resize the preview to see the cap apply
340
+ * on wide widths and the floor reduce the count on narrow widths.
341
+ */
342
+ export const Layered: Story = {
343
+ args: {
344
+ layout: [
345
+ { key: 'a', width: 1, order: 1 },
346
+ { key: 'b', width: 2, order: 2 },
347
+ { key: 'c', width: 2, order: 3 },
348
+ { key: 'd', width: 1, order: 4 },
349
+ { key: 'e', width: 2, order: 5 },
350
+ { key: 'f', width: 2, order: 6 },
351
+ ],
352
+ rowHeight: 96,
353
+ columns: 6,
354
+ minColumnWidth: 240,
355
+ children: [
356
+ <Tile key="a" tone="brand">
357
+ width: 1
358
+ </Tile>,
359
+ <Tile key="b" tone="info">
360
+ width: 2
361
+ </Tile>,
362
+ <Tile key="c" tone="success">
363
+ width: 2
364
+ </Tile>,
365
+ <Tile key="d" tone="warning">
366
+ width: 1
367
+ </Tile>,
368
+ <Tile key="e" tone="error">
369
+ width: 2
370
+ </Tile>,
371
+ <Tile key="f" tone="neutral">
372
+ width: 2
373
+ </Tile>,
374
+ ],
375
+ },
376
+ };
377
+
378
+ /**
379
+ * A `width: 'fill'` item expands to cover the remaining columns in
380
+ * its row. Mix it with fixed-width items on either side to build
381
+ * sidebar-like layouts that adapt to the column count.
382
+ */
383
+ export const FillWidth: Story = {
384
+ args: {
385
+ layout: [
386
+ { key: 'left', width: 1 },
387
+ { key: 'fill', width: 'fill' },
388
+ { key: 'right', width: 2 },
389
+ { key: 'solo', width: 'fill' },
390
+ ],
391
+ columns: 6,
392
+ children: [
393
+ <Tile key="left" tone="brand">
394
+ width: 1
395
+ </Tile>,
396
+ <Tile key="fill" tone="info">
397
+ width: &quot;fill&quot;
398
+ </Tile>,
399
+ <Tile key="right" tone="success">
400
+ width: 2
401
+ </Tile>,
402
+ <Tile key="solo" tone="warning">
403
+ width: &quot;fill&quot; (alone in row)
404
+ </Tile>,
405
+ ],
406
+ },
407
+ };
408
+
409
+ /**
410
+ * A `width: 'full'` item spans every column (`grid-column: 1 / -1`),
411
+ * forcing a row break around it. Useful for dividers, hero banners,
412
+ * or embedded content that should always take the full width.
413
+ */
414
+ export const FullWidth: Story = {
415
+ args: {
416
+ layout: [
417
+ { key: 'a', width: 2 },
418
+ { key: 'b', width: 4 },
419
+ { key: 'hero', width: 'full', height: 1 },
420
+ { key: 'c', width: 3 },
421
+ { key: 'd', width: 3 },
422
+ ],
423
+ columns: 6,
424
+ children: [
425
+ <Tile key="a" tone="brand">
426
+ width: 2
427
+ </Tile>,
428
+ <Tile key="b" tone="info">
429
+ width: 4
430
+ </Tile>,
431
+ <Tile key="hero" tone="success">
432
+ width: &quot;full&quot;
433
+ </Tile>,
434
+ <Tile key="c" tone="warning">
435
+ width: 3
436
+ </Tile>,
437
+ <Tile key="d" tone="error">
438
+ width: 3
439
+ </Tile>,
440
+ ],
441
+ },
442
+ };
443
+
444
+ /**
445
+ * Numeric `rowHeight` lets items span multiple rows via `height`.
446
+ * Combined with `width`, this produces tile-based dashboards where
447
+ * each cell can be tuned independently.
448
+ */
449
+ export const RowHeight: Story = {
450
+ args: {
451
+ layout: [
452
+ { key: 'a', width: 2, height: 2, order: 1 },
453
+ { key: 'b', width: 2, height: 1, order: 2 },
454
+ { key: 'c', width: 2, height: 3, order: 3 },
455
+ { key: 'd', width: 4, height: 1, order: 4 },
456
+ { key: 'e', width: 2, height: 1, order: 5 },
457
+ ],
458
+ columns: 6,
459
+ rowHeight: 80,
460
+ children: [
461
+ <Tile key="a" tone="brand">
462
+ 2 cols × 2 rows
463
+ </Tile>,
464
+ <Tile key="b" tone="info">
465
+ 2 cols × 1 row
466
+ </Tile>,
467
+ <Tile key="c" tone="success">
468
+ 2 cols × 3 rows
469
+ </Tile>,
470
+ <Tile key="d" tone="warning">
471
+ 4 cols × 1 row
472
+ </Tile>,
473
+ <Tile key="e" tone="error">
474
+ 2 cols × 1 row
475
+ </Tile>,
476
+ ],
477
+ },
478
+ };
479
+
480
+ /**
481
+ * Edit mode with drag, resize, and all width modes. While `editMode`
482
+ * is on, `<DashboardGrid />` paints its default overlay behind the
483
+ * tiles to visualize the underlying template: rounded row-marker
484
+ * tiles in each column when `rowHeight` is numeric. The overlay
485
+ * disappears when `editMode` flips back to `false`.
486
+ *
487
+ * Theme the default look in place via `--wp-grid-overlay-tile-bg`,
488
+ * or replace the visual wholesale by passing `renderGridOverlay`.
489
+ * See the `Custom Grid Overlay` story for a full override example.
490
+ *
491
+ * A state panel shows the raw layout JSON. Drag items to reorder;
492
+ * resize from the bottom-right handle. Keyboard sensor is enabled:
493
+ * use Tab to focus an item, Space to grab, arrow keys to move, Space
494
+ * to drop.
495
+ */
496
+ export const EditMode: Story = {
497
+ args: {
498
+ columns: 12,
499
+ rowHeight: 80,
500
+ editMode: true,
501
+ },
502
+
503
+ render: function EditModeStory( args ) {
504
+ const initialLayout: ( DashboardGridLayoutItem & {
505
+ tone: Tone;
506
+ } )[] = [
507
+ {
508
+ key: 'fixed-1',
509
+ width: 1,
510
+ height: 1,
511
+ order: 1,
512
+ tone: 'success',
513
+ },
514
+ {
515
+ key: 'fixed-1-1',
516
+ width: 5,
517
+ height: 1,
518
+ order: 2,
519
+ tone: 'info',
520
+ },
521
+ {
522
+ key: 'fixed-2',
523
+ width: 5,
524
+ height: 1,
525
+ order: 3,
526
+ tone: 'brand',
527
+ },
528
+ {
529
+ key: 'full',
530
+ width: 'full',
531
+ height: 1,
532
+ order: 4,
533
+ tone: 'neutral',
534
+ },
535
+ {
536
+ key: 'fixed-3',
537
+ width: 2,
538
+ height: 1,
539
+ order: 5,
540
+ tone: 'warning',
541
+ },
542
+ {
543
+ key: 'fixed-4',
544
+ width: 2,
545
+ height: 1,
546
+ order: 6,
547
+ tone: 'error',
548
+ },
549
+ ];
550
+
551
+ const [ tiles, setTiles ] = useState( initialLayout );
552
+ const [ previewLayout, setPreviewLayout ] = useState<
553
+ DashboardGridLayoutItem[] | null
554
+ >( null );
555
+
556
+ const layout: DashboardGridLayoutItem[] = tiles.map(
557
+ ( { tone: _tone, ...item } ) => item
558
+ );
559
+
560
+ const onChangeLayout = ( next: DashboardGridLayoutItem[] ) => {
561
+ setTiles(
562
+ next.map( ( item ) => {
563
+ const existing = tiles.find( ( t ) => t.key === item.key );
564
+ return {
565
+ ...item,
566
+ tone: existing?.tone ?? 'neutral',
567
+ };
568
+ } )
569
+ );
570
+ setPreviewLayout( null );
571
+ };
572
+
573
+ const removeTile = ( key: string ) => {
574
+ setTiles( tiles.filter( ( tile ) => tile.key !== key ) );
575
+ };
576
+
577
+ const toggleFill = ( key: string ) => {
578
+ setTiles(
579
+ tiles.map( ( tile ) =>
580
+ tile.key === key
581
+ ? {
582
+ ...tile,
583
+ width:
584
+ tile.width === 'fill' ? undefined : 'fill',
585
+ }
586
+ : tile
587
+ )
588
+ );
589
+ };
590
+
591
+ const toggleFull = ( key: string ) => {
592
+ setTiles(
593
+ tiles.map( ( tile ) =>
594
+ tile.key === key
595
+ ? {
596
+ ...tile,
597
+ width:
598
+ tile.width === 'full' ? undefined : 'full',
599
+ }
600
+ : tile
601
+ )
602
+ );
603
+ };
604
+
605
+ // Memoize the Tile elements so the grid's `children` prop keeps
606
+ // a stable reference across parent re-renders driven by
607
+ // onPreviewLayout. Without this, every preview tick produces a
608
+ // fresh array of elements and the grid's children walk has to
609
+ // re-run on each frame of a resize gesture.
610
+ const tileElements = useMemo(
611
+ () =>
612
+ tiles.map( ( tile ) => (
613
+ <Tile
614
+ key={ tile.key }
615
+ tone={ tile.tone }
616
+ actionableArea={
617
+ <TileActions
618
+ isFill={ tile.width === 'fill' }
619
+ isFull={ tile.width === 'full' }
620
+ onToggleFill={ () => toggleFill( tile.key ) }
621
+ onToggleFull={ () => toggleFull( tile.key ) }
622
+ onRemove={ () => removeTile( tile.key ) }
623
+ />
624
+ }
625
+ >
626
+ { formatTileLabel( tile ) }
627
+ </Tile>
628
+ ) ),
629
+ // eslint-disable-next-line react-hooks/exhaustive-deps
630
+ [ tiles ]
631
+ );
632
+
633
+ return (
634
+ <Stack direction="row" gap="lg" align="flex-start">
635
+ <div style={ { width: '800px' } }>
636
+ <DashboardGrid
637
+ { ...args }
638
+ layout={ layout }
639
+ onChangeLayout={ onChangeLayout }
640
+ onPreviewLayout={ setPreviewLayout }
641
+ >
642
+ { tileElements }
643
+ </DashboardGrid>
644
+ </div>
645
+
646
+ <LayoutStatePanel
647
+ label={ previewLayout ? 'Staging' : 'Committed' }
648
+ layout={ previewLayout ?? layout }
649
+ tone={ previewLayout ? 'warning' : 'success' }
650
+ />
651
+ </Stack>
652
+ );
653
+ },
654
+ };
655
+
656
+ /**
657
+ * Custom corner-resize glyph: a diagonal line plus a filled triangle,
658
+ * both leaning toward the bottom-right corner of the tile.
659
+ */
660
+ const resizeCornerSE = (
661
+ <svg
662
+ xmlns="http://www.w3.org/2000/svg"
663
+ viewBox="0 0 24 24"
664
+ aria-hidden="true"
665
+ >
666
+ <path
667
+ d="M0 24L24 0"
668
+ stroke="currentColor"
669
+ strokeWidth="3"
670
+ strokeLinecap="round"
671
+ fill="none"
672
+ />
673
+
674
+ <polygon points="24,24 10,24 24,10" fill="currentColor" />
675
+ </svg>
676
+ );
677
+
678
+ /**
679
+ * Override the default corner-triangle resize handle with a custom
680
+ * element via `renderResizeHandle`. The grid keeps the gesture
681
+ * machinery (dnd-kit context, throttled delta loop) and passes the
682
+ * wiring (`ref`, `listeners`, `attributes`) to the consumer — so the
683
+ * custom visual still drives the same resize behavior.
684
+ */
685
+ function CustomResizeHandle( {
686
+ ref,
687
+ listeners,
688
+ attributes,
689
+ isResizing,
690
+ }: ResizeHandleRenderProps ) {
691
+ return (
692
+ <div
693
+ ref={ ref }
694
+ { ...listeners }
695
+ { ...attributes }
696
+ style={ {
697
+ position: 'absolute',
698
+ bottom: 0,
699
+ insetInlineEnd: 0,
700
+ display: 'flex',
701
+ cursor: 'nwse-resize',
702
+ opacity: isResizing ? 0.5 : 1,
703
+ transition: 'opacity 120ms ease',
704
+ } }
705
+ >
706
+ <Icon icon={ resizeCornerSE } size={ 16 } />
707
+ </div>
708
+ );
709
+ }
710
+
711
+ /**
712
+ * Example `renderDragPreview` wrapper: keeps the clone height chain
713
+ * intact. Lift, shadow, and motion live on the grid
714
+ * `.drag-preview-frame`; set `--wp-grid-drag-preview-radius` on the
715
+ * surface when the lift shadow should match rounded tiles (see widget
716
+ * dashboard).
717
+ */
718
+ function CustomDragPreview( { children }: DragPreviewRenderProps ) {
719
+ return <div style={ { height: '100%' } }>{ children }</div>;
720
+ }
721
+
722
+ /**
723
+ * Exercises the three customization vectors on a single grid:
724
+ *
725
+ * 1. `renderResizeHandle` swaps the default corner triangle for a
726
+ * custom diagonal-arrow icon.
727
+ * 2. `renderDragPreview` wraps the dragged clone (here only for the
728
+ * height chain; lift and shadow stay on the grid frame).
729
+ * 3. CSS custom properties on an ancestor retheme the lift scale,
730
+ * placeholder opacity, placeholder outline color, and placeholder
731
+ * border-radius without touching the package.
732
+ *
733
+ * Toggle `editMode`, then drag and resize a tile to see all three
734
+ * respond.
735
+ */
736
+ export const Customization: Story = {
737
+ args: {
738
+ columns: 6,
739
+ rowHeight: 80,
740
+ editMode: true,
741
+ layout: [
742
+ { key: 'a', width: 2, height: 1 },
743
+ { key: 'b', width: 4, height: 1 },
744
+ { key: 'c', width: 3, height: 2 },
745
+ { key: 'd', width: 3, height: 1 },
746
+ { key: 'e', width: 3, height: 1 },
747
+ ],
748
+ },
749
+ render: function CustomizationRender( args ) {
750
+ const [ layout, setLayout ] = useState< DashboardGridLayoutItem[] >(
751
+ args.layout
752
+ );
753
+
754
+ const tiles = useMemo(
755
+ () => [
756
+ <Tile key="a" tone="brand">
757
+ A
758
+ </Tile>,
759
+ <Tile key="b" tone="info">
760
+ B
761
+ </Tile>,
762
+ <Tile key="c" tone="success">
763
+ C
764
+ </Tile>,
765
+ <Tile key="d" tone="warning">
766
+ D
767
+ </Tile>,
768
+ <Tile key="e" tone="error">
769
+ E
770
+ </Tile>,
771
+ ],
772
+ []
773
+ );
774
+
775
+ const customTokens = {
776
+ '--wp-grid-drag-preview-scale': '1.08',
777
+ '--wp-grid-placeholder-opacity': '0.2',
778
+ '--wp-grid-placeholder-outline-color':
779
+ 'var(--wpds-color-foreground-content-warning)',
780
+ '--wp-grid-placeholder-radius': '12px',
781
+ } as React.CSSProperties;
782
+
783
+ return (
784
+ <div style={ customTokens }>
785
+ <DashboardGrid
786
+ { ...args }
787
+ layout={ layout }
788
+ onChangeLayout={ setLayout }
789
+ renderResizeHandle={ CustomResizeHandle }
790
+ renderDragPreview={ CustomDragPreview }
791
+ >
792
+ { tiles }
793
+ </DashboardGrid>
794
+ </div>
795
+ );
796
+ },
797
+ };
798
+
799
+ /**
800
+ * Example custom overlay supplied to `<DashboardGrid />` through the
801
+ * `renderGridOverlay` prop. Receives the grid's resolved column
802
+ * count, gap, row height, and `isActive` flag; this implementation
803
+ * drops the row dividers, swaps to an info tone, labels each column
804
+ * track with its index, and fades in/out on `isActive` toggles. The
805
+ * grid always mounts the overlay; the consumer owns the visual and
806
+ * its transition.
807
+ *
808
+ * @param props Render props supplied by the grid.
809
+ * @param props.columns Number of column tracks to mirror.
810
+ * @param props.isActive Whether the overlay should be visible.
811
+ */
812
+ function NumberedOverlay( { columns, isActive }: GridOverlayRenderProps ) {
813
+ return (
814
+ <div
815
+ aria-hidden
816
+ style={ {
817
+ position: 'absolute',
818
+ inset: 0,
819
+ display: 'grid',
820
+ gridTemplateColumns: `repeat(${ columns }, minmax(0, 1fr))`,
821
+ gap: 'var(--wpds-dimension-gap-xl)',
822
+ pointerEvents: 'none',
823
+ opacity: isActive ? 1 : 0,
824
+ visibility: isActive ? 'visible' : 'hidden',
825
+ transition: isActive
826
+ ? 'opacity 200ms ease, visibility 0s linear 0s'
827
+ : 'opacity 200ms ease, visibility 0s linear 200ms',
828
+ backgroundImage: `repeating-linear-gradient(135deg, color-mix(in srgb, var(--wpds-color-background-surface-info) 24%, transparent) 0 6px, transparent 6px 12px)`,
829
+ } }
830
+ >
831
+ { Array.from( { length: columns } ).map( ( _, i ) => (
832
+ <div
833
+ key={ i }
834
+ style={ {
835
+ outline:
836
+ '1px dashed var(--wpds-color-stroke-surface-info)',
837
+ backgroundColor:
838
+ 'color-mix(in srgb, var(--wpds-color-background-surface-info) 10%, transparent)',
839
+ position: 'relative',
840
+ } }
841
+ >
842
+ <span
843
+ style={ {
844
+ position: 'absolute',
845
+ top: 4,
846
+ insetInlineStart: 4,
847
+ fontSize: 10,
848
+ padding: '1px 6px',
849
+ borderRadius: 2,
850
+ background:
851
+ 'var(--wpds-color-background-surface-info)',
852
+ color: 'var(--wpds-color-foreground-content-info)',
853
+ fontFamily:
854
+ 'var(--wpds-typography-font-family-mono)',
855
+ } }
856
+ >
857
+ { i + 1 }
858
+ </span>
859
+ </div>
860
+ ) ) }
861
+ </div>
862
+ );
863
+ }
864
+
865
+ /**
866
+ * Replaces the package's default edit-mode overlay with a custom
867
+ * visual through the `renderGridOverlay` prop. The grid mounts the
868
+ * supplied component as a sibling behind the tiles whenever
869
+ * `editMode` is on, passing the resolved `{ columns, rowHeight }`
870
+ * so the override can reproduce the column and row tracks
871
+ * pixel-accurately without re-deriving them.
872
+ *
873
+ * Here the override (see `NumberedOverlay` above) swaps the warning
874
+ * tone for info, drops the row dividers, and labels each column
875
+ * track with its index. Pass `renderGridOverlay={ () => null }` to
876
+ * suppress the overlay entirely while keeping `editMode` interactions
877
+ * on.
878
+ */
879
+ export const CustomGridOverlayStory: Story = {
880
+ name: 'Custom Grid Overlay',
881
+ args: {
882
+ columns: 12,
883
+ rowHeight: 80,
884
+ editMode: true,
885
+ layout: [
886
+ { key: 'a', width: 3, height: 1 },
887
+ { key: 'b', width: 5, height: 1 },
888
+ { key: 'c', width: 4, height: 1 },
889
+ { key: 'd', width: 2, height: 2 },
890
+ { key: 'e', width: 6, height: 1 },
891
+ ],
892
+ },
893
+ render: function CustomGridOverlayRender( args ) {
894
+ const [ layout, setLayout ] = useState< DashboardGridLayoutItem[] >(
895
+ args.layout
896
+ );
897
+
898
+ const tiles = useMemo(
899
+ () => [
900
+ <Tile key="a" tone="brand">
901
+ A
902
+ </Tile>,
903
+ <Tile key="b" tone="info">
904
+ B
905
+ </Tile>,
906
+ <Tile key="c" tone="success">
907
+ C
908
+ </Tile>,
909
+ <Tile key="d" tone="warning">
910
+ D
911
+ </Tile>,
912
+ <Tile key="e" tone="error">
913
+ E
914
+ </Tile>,
915
+ ],
916
+ []
917
+ );
918
+
919
+ return (
920
+ <DashboardGrid
921
+ { ...args }
922
+ layout={ layout }
923
+ onChangeLayout={ setLayout }
924
+ renderGridOverlay={ NumberedOverlay }
925
+ >
926
+ { tiles }
927
+ </DashboardGrid>
928
+ );
929
+ },
930
+ };