gbs-add-block 1.2.7 → 1.2.9
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/README.md +3 -3
- package/index.cjs +10 -9
- package/package.json +1 -1
- package/source/components/combobox/README.md +170 -0
- package/source/components/combobox/__tests__/core.test.ts +134 -0
- package/source/components/combobox/core/filter.ts +155 -0
- package/source/components/combobox/core/index.ts +16 -0
- package/source/components/combobox/core/types.ts +48 -0
- package/source/components/combobox/core/virtual.ts +67 -0
- package/source/components/combobox/index.ts +13 -0
- package/source/components/combobox/react/Listbox.tsx +302 -0
- package/source/components/combobox/react/MultiSelect.tsx +263 -0
- package/source/components/combobox/react/Popover.tsx +70 -0
- package/source/components/combobox/react/Select.tsx +198 -0
- package/source/components/combobox/react/icons.tsx +34 -0
- package/source/components/combobox/react/locale.ts +16 -0
- package/source/components/combobox/react/props.ts +86 -0
- package/source/components/combobox/react/useCombobox.ts +399 -0
- package/source/components/combobox/styles.css +417 -0
- package/source/components/datagridbeta/README.md +232 -0
- package/source/components/datagridbeta/__tests__/core.test.ts +341 -0
- package/source/components/datagridbeta/__tests__/export.test.ts +70 -0
- package/source/components/datagridbeta/core/columnHelper.ts +21 -0
- package/source/components/datagridbeta/core/columns.ts +288 -0
- package/source/components/datagridbeta/core/filtering.ts +237 -0
- package/source/components/datagridbeta/core/grid.ts +616 -0
- package/source/components/datagridbeta/core/index.ts +14 -0
- package/source/components/datagridbeta/core/rows.ts +64 -0
- package/source/components/datagridbeta/core/sorting.ts +111 -0
- package/source/components/datagridbeta/core/state.ts +82 -0
- package/source/components/datagridbeta/core/store.ts +52 -0
- package/source/components/datagridbeta/core/types.ts +266 -0
- package/source/components/datagridbeta/core/values.ts +98 -0
- package/source/components/datagridbeta/core/virtual.ts +85 -0
- package/source/components/datagridbeta/export/csv.ts +32 -0
- package/source/components/datagridbeta/export/download.ts +12 -0
- package/source/components/datagridbeta/export/pdf.ts +61 -0
- package/source/components/datagridbeta/export/table.ts +73 -0
- package/source/components/datagridbeta/export/xlsx.ts +145 -0
- package/source/components/datagridbeta/export/zip.ts +87 -0
- package/source/components/datagridbeta/index.ts +7 -0
- package/source/components/datagridbeta/react/Cell.tsx +282 -0
- package/source/components/datagridbeta/react/ColumnMenu.tsx +135 -0
- package/source/components/datagridbeta/react/DataGrid.tsx +296 -0
- package/source/components/datagridbeta/react/FilterForm.tsx +142 -0
- package/source/components/datagridbeta/react/HeaderRow.tsx +306 -0
- package/source/components/datagridbeta/react/Pagination.tsx +107 -0
- package/source/components/datagridbeta/react/Popover.tsx +79 -0
- package/source/components/datagridbeta/react/Row.tsx +85 -0
- package/source/components/datagridbeta/react/Toolbar.tsx +266 -0
- package/source/components/datagridbeta/react/Viewport.tsx +167 -0
- package/source/components/datagridbeta/react/context.ts +57 -0
- package/source/components/datagridbeta/react/hooks.ts +80 -0
- package/source/components/datagridbeta/react/icons.tsx +72 -0
- package/source/components/datagridbeta/react/keyboard.ts +110 -0
- package/source/components/datagridbeta/react/locale.ts +128 -0
- package/source/components/datagridbeta/styles.css +717 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Combobox styles (Select and MultiSelect).
|
|
3
|
+
*
|
|
4
|
+
* Tokens default to the DataGrid's --dg-* variables when that stylesheet is
|
|
5
|
+
* present, so both components share one theme, and fall back to the same
|
|
6
|
+
* palette when used on their own. Rules live in the `components` layer, so
|
|
7
|
+
* utility classes passed through `className` / `classNames` override them.
|
|
8
|
+
*/
|
|
9
|
+
@layer theme, base, components, utilities;
|
|
10
|
+
|
|
11
|
+
@layer components {
|
|
12
|
+
.cb-root {
|
|
13
|
+
--cb-bg: var(--dg-bg, light-dark(#ffffff, #0b0b0e));
|
|
14
|
+
--cb-fg: var(--dg-fg, light-dark(#18181b, #f4f4f5));
|
|
15
|
+
--cb-muted: var(--dg-muted, light-dark(#71717a, #a1a1aa));
|
|
16
|
+
--cb-border: var(--dg-border, light-dark(#e4e4e7, #27272a));
|
|
17
|
+
--cb-hover: var(--dg-hover, light-dark(#f4f4f5, #1f1f23));
|
|
18
|
+
--cb-input-bg: var(--dg-input-bg, light-dark(#ffffff, #121216));
|
|
19
|
+
--cb-accent: var(--dg-accent, light-dark(#2563eb, #60a5fa));
|
|
20
|
+
--cb-accent-fg: var(--dg-accent-fg, light-dark(#ffffff, #0b1220));
|
|
21
|
+
--cb-accent-soft: var(--dg-accent-soft, light-dark(#eff6ff, #172554));
|
|
22
|
+
--cb-accent-strong: var(--dg-accent-strong, light-dark(#1d4ed8, #bfdbfe));
|
|
23
|
+
--cb-focus: var(--dg-focus, light-dark(#2563eb, #60a5fa));
|
|
24
|
+
--cb-danger: var(--dg-danger, light-dark(#dc2626, #f87171));
|
|
25
|
+
--cb-shadow: var(--dg-shadow, 0 10px 30px -8px light-dark(rgb(0 0 0 / 0.18), rgb(0 0 0 / 0.6)));
|
|
26
|
+
--cb-radius: var(--dg-radius, 8px);
|
|
27
|
+
--cb-font-size: var(--dg-font-size, 13px);
|
|
28
|
+
--cb-height: 36px;
|
|
29
|
+
--cb-px: 10px;
|
|
30
|
+
|
|
31
|
+
color-scheme: inherit;
|
|
32
|
+
position: relative;
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
gap: 4px;
|
|
36
|
+
width: 100%;
|
|
37
|
+
min-width: 0;
|
|
38
|
+
font-size: var(--cb-font-size);
|
|
39
|
+
line-height: 1.4;
|
|
40
|
+
color: var(--cb-fg);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
:where(.dark, [data-theme="dark"]) .cb-root {
|
|
44
|
+
color-scheme: dark;
|
|
45
|
+
}
|
|
46
|
+
:where(.light, [data-theme="light"]) .cb-root {
|
|
47
|
+
color-scheme: light;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
:where(.cb-root, .cb-popover) *,
|
|
51
|
+
:where(.cb-root, .cb-popover) *::before,
|
|
52
|
+
:where(.cb-root, .cb-popover) *::after {
|
|
53
|
+
box-sizing: border-box;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.cb-root[data-size="sm"] {
|
|
57
|
+
--cb-height: 30px;
|
|
58
|
+
--cb-font-size: 12px;
|
|
59
|
+
--cb-px: 8px;
|
|
60
|
+
}
|
|
61
|
+
.cb-root[data-size="lg"] {
|
|
62
|
+
--cb-height: 44px;
|
|
63
|
+
--cb-font-size: 14px;
|
|
64
|
+
--cb-px: 12px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.cb-label {
|
|
68
|
+
font-weight: 500;
|
|
69
|
+
}
|
|
70
|
+
.cb-label[data-required]::after {
|
|
71
|
+
content: " *";
|
|
72
|
+
color: var(--cb-danger);
|
|
73
|
+
}
|
|
74
|
+
.cb-description {
|
|
75
|
+
color: var(--cb-muted);
|
|
76
|
+
font-size: 0.92em;
|
|
77
|
+
}
|
|
78
|
+
.cb-error {
|
|
79
|
+
color: var(--cb-danger);
|
|
80
|
+
font-size: 0.92em;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* -------------------------------------------------------------- trigger */
|
|
84
|
+
|
|
85
|
+
.cb-control {
|
|
86
|
+
display: flex;
|
|
87
|
+
align-items: center;
|
|
88
|
+
gap: 6px;
|
|
89
|
+
width: 100%;
|
|
90
|
+
min-height: var(--cb-height);
|
|
91
|
+
padding: 3px var(--cb-px);
|
|
92
|
+
border: 1px solid var(--cb-border);
|
|
93
|
+
border-radius: var(--cb-radius);
|
|
94
|
+
background: var(--cb-input-bg);
|
|
95
|
+
color: inherit;
|
|
96
|
+
font: inherit;
|
|
97
|
+
text-align: start;
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
}
|
|
100
|
+
.cb-control:hover:not([data-disabled]) {
|
|
101
|
+
border-color: color-mix(in oklab, var(--cb-border), var(--cb-fg) 25%);
|
|
102
|
+
}
|
|
103
|
+
.cb-control:focus-visible,
|
|
104
|
+
.cb-root[data-state="open"] .cb-control {
|
|
105
|
+
outline: 2px solid var(--cb-focus);
|
|
106
|
+
outline-offset: -1px;
|
|
107
|
+
}
|
|
108
|
+
.cb-control[data-invalid] {
|
|
109
|
+
border-color: var(--cb-danger);
|
|
110
|
+
}
|
|
111
|
+
.cb-control[data-invalid]:focus-visible,
|
|
112
|
+
.cb-root[data-invalid][data-state="open"] .cb-control {
|
|
113
|
+
outline-color: var(--cb-danger);
|
|
114
|
+
}
|
|
115
|
+
.cb-control[data-disabled] {
|
|
116
|
+
opacity: 0.55;
|
|
117
|
+
cursor: not-allowed;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.cb-value {
|
|
121
|
+
flex: 1 1 auto;
|
|
122
|
+
min-width: 0;
|
|
123
|
+
overflow: hidden;
|
|
124
|
+
text-overflow: ellipsis;
|
|
125
|
+
white-space: nowrap;
|
|
126
|
+
}
|
|
127
|
+
.cb-placeholder {
|
|
128
|
+
color: var(--cb-muted);
|
|
129
|
+
}
|
|
130
|
+
.cb-value-icon {
|
|
131
|
+
display: inline-flex;
|
|
132
|
+
margin-inline-end: 6px;
|
|
133
|
+
vertical-align: -2px;
|
|
134
|
+
color: var(--cb-muted);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.cb-tags {
|
|
138
|
+
display: flex;
|
|
139
|
+
flex: 1 1 auto;
|
|
140
|
+
flex-wrap: wrap;
|
|
141
|
+
align-items: center;
|
|
142
|
+
gap: 4px;
|
|
143
|
+
min-width: 0;
|
|
144
|
+
}
|
|
145
|
+
.cb-tag {
|
|
146
|
+
display: inline-flex;
|
|
147
|
+
align-items: center;
|
|
148
|
+
gap: 2px;
|
|
149
|
+
max-width: 100%;
|
|
150
|
+
padding-inline: 8px 3px;
|
|
151
|
+
border-radius: 999px;
|
|
152
|
+
background: var(--cb-accent-soft);
|
|
153
|
+
color: var(--cb-accent-strong);
|
|
154
|
+
font-size: 0.92em;
|
|
155
|
+
line-height: 20px;
|
|
156
|
+
}
|
|
157
|
+
.cb-tag-label {
|
|
158
|
+
overflow: hidden;
|
|
159
|
+
text-overflow: ellipsis;
|
|
160
|
+
white-space: nowrap;
|
|
161
|
+
}
|
|
162
|
+
.cb-tag-remove {
|
|
163
|
+
display: inline-grid;
|
|
164
|
+
place-items: center;
|
|
165
|
+
width: 16px;
|
|
166
|
+
height: 16px;
|
|
167
|
+
padding: 0;
|
|
168
|
+
border: 0;
|
|
169
|
+
border-radius: 50%;
|
|
170
|
+
background: transparent;
|
|
171
|
+
color: inherit;
|
|
172
|
+
cursor: pointer;
|
|
173
|
+
}
|
|
174
|
+
.cb-tag-remove:hover {
|
|
175
|
+
background: color-mix(in oklab, var(--cb-accent-strong), transparent 85%);
|
|
176
|
+
}
|
|
177
|
+
.cb-tag-overflow {
|
|
178
|
+
color: var(--cb-muted);
|
|
179
|
+
font-size: 0.92em;
|
|
180
|
+
white-space: nowrap;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.cb-actions {
|
|
184
|
+
display: flex;
|
|
185
|
+
flex: none;
|
|
186
|
+
align-items: center;
|
|
187
|
+
gap: 2px;
|
|
188
|
+
color: var(--cb-muted);
|
|
189
|
+
}
|
|
190
|
+
.cb-icon-button {
|
|
191
|
+
display: inline-grid;
|
|
192
|
+
place-items: center;
|
|
193
|
+
width: 22px;
|
|
194
|
+
height: 22px;
|
|
195
|
+
padding: 0;
|
|
196
|
+
border: 0;
|
|
197
|
+
border-radius: 4px;
|
|
198
|
+
background: transparent;
|
|
199
|
+
color: inherit;
|
|
200
|
+
cursor: pointer;
|
|
201
|
+
}
|
|
202
|
+
.cb-icon-button:hover {
|
|
203
|
+
background: var(--cb-hover);
|
|
204
|
+
color: var(--cb-fg);
|
|
205
|
+
}
|
|
206
|
+
.cb-icon-button:focus-visible {
|
|
207
|
+
outline: 2px solid var(--cb-focus);
|
|
208
|
+
outline-offset: 1px;
|
|
209
|
+
}
|
|
210
|
+
.cb-chevron {
|
|
211
|
+
transition: transform 120ms ease;
|
|
212
|
+
}
|
|
213
|
+
.cb-root[data-state="open"] .cb-chevron {
|
|
214
|
+
transform: rotate(180deg);
|
|
215
|
+
}
|
|
216
|
+
@media (prefers-reduced-motion: reduce) {
|
|
217
|
+
.cb-chevron {
|
|
218
|
+
transition: none;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* -------------------------------------------------------------- popover */
|
|
223
|
+
|
|
224
|
+
.cb-popover {
|
|
225
|
+
position: fixed;
|
|
226
|
+
inset: auto;
|
|
227
|
+
margin: 0;
|
|
228
|
+
padding: 0;
|
|
229
|
+
overflow: hidden;
|
|
230
|
+
border: 1px solid var(--cb-border);
|
|
231
|
+
border-radius: var(--cb-radius);
|
|
232
|
+
background: var(--cb-bg);
|
|
233
|
+
color: var(--cb-fg);
|
|
234
|
+
box-shadow: var(--cb-shadow);
|
|
235
|
+
font-size: var(--cb-font-size);
|
|
236
|
+
}
|
|
237
|
+
.cb-search {
|
|
238
|
+
display: flex;
|
|
239
|
+
align-items: center;
|
|
240
|
+
gap: 6px;
|
|
241
|
+
padding: 6px 10px;
|
|
242
|
+
border-bottom: 1px solid var(--cb-border);
|
|
243
|
+
color: var(--cb-muted);
|
|
244
|
+
}
|
|
245
|
+
.cb-search input {
|
|
246
|
+
width: 100%;
|
|
247
|
+
min-width: 0;
|
|
248
|
+
border: 0;
|
|
249
|
+
outline: 0;
|
|
250
|
+
background: transparent;
|
|
251
|
+
color: var(--cb-fg);
|
|
252
|
+
font: inherit;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.cb-list {
|
|
256
|
+
position: relative;
|
|
257
|
+
overflow: auto;
|
|
258
|
+
overscroll-behavior: contain;
|
|
259
|
+
padding: 4px;
|
|
260
|
+
outline: none;
|
|
261
|
+
}
|
|
262
|
+
.cb-list:focus-visible {
|
|
263
|
+
outline: 2px solid var(--cb-focus);
|
|
264
|
+
outline-offset: -2px;
|
|
265
|
+
}
|
|
266
|
+
.cb-list[data-virtual] .cb-item,
|
|
267
|
+
.cb-list[data-virtual] .cb-group {
|
|
268
|
+
position: absolute;
|
|
269
|
+
inset-inline: 4px;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.cb-group {
|
|
273
|
+
display: flex;
|
|
274
|
+
align-items: center;
|
|
275
|
+
padding: 6px 8px 2px;
|
|
276
|
+
color: var(--cb-muted);
|
|
277
|
+
font-size: 0.85em;
|
|
278
|
+
font-weight: 600;
|
|
279
|
+
letter-spacing: 0.04em;
|
|
280
|
+
text-transform: uppercase;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.cb-item {
|
|
284
|
+
display: flex;
|
|
285
|
+
align-items: center;
|
|
286
|
+
gap: 8px;
|
|
287
|
+
width: 100%;
|
|
288
|
+
padding: 6px 8px;
|
|
289
|
+
border: 0;
|
|
290
|
+
border-radius: 6px;
|
|
291
|
+
background: transparent;
|
|
292
|
+
color: inherit;
|
|
293
|
+
font: inherit;
|
|
294
|
+
text-align: start;
|
|
295
|
+
cursor: pointer;
|
|
296
|
+
}
|
|
297
|
+
.cb-item[data-active] {
|
|
298
|
+
background: var(--cb-hover);
|
|
299
|
+
}
|
|
300
|
+
.cb-item[data-selected] {
|
|
301
|
+
color: var(--cb-accent-strong);
|
|
302
|
+
font-weight: 500;
|
|
303
|
+
}
|
|
304
|
+
.cb-item[data-disabled] {
|
|
305
|
+
opacity: 0.45;
|
|
306
|
+
cursor: not-allowed;
|
|
307
|
+
}
|
|
308
|
+
.cb-item-check {
|
|
309
|
+
flex: none;
|
|
310
|
+
width: 16px;
|
|
311
|
+
color: var(--cb-accent);
|
|
312
|
+
opacity: 0;
|
|
313
|
+
}
|
|
314
|
+
.cb-item[data-selected] .cb-item-check {
|
|
315
|
+
opacity: 1;
|
|
316
|
+
}
|
|
317
|
+
.cb-item-icon {
|
|
318
|
+
flex: none;
|
|
319
|
+
display: inline-flex;
|
|
320
|
+
color: var(--cb-muted);
|
|
321
|
+
}
|
|
322
|
+
.cb-item-body {
|
|
323
|
+
flex: 1 1 auto;
|
|
324
|
+
min-width: 0;
|
|
325
|
+
}
|
|
326
|
+
.cb-item-label {
|
|
327
|
+
overflow: hidden;
|
|
328
|
+
text-overflow: ellipsis;
|
|
329
|
+
white-space: nowrap;
|
|
330
|
+
}
|
|
331
|
+
.cb-item-description {
|
|
332
|
+
overflow: hidden;
|
|
333
|
+
color: var(--cb-muted);
|
|
334
|
+
font-size: 0.9em;
|
|
335
|
+
text-overflow: ellipsis;
|
|
336
|
+
white-space: nowrap;
|
|
337
|
+
}
|
|
338
|
+
.cb-mark {
|
|
339
|
+
background: color-mix(in oklab, var(--cb-accent), transparent 82%);
|
|
340
|
+
border-radius: 2px;
|
|
341
|
+
color: inherit;
|
|
342
|
+
font-weight: 600;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.cb-empty,
|
|
346
|
+
.cb-status {
|
|
347
|
+
display: flex;
|
|
348
|
+
align-items: center;
|
|
349
|
+
justify-content: center;
|
|
350
|
+
gap: 8px;
|
|
351
|
+
padding: 16px 8px;
|
|
352
|
+
color: var(--cb-muted);
|
|
353
|
+
}
|
|
354
|
+
.cb-footer {
|
|
355
|
+
display: flex;
|
|
356
|
+
align-items: center;
|
|
357
|
+
justify-content: space-between;
|
|
358
|
+
gap: 8px;
|
|
359
|
+
padding: 4px 6px;
|
|
360
|
+
border-top: 1px solid var(--cb-border);
|
|
361
|
+
}
|
|
362
|
+
.cb-link-button {
|
|
363
|
+
padding: 4px 6px;
|
|
364
|
+
border: 0;
|
|
365
|
+
border-radius: 4px;
|
|
366
|
+
background: none;
|
|
367
|
+
color: var(--cb-accent);
|
|
368
|
+
font: inherit;
|
|
369
|
+
cursor: pointer;
|
|
370
|
+
}
|
|
371
|
+
.cb-link-button:hover {
|
|
372
|
+
background: var(--cb-hover);
|
|
373
|
+
}
|
|
374
|
+
.cb-link-button:disabled {
|
|
375
|
+
opacity: 0.5;
|
|
376
|
+
cursor: default;
|
|
377
|
+
}
|
|
378
|
+
.cb-footer-count {
|
|
379
|
+
color: var(--cb-muted);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.cb-spinner {
|
|
383
|
+
animation: cb-spin 0.7s linear infinite;
|
|
384
|
+
}
|
|
385
|
+
@keyframes cb-spin {
|
|
386
|
+
to {
|
|
387
|
+
transform: rotate(360deg);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
@media (prefers-reduced-motion: reduce) {
|
|
391
|
+
.cb-spinner {
|
|
392
|
+
animation-duration: 2s;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.cb-sr-only {
|
|
397
|
+
position: absolute;
|
|
398
|
+
width: 1px;
|
|
399
|
+
height: 1px;
|
|
400
|
+
margin: -1px;
|
|
401
|
+
padding: 0;
|
|
402
|
+
overflow: hidden;
|
|
403
|
+
clip-path: inset(50%);
|
|
404
|
+
white-space: nowrap;
|
|
405
|
+
border: 0;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
@media (forced-colors: active) {
|
|
409
|
+
.cb-item[data-active] {
|
|
410
|
+
outline: 1px solid Highlight;
|
|
411
|
+
}
|
|
412
|
+
.cb-mark {
|
|
413
|
+
background: Highlight;
|
|
414
|
+
color: HighlightText;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# DataGrid
|
|
2
|
+
|
|
3
|
+
A virtualized data grid for React 19 with no runtime dependencies besides React.
|
|
4
|
+
Works in Vite/SPA apps and in the Next.js App Router.
|
|
5
|
+
|
|
6
|
+
- **Fast by design:** rows and columns are virtualized; state lives in an
|
|
7
|
+
external store with per-row subscriptions, so selecting a row or moving focus
|
|
8
|
+
re-renders only the rows involved; column resizing rewrites CSS variables
|
|
9
|
+
without rendering; filtering and sorting run at low priority via
|
|
10
|
+
`useDeferredValue`.
|
|
11
|
+
- **Features:** typed columns, multi-column sort, typed column filters, global
|
|
12
|
+
search, pagination, row selection (single, multiple, shift-range), column
|
|
13
|
+
resize / reorder (drag or menu) / pin / hide, inline editing with validation and
|
|
14
|
+
async saves, CSV / Excel / PDF export, clipboard copy, density, i18n, RTL, dark
|
|
15
|
+
mode, WAI-ARIA grid keyboard navigation.
|
|
16
|
+
- **Client or server mode:** let the grid sort/filter/paginate, or receive a
|
|
17
|
+
query and fetch pages yourself.
|
|
18
|
+
|
|
19
|
+
## Setup
|
|
20
|
+
|
|
21
|
+
Import the stylesheet once (e.g. in your root CSS or layout):
|
|
22
|
+
|
|
23
|
+
```css
|
|
24
|
+
@import "../component-lib/data-grid/styles.css";
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
or from JS: `import "component-lib/data-grid/styles.css";`
|
|
28
|
+
|
|
29
|
+
## Basic usage
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { createColumnHelper, DataGrid } from "component-lib/data-grid";
|
|
33
|
+
|
|
34
|
+
interface Employee { id: number; name: string; salary: number; startDate: string; active: boolean }
|
|
35
|
+
|
|
36
|
+
const col = createColumnHelper<Employee>();
|
|
37
|
+
|
|
38
|
+
// Define columns outside the component (or memoize them).
|
|
39
|
+
const columns = [
|
|
40
|
+
col.field("id", { header: "ID", type: "number", width: 80, pin: "left" }),
|
|
41
|
+
col.field("name", { width: 200 }),
|
|
42
|
+
col.field("salary", { type: "number", format: (v) => `$${v.toLocaleString()}` }),
|
|
43
|
+
col.field("startDate", { type: "date" }),
|
|
44
|
+
col.field("active", { type: "boolean" }),
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
export function Employees({ data }: { data: Employee[] }) {
|
|
48
|
+
return <DataGrid data={data} columns={columns} getRowId="id" enableRowSelection height={600} />;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`createColumnHelper` infers the value type for `cell`, `format`, `validate` and
|
|
53
|
+
`sortFn`. Plain `ColumnDef<T>[]` objects work too.
|
|
54
|
+
|
|
55
|
+
### Stable inputs
|
|
56
|
+
|
|
57
|
+
The grid memoizes on the identity of `data`, `columns` and `getRowId`. Keep
|
|
58
|
+
them stable: define columns at module level or with `useMemo`, and pass
|
|
59
|
+
`getRowId` as a property name (`getRowId="id"`). With React Compiler enabled,
|
|
60
|
+
inline values are memoized for you.
|
|
61
|
+
|
|
62
|
+
## Columns
|
|
63
|
+
|
|
64
|
+
| Option | Purpose |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| `field` / `accessor` / `id` | Where the value comes from. Display-only columns need just `id` and `cell`. |
|
|
67
|
+
| `header`, `width`, `minWidth`, `maxWidth`, `align` | Presentation. |
|
|
68
|
+
| `type` | `string` (default), `number`, `date`, `boolean`. Drives filter operators, sorting, alignment, editors and Excel cell types. |
|
|
69
|
+
| `options` | `{ label, value }[]` for enum columns: "is any of" filter, select editor, label display. |
|
|
70
|
+
| `format(value, row)` | Display text. Also used by search, CSV, PDF and copy. |
|
|
71
|
+
| `cell(ctx)` | Custom renderer. Clicks on buttons/inputs inside cells don't trigger `onRowClick`. |
|
|
72
|
+
| `pin`, `hidden` | Initial pin side / visibility. |
|
|
73
|
+
| `sortable`, `filterable`, `resizable`, `reorderable`, `pinnable`, `hideable`, `searchable` | Per-column feature switches (default `true`). |
|
|
74
|
+
| `sortFn`, `filterFn` | Custom comparison / matching. |
|
|
75
|
+
| `editable`, `editor`, `validate` | Inline editing (below). |
|
|
76
|
+
| `exportable`, `exportValue(row)` | Export control. |
|
|
77
|
+
| `headerClassName`, `cellClassName` | Styling hooks. |
|
|
78
|
+
|
|
79
|
+
## Server mode
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
const [query, setQuery] = useState<GridQuery>(initialQuery);
|
|
83
|
+
const { data, isFetching } = useQuery({ queryKey: ["orders", query], queryFn: () => fetchOrders(query) });
|
|
84
|
+
|
|
85
|
+
<DataGrid
|
|
86
|
+
mode="server"
|
|
87
|
+
data={data?.rows ?? []}
|
|
88
|
+
rowCount={data?.total ?? 0}
|
|
89
|
+
loading={isFetching}
|
|
90
|
+
columns={columns}
|
|
91
|
+
initialState={initialQuery}
|
|
92
|
+
onQueryChange={setQuery}
|
|
93
|
+
/>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`onQueryChange` receives `{ sorting, filters, globalFilter, pagination }`
|
|
97
|
+
whenever one of them changes. Changing filters, search or sorting resets to the
|
|
98
|
+
first page. The previous rows stay visible while `loading`.
|
|
99
|
+
|
|
100
|
+
The framework-free core (`component-lib/data-grid/core`) exports `filterRows`,
|
|
101
|
+
`sortRows` and `paginate`, so a Node or Next.js route handler can apply exactly
|
|
102
|
+
the same semantics on the server.
|
|
103
|
+
|
|
104
|
+
## State
|
|
105
|
+
|
|
106
|
+
All grid state can be controlled or left internal:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
interface GridState {
|
|
110
|
+
sorting; filters; globalFilter; pagination; rowSelection;
|
|
111
|
+
columnOrder; columnVisibility; columnSizing; columnPinning; density;
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- `initialState` seeds internal state.
|
|
116
|
+
- `state` controls any subset of keys; the grid then calls `onStateChange(next, prev)`
|
|
117
|
+
instead of updating those keys itself.
|
|
118
|
+
- Example: persist column layout with
|
|
119
|
+
`onStateChange={(s) => save(pick(s, ["columnOrder", "columnSizing", "columnPinning", "columnVisibility"]))}`.
|
|
120
|
+
|
|
121
|
+
## Editing
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
col.field("salary", {
|
|
125
|
+
type: "number",
|
|
126
|
+
editable: (row) => !row.locked,
|
|
127
|
+
validate: (v) => (v < 0 ? "Must be positive" : null),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
<DataGrid onCellEdit={async ({ rowId, columnId, value }) => { await save(rowId, columnId, value); }} />
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Start editing with double-click, Enter or F2. Enter commits and moves down,
|
|
134
|
+
Tab commits and moves right, and Escape cancels. The grid never mutates `data`:
|
|
135
|
+
update it in `onCellEdit`. If `onCellEdit` returns a promise, the cell shows the
|
|
136
|
+
pending value until it settles, and a rejection marks the cell invalid.
|
|
137
|
+
Custom editors: `editor: (props) => <MyInput value={props.value} onChange={props.onChange} onBlur={() => props.commit()} />`.
|
|
138
|
+
|
|
139
|
+
## Imperative API
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
const api = useRef<GridApi<Employee>>(null);
|
|
143
|
+
<DataGrid ref={api} ... />
|
|
144
|
+
|
|
145
|
+
api.current?.setFilter("status", { operator: "in", value: ["active"] });
|
|
146
|
+
api.current?.exportExcel({ scope: "selected", fileName: "people" });
|
|
147
|
+
api.current?.focusCell(0, "name");
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Methods include `toggleSort`, `setSorting`, `setFilter`, `clearFilters`,
|
|
151
|
+
`setGlobalFilter`, `setPageIndex`, `setPageSize`, `toggleRowSelected`,
|
|
152
|
+
`toggleAllRowsSelected`, `getSelectedRows`, `setColumnVisibility`,
|
|
153
|
+
`setColumnWidth`, `pinColumn`, `moveColumn`, `resetColumns`, `scrollToRow`,
|
|
154
|
+
`focusCell`, `startEditing`, `getRows`, `exportCsv`, `exportExcel`, `exportPdf`
|
|
155
|
+
and `copyToClipboard`.
|
|
156
|
+
|
|
157
|
+
## Export
|
|
158
|
+
|
|
159
|
+
Export code is split into chunks loaded on first use.
|
|
160
|
+
|
|
161
|
+
| Format | Notes |
|
|
162
|
+
| --- | --- |
|
|
163
|
+
| CSV | UTF-8 with BOM (opens correctly in Excel). Cells starting with `= + - @` are prefixed with `'` to prevent formula injection. |
|
|
164
|
+
| Excel | Real `.xlsx`: typed numbers, booleans and dates, bold frozen header, auto-filter, column widths. Written without a library. |
|
|
165
|
+
| PDF | Opens the browser print dialog with a print-formatted table; users choose "Save as PDF". Best for up to a few thousand rows. |
|
|
166
|
+
|
|
167
|
+
Scopes: `filtered` (default), `all`, `selected`, `page`, or pass `rows`, e.g. a
|
|
168
|
+
full result set fetched from the server.
|
|
169
|
+
|
|
170
|
+
## Keyboard
|
|
171
|
+
|
|
172
|
+
| Keys | Action |
|
|
173
|
+
| --- | --- |
|
|
174
|
+
| Arrows, Home/End, Ctrl+Home/End, PageUp/PageDown | Move between cells (header row included). |
|
|
175
|
+
| Enter (header) | Sort; Shift+Enter adds to the sort. |
|
|
176
|
+
| Alt+↓ or the Menu key (header) | Open the column menu. |
|
|
177
|
+
| Enter / F2 | Edit the cell, or focus the widget inside a custom cell. |
|
|
178
|
+
| Space | Toggle row selection (Shift for a range). |
|
|
179
|
+
| Ctrl/Cmd+A | Select all rows. |
|
|
180
|
+
| Ctrl/Cmd+C | Copy selected rows (TSV) or the active cell. |
|
|
181
|
+
| Escape | Cancel editing / leave a widget inside a cell. |
|
|
182
|
+
|
|
183
|
+
## Theming
|
|
184
|
+
|
|
185
|
+
Styles live in `@layer components`, so Tailwind utilities passed via
|
|
186
|
+
`classNames` / `className` win. Override the `--dg-*` variables for a theme:
|
|
187
|
+
|
|
188
|
+
```css
|
|
189
|
+
.dg-root { --dg-accent: #7c3aed; --dg-radius: 12px; --dg-font-size: 14px; }
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Colors follow the page's `color-scheme`. A `.dark` or `[data-theme="dark"]`
|
|
193
|
+
ancestor forces a scheme. State is exposed as attributes for styling:
|
|
194
|
+
`data-selected`, `data-active`, `data-editing`, `data-pinned`, `data-density`,
|
|
195
|
+
and `aria-sort` on header cells.
|
|
196
|
+
|
|
197
|
+
Slots for `classNames`: `root`, `toolbar`, `viewport`, `header`, `headerCell`,
|
|
198
|
+
`row`, `cell`, `pagination`.
|
|
199
|
+
|
|
200
|
+
## Next.js
|
|
201
|
+
|
|
202
|
+
Components carry `"use client"`; import `DataGrid` from a Server Component and
|
|
203
|
+
pass serializable props (`data`, `initialState`). Column definitions contain
|
|
204
|
+
functions, so define them in a client module. For server mode, fetch in a
|
|
205
|
+
Server Component or Route Handler using `core` functions, and sync the query to
|
|
206
|
+
the URL (e.g. with `nuqs`) through `state` + `onQueryChange`.
|
|
207
|
+
|
|
208
|
+
## Known limits
|
|
209
|
+
|
|
210
|
+
- Rows have a fixed height (per density or `rowHeight`); variable-height rows are not supported.
|
|
211
|
+
- Updates from the grid's store render synchronously; heavy client-side filtering is
|
|
212
|
+
deferred with `useDeferredValue`, but a single filter pass over very large data
|
|
213
|
+
(1M+ rows) still runs on the main thread. For that scale, use server mode or a worker.
|
|
214
|
+
- Row grouping, tree data and pivoting are not implemented.
|
|
215
|
+
|
|
216
|
+
## Migrating from `component-lib/datagrid`
|
|
217
|
+
|
|
218
|
+
| Old prop | New |
|
|
219
|
+
| --- | --- |
|
|
220
|
+
| `dataSource` (array) | `data` |
|
|
221
|
+
| `dataSource` (URL string) | Fetch in your app; pass `data` (the grid no longer fetches). |
|
|
222
|
+
| `lazy` + `pageSettings.totalCount` | `mode="server"` + `rowCount` |
|
|
223
|
+
| `pageSettings.pageNumber` / `pageSize` | `initialState={{ pagination: { pageIndex: 0, pageSize } }}` |
|
|
224
|
+
| `enableSearch`, `enableExcelExport`, `enablePdfExport` | `toolbar={{ search, export }}` |
|
|
225
|
+
| `selectAll`, `onSelectRow` | `enableRowSelection`, `onStateChange` / `api.getSelectedRows()` |
|
|
226
|
+
| `rowChange` / `onRowClick` | `onRowClick` |
|
|
227
|
+
| `pageStatus` / `onPageChange`, `onFilterChange`, `onSearchChange` | `onQueryChange` |
|
|
228
|
+
| `initialFilters`, `initialSearchParam` | `initialState={{ filters, globalFilter }}` |
|
|
229
|
+
| `isFetching` | `loading` |
|
|
230
|
+
| `column.headerText`, `column.template` | `header`, `cell` |
|
|
231
|
+
| `grid*Class` props | `classNames` slots |
|
|
232
|
+
| `ref.goToPage(n)` etc. | `ref.setPageIndex(n)` and the API above |
|