@ornery/ui-grid-react 0.1.5 → 0.1.6
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/dist/UiGrid.d.ts +11 -0
- package/dist/UiGrid.d.ts.map +1 -0
- package/dist/gridStateMath.d.ts +8 -0
- package/dist/gridStateMath.d.ts.map +1 -0
- package/dist/index.d.ts +13 -155
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +541 -1054
- package/dist/index.mjs +434 -919
- package/dist/mountUiGrid.d.ts +4 -0
- package/dist/mountUiGrid.d.ts.map +1 -0
- package/dist/rustWasmGridEngine.d.ts +8 -0
- package/dist/rustWasmGridEngine.d.ts.map +1 -0
- package/dist/{index.d.mts → useGridState.d.ts} +5 -50
- package/dist/useGridState.d.ts.map +1 -0
- package/dist/useVirtualScroll.d.ts +20 -0
- package/dist/useVirtualScroll.d.ts.map +1 -0
- package/dist/virtualScrollMath.d.ts +17 -0
- package/dist/virtualScrollMath.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/UiGrid.test.tsx +2 -1
- package/src/UiGrid.tsx +174 -45
- package/src/index.ts +1 -0
- package/src/mountUiGrid.tsx +10 -0
- package/src/rustWasmGridEngine.ts +3 -1
- package/src/ui-grid.css +161 -1
- package/src/useGridState.ts +39 -27
- package/src/useVirtualScroll.ts +2 -0
- package/tsconfig.build.json +6 -0
- package/tsconfig.dts.json +15 -0
- package/CLAUDE.md +0 -283
package/src/ui-grid.css
CHANGED
|
@@ -20,6 +20,17 @@
|
|
|
20
20
|
--ui-grid-status-enterprise-color: var(--app-ui-grid-status-enterprise-color, #115e59);
|
|
21
21
|
--ui-grid-status-pilot-bg: var(--app-ui-grid-status-pilot-bg, rgba(234, 88, 12, 0.14));
|
|
22
22
|
--ui-grid-status-pilot-color: var(--app-ui-grid-status-pilot-color, #c2410c);
|
|
23
|
+
--ui-grid-pin-menu-open-z-index: var(--app-ui-grid-pin-menu-open-z-index, 8);
|
|
24
|
+
--ui-grid-pin-menu-z-index: var(--app-ui-grid-pin-menu-z-index, 20);
|
|
25
|
+
--ui-grid-pin-menu-gap: var(--app-ui-grid-pin-menu-gap, 0.25rem);
|
|
26
|
+
--ui-grid-pin-menu-padding: var(--app-ui-grid-pin-menu-padding, 0.25rem);
|
|
27
|
+
--ui-grid-pin-menu-radius: var(--app-ui-grid-pin-menu-radius, 999px);
|
|
28
|
+
--ui-grid-pin-menu-shadow: var(--app-ui-grid-pin-menu-shadow, 0 10px 24px color-mix(in srgb, var(--ui-grid-cell-color) 10%, transparent));
|
|
29
|
+
--ui-grid-pin-menu-action-size: var(--app-ui-grid-pin-menu-action-size, 1.75rem);
|
|
30
|
+
--ui-grid-pin-control-collapsed-size: var(--app-ui-grid-pin-control-collapsed-size, 1px);
|
|
31
|
+
--ui-grid-pin-control-transition-duration: var(--app-ui-grid-pin-control-transition-duration, 160ms);
|
|
32
|
+
--ui-grid-pin-control-transition-easing: var(--app-ui-grid-pin-control-transition-easing, cubic-bezier(0.22, 1, 0.36, 1));
|
|
33
|
+
--ui-grid-pin-menu-scale-closed: var(--app-ui-grid-pin-menu-scale-closed, 0.72);
|
|
23
34
|
display: block;
|
|
24
35
|
color: var(--ui-grid-cell-color);
|
|
25
36
|
}
|
|
@@ -39,12 +50,18 @@
|
|
|
39
50
|
.grid-hero {
|
|
40
51
|
display: flex;
|
|
41
52
|
justify-content: space-between;
|
|
53
|
+
flex-wrap: wrap;
|
|
42
54
|
gap: 1.5rem;
|
|
43
|
-
align-items:
|
|
55
|
+
align-items: flex-start;
|
|
44
56
|
padding: 1rem 0;
|
|
45
57
|
color: var(--ui-grid-cell-color);
|
|
46
58
|
}
|
|
47
59
|
|
|
60
|
+
.grid-hero > :first-child {
|
|
61
|
+
flex: 1 1 28rem;
|
|
62
|
+
min-width: min(100%, 20rem);
|
|
63
|
+
}
|
|
64
|
+
|
|
48
65
|
.ui-grid-host .eyebrow {
|
|
49
66
|
margin: 0 0 0.5rem;
|
|
50
67
|
text-transform: uppercase;
|
|
@@ -70,6 +87,9 @@
|
|
|
70
87
|
gap: 1rem;
|
|
71
88
|
align-items: center;
|
|
72
89
|
flex-wrap: wrap;
|
|
90
|
+
flex: 0 1 auto;
|
|
91
|
+
margin-left: auto;
|
|
92
|
+
justify-content: flex-end;
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
.action {
|
|
@@ -168,6 +188,22 @@
|
|
|
168
188
|
.filter-grid,
|
|
169
189
|
.body-grid {
|
|
170
190
|
display: grid;
|
|
191
|
+
width: max-content;
|
|
192
|
+
min-width: 100%;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.header-grid,
|
|
196
|
+
.filter-grid {
|
|
197
|
+
position: sticky;
|
|
198
|
+
z-index: 3;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.header-grid {
|
|
202
|
+
top: 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.filter-grid {
|
|
206
|
+
top: var(--ui-grid-header-sticky-top, 0px);
|
|
171
207
|
}
|
|
172
208
|
|
|
173
209
|
.header-cell,
|
|
@@ -192,8 +228,18 @@
|
|
|
192
228
|
background: color-mix(in srgb, var(--ui-grid-accent) 8%, var(--ui-grid-header-background));
|
|
193
229
|
}
|
|
194
230
|
|
|
231
|
+
.header-cell.is-pin-menu-open {
|
|
232
|
+
z-index: var(--ui-grid-pin-menu-open-z-index);
|
|
233
|
+
}
|
|
234
|
+
|
|
195
235
|
.header-label {
|
|
196
236
|
min-width: 0;
|
|
237
|
+
display: block;
|
|
238
|
+
line-height: 1.25;
|
|
239
|
+
white-space: nowrap;
|
|
240
|
+
overflow: hidden;
|
|
241
|
+
text-overflow: clip;
|
|
242
|
+
hyphens: none;
|
|
197
243
|
}
|
|
198
244
|
|
|
199
245
|
.header-actions {
|
|
@@ -201,6 +247,8 @@
|
|
|
201
247
|
align-items: center;
|
|
202
248
|
gap: 0.4rem;
|
|
203
249
|
justify-self: end;
|
|
250
|
+
flex-wrap: nowrap;
|
|
251
|
+
flex-shrink: 0;
|
|
204
252
|
}
|
|
205
253
|
|
|
206
254
|
.header-action,
|
|
@@ -250,14 +298,115 @@
|
|
|
250
298
|
color: var(--ui-grid-accent);
|
|
251
299
|
}
|
|
252
300
|
|
|
301
|
+
.pin-control {
|
|
302
|
+
position: relative;
|
|
303
|
+
display: inline-grid;
|
|
304
|
+
place-items: center;
|
|
305
|
+
justify-items: center;
|
|
306
|
+
min-width: 2rem;
|
|
307
|
+
overflow: hidden;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.pin-trigger {
|
|
311
|
+
grid-area: 1 / 1;
|
|
312
|
+
transform-origin: center;
|
|
313
|
+
transition:
|
|
314
|
+
width var(--ui-grid-pin-control-transition-duration) var(--ui-grid-pin-control-transition-easing),
|
|
315
|
+
height var(--ui-grid-pin-control-transition-duration) var(--ui-grid-pin-control-transition-easing),
|
|
316
|
+
opacity var(--ui-grid-pin-control-transition-duration) var(--ui-grid-pin-control-transition-easing),
|
|
317
|
+
transform var(--ui-grid-pin-control-transition-duration) var(--ui-grid-pin-control-transition-easing),
|
|
318
|
+
border-color var(--ui-grid-pin-control-transition-duration) var(--ui-grid-pin-control-transition-easing),
|
|
319
|
+
background-color var(--ui-grid-pin-control-transition-duration) var(--ui-grid-pin-control-transition-easing),
|
|
320
|
+
color var(--ui-grid-pin-control-transition-duration) var(--ui-grid-pin-control-transition-easing);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.pin-menu {
|
|
324
|
+
grid-area: 1 / 1;
|
|
325
|
+
z-index: var(--ui-grid-pin-menu-z-index);
|
|
326
|
+
display: grid;
|
|
327
|
+
grid-auto-rows: var(--ui-grid-pin-menu-action-size);
|
|
328
|
+
gap: var(--ui-grid-pin-menu-gap);
|
|
329
|
+
padding: var(--ui-grid-pin-menu-padding);
|
|
330
|
+
border: 1px solid var(--ui-grid-border-color);
|
|
331
|
+
border-radius: var(--ui-grid-pin-menu-radius);
|
|
332
|
+
background: var(--ui-grid-surface);
|
|
333
|
+
box-shadow: var(--ui-grid-pin-menu-shadow);
|
|
334
|
+
justify-items: center;
|
|
335
|
+
align-items: center;
|
|
336
|
+
width: 2rem;
|
|
337
|
+
max-height: var(--ui-grid-pin-control-collapsed-size);
|
|
338
|
+
opacity: 0;
|
|
339
|
+
pointer-events: none;
|
|
340
|
+
transform: scale(var(--ui-grid-pin-menu-scale-closed));
|
|
341
|
+
transform-origin: center;
|
|
342
|
+
transition:
|
|
343
|
+
max-height var(--ui-grid-pin-control-transition-duration) var(--ui-grid-pin-control-transition-easing),
|
|
344
|
+
opacity var(--ui-grid-pin-control-transition-duration) var(--ui-grid-pin-control-transition-easing),
|
|
345
|
+
transform var(--ui-grid-pin-control-transition-duration) var(--ui-grid-pin-control-transition-easing),
|
|
346
|
+
padding var(--ui-grid-pin-control-transition-duration) var(--ui-grid-pin-control-transition-easing),
|
|
347
|
+
border-color var(--ui-grid-pin-control-transition-duration) var(--ui-grid-pin-control-transition-easing),
|
|
348
|
+
box-shadow var(--ui-grid-pin-control-transition-duration) var(--ui-grid-pin-control-transition-easing);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.pin-control-open {
|
|
352
|
+
align-items: start;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.pin-control-open .pin-trigger {
|
|
356
|
+
width: var(--ui-grid-pin-control-collapsed-size);
|
|
357
|
+
height: var(--ui-grid-pin-control-collapsed-size);
|
|
358
|
+
opacity: 0;
|
|
359
|
+
transform: scale(0.72);
|
|
360
|
+
pointer-events: none;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.pin-control-open .pin-menu {
|
|
364
|
+
max-height: calc((var(--ui-grid-pin-menu-action-size) * 2) + var(--ui-grid-pin-menu-gap) + (var(--ui-grid-pin-menu-padding) * 2));
|
|
365
|
+
opacity: 1;
|
|
366
|
+
pointer-events: auto;
|
|
367
|
+
transform: scale(1);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.pin-menu-action {
|
|
371
|
+
display: inline-flex;
|
|
372
|
+
align-items: center;
|
|
373
|
+
justify-content: center;
|
|
374
|
+
width: var(--ui-grid-pin-menu-action-size);
|
|
375
|
+
height: var(--ui-grid-pin-menu-action-size);
|
|
376
|
+
border: 0;
|
|
377
|
+
border-radius: 999px;
|
|
378
|
+
background: transparent;
|
|
379
|
+
color: var(--ui-grid-cell-color);
|
|
380
|
+
cursor: pointer;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.pin-menu-action:hover,
|
|
384
|
+
.pin-menu-action:focus-visible {
|
|
385
|
+
outline: 0;
|
|
386
|
+
background: color-mix(in srgb, var(--ui-grid-accent) 12%, var(--ui-grid-surface));
|
|
387
|
+
color: var(--ui-grid-accent);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.pin-menu-action svg {
|
|
391
|
+
width: 1rem;
|
|
392
|
+
height: 1rem;
|
|
393
|
+
fill: currentColor;
|
|
394
|
+
}
|
|
395
|
+
|
|
253
396
|
.filter-cell {
|
|
254
397
|
padding: 0.75rem 1rem 1rem;
|
|
255
398
|
border-bottom: 1px solid var(--ui-grid-border-color);
|
|
256
399
|
background: var(--ui-grid-header-background);
|
|
400
|
+
overflow: hidden;
|
|
257
401
|
}
|
|
258
402
|
|
|
259
403
|
.filter-cell input {
|
|
404
|
+
display: block;
|
|
260
405
|
width: 100%;
|
|
406
|
+
min-width: 0;
|
|
407
|
+
max-width: 100%;
|
|
408
|
+
min-inline-size: 0;
|
|
409
|
+
max-inline-size: 100%;
|
|
261
410
|
border: 1px solid var(--ui-grid-border-color);
|
|
262
411
|
border-radius: var(--ui-grid-radius);
|
|
263
412
|
background: var(--ui-grid-surface);
|
|
@@ -390,6 +539,17 @@
|
|
|
390
539
|
width: 100%;
|
|
391
540
|
}
|
|
392
541
|
|
|
542
|
+
.grid-virtual-spacer {
|
|
543
|
+
position: relative;
|
|
544
|
+
width: max-content;
|
|
545
|
+
min-width: 100%;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.grid-virtual-body {
|
|
549
|
+
position: absolute;
|
|
550
|
+
left: 0;
|
|
551
|
+
}
|
|
552
|
+
|
|
393
553
|
.body-cell.align-center {
|
|
394
554
|
text-align: center;
|
|
395
555
|
}
|
package/src/useGridState.ts
CHANGED
|
@@ -88,19 +88,10 @@ import {
|
|
|
88
88
|
FEATURE_AUTO_RESIZE,
|
|
89
89
|
FEATURE_SAVE_STATE,
|
|
90
90
|
FEATURE_PINNING,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
ExpandableItem,
|
|
96
|
-
RowItem,
|
|
97
|
-
PipelineResult,
|
|
98
|
-
GridInfiniteScrollState,
|
|
99
|
-
GridMoveDirection,
|
|
100
|
-
GridCellTemplateContext,
|
|
101
|
-
GridExpandableTemplateContext,
|
|
102
|
-
} from '@ornery/ui-grid';
|
|
103
|
-
import {
|
|
91
|
+
buildInitialPinnedState,
|
|
92
|
+
computePinnedOffset,
|
|
93
|
+
isColumnPinnable,
|
|
94
|
+
isPinningEnabled,
|
|
104
95
|
applyGridSortStateCommand,
|
|
105
96
|
updateGridFilterCommand,
|
|
106
97
|
clearGridFiltersCommand,
|
|
@@ -127,16 +118,6 @@ import {
|
|
|
127
118
|
setGridInfiniteScrollDirectionsCommand,
|
|
128
119
|
restoreGridStateCommand,
|
|
129
120
|
pinGridColumnCommand,
|
|
130
|
-
} from '../../ui-grid/src/lib/grid/ui-grid.commands';
|
|
131
|
-
import {
|
|
132
|
-
buildInitialPinnedState,
|
|
133
|
-
computePinnedOffset,
|
|
134
|
-
isColumnPinnable,
|
|
135
|
-
isPinningEnabled,
|
|
136
|
-
PinDirection,
|
|
137
|
-
PinnedColumnState,
|
|
138
|
-
} from '../../ui-grid/src/lib/grid/grid.core';
|
|
139
|
-
import {
|
|
140
121
|
raiseGridRenderingComplete,
|
|
141
122
|
raiseGridRowsRendered,
|
|
142
123
|
raiseGridRowsVisibleChanged,
|
|
@@ -145,8 +126,22 @@ import {
|
|
|
145
126
|
raiseGridScrollBegin,
|
|
146
127
|
raiseGridScrollEnd,
|
|
147
128
|
raiseGridBenchmarkComplete,
|
|
148
|
-
|
|
149
|
-
|
|
129
|
+
downloadGridCsvFile,
|
|
130
|
+
observeGridHostSize,
|
|
131
|
+
} from '@ornery/ui-grid';
|
|
132
|
+
import type {
|
|
133
|
+
DisplayItem,
|
|
134
|
+
GroupItem,
|
|
135
|
+
ExpandableItem,
|
|
136
|
+
RowItem,
|
|
137
|
+
PipelineResult,
|
|
138
|
+
GridInfiniteScrollState,
|
|
139
|
+
GridMoveDirection,
|
|
140
|
+
GridCellTemplateContext,
|
|
141
|
+
GridExpandableTemplateContext,
|
|
142
|
+
PinDirection,
|
|
143
|
+
PinnedColumnState,
|
|
144
|
+
} from '@ornery/ui-grid';
|
|
150
145
|
|
|
151
146
|
function escapeCssSelectorValue(value: string): string {
|
|
152
147
|
const nativeEscape = globalThis.CSS?.escape;
|
|
@@ -398,8 +393,25 @@ export function useGridState(
|
|
|
398
393
|
const rowSize = options.rowHeight ?? 44;
|
|
399
394
|
|
|
400
395
|
const visibleColumns = useMemo(() => {
|
|
401
|
-
|
|
402
|
-
|
|
396
|
+
const orderedColumns = orderVisibleColumns(options.columnDefs, columnOrder);
|
|
397
|
+
const pinnedEntries = Object.entries(pinnedColumns);
|
|
398
|
+
if (pinnedEntries.length === 0) {
|
|
399
|
+
return orderedColumns;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const columnByName = new Map(orderedColumns.map((column) => [column.name, column]));
|
|
403
|
+
const pinnedLeft = pinnedEntries
|
|
404
|
+
.filter(([, direction]) => direction === 'left')
|
|
405
|
+
.map(([columnName]) => columnByName.get(columnName))
|
|
406
|
+
.filter((column): column is GridColumnDef => column !== undefined);
|
|
407
|
+
const pinnedRight = pinnedEntries
|
|
408
|
+
.filter(([, direction]) => direction === 'right')
|
|
409
|
+
.map(([columnName]) => columnByName.get(columnName))
|
|
410
|
+
.filter((column): column is GridColumnDef => column !== undefined);
|
|
411
|
+
const centerColumns = orderedColumns.filter((column) => pinnedColumns[column.name] === undefined);
|
|
412
|
+
|
|
413
|
+
return [...pinnedLeft, ...centerColumns, ...pinnedRight];
|
|
414
|
+
}, [options.columnDefs, columnOrder, pinnedColumns]);
|
|
403
415
|
|
|
404
416
|
const visibleColumnsRef = useRef(visibleColumns);
|
|
405
417
|
visibleColumnsRef.current = visibleColumns;
|
package/src/useVirtualScroll.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface UseVirtualScrollResult {
|
|
|
13
13
|
totalHeight: number;
|
|
14
14
|
offsetY: number;
|
|
15
15
|
onScroll: (event: React.UIEvent<HTMLDivElement>) => void;
|
|
16
|
+
setScrollTop: (scrollTop: number) => void;
|
|
16
17
|
viewportRef: React.RefObject<HTMLDivElement | null>;
|
|
17
18
|
scrollTop: number;
|
|
18
19
|
}
|
|
@@ -39,6 +40,7 @@ export function useVirtualScroll(options: UseVirtualScrollOptions): UseVirtualSc
|
|
|
39
40
|
totalHeight: virtualWindow.totalHeight,
|
|
40
41
|
offsetY: virtualWindow.offsetY,
|
|
41
42
|
onScroll,
|
|
43
|
+
setScrollTop,
|
|
42
44
|
viewportRef,
|
|
43
45
|
scrollTop,
|
|
44
46
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"declaration": true,
|
|
5
|
+
"declarationMap": true,
|
|
6
|
+
"emitDeclarationOnly": true,
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"paths": {
|
|
10
|
+
"@ornery/ui-grid": ["../../dist/ui-grid"]
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
|
14
|
+
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"]
|
|
15
|
+
}
|
package/CLAUDE.md
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
# @ornery/ui-grid-react — React Wrapper
|
|
2
|
-
|
|
3
|
-
## What to build
|
|
4
|
-
|
|
5
|
-
A React wrapper for the `@ornery/ui-grid` Angular library's pure-TypeScript core. This package publishes as `@ornery/ui-grid-react` and reuses 100% of the core logic — no code duplication. The React wrapper is a thin rendering layer.
|
|
6
|
-
|
|
7
|
-
## Critical: Core reuse
|
|
8
|
-
|
|
9
|
-
The entire core is pure TypeScript with ZERO Angular dependencies. Import everything from the sibling library path. In this monorepo, the path alias `@ornery/ui-grid` maps to `projects/ui-grid/src/public-api.ts`.
|
|
10
|
-
|
|
11
|
-
**Core files to reuse (all pure TS, no Angular):**
|
|
12
|
-
|
|
13
|
-
- `grid.core.pipeline.ts` → `buildGridPipeline()` — the entire data pipeline
|
|
14
|
-
- `grid.api.ts` → `createGridApi()`, `UiGridApi` — the API object
|
|
15
|
-
- `grid.models.ts` → `GridOptions`, `GridColumnDef`, `GridRow`, `GridLabels`, `DEFAULT_GRID_LABELS`, etc.
|
|
16
|
-
- `grid.features.ts` → `FEATURE_SORTING`, `FEATURE_FILTERING`, etc. — build-time feature flags
|
|
17
|
-
- `grid.core.viewmodel.ts` → `resolveGridLabels()`, `gridSortButtonLabel()`, `gridSortAriaSort()`, all label functions
|
|
18
|
-
- `grid.core.display.ts` → `buildGridCellContext()`, `formatGridCellDisplayValue()`
|
|
19
|
-
- `grid.core.edit.ts` → `findNextGridCell()`, `isPrintableGridKey()`, `buildGridFocusCellResult()`, etc.
|
|
20
|
-
- `grid.core.export.ts` → `exportCsvRows()`, `headerLabel()`
|
|
21
|
-
- `grid.core.types.ts` → `DisplayItem`, `GroupItem`, `RowItem`, `ExpandableItem`, `PipelineResult`, `BuildGridPipelineContext`
|
|
22
|
-
- `grid.constants.ts` → `SORT_DIRECTIONS`, `FILTER_CONDITIONS`
|
|
23
|
-
- `grid.utils.ts` → `getCellValue`, `getPathValue`, `setPathValue`, `titleize`, etc.
|
|
24
|
-
- `row-searcher.ts` → `runColumnFilter`, `setupFilters`
|
|
25
|
-
- `row-sorter.ts` → `getSortFn`
|
|
26
|
-
- `ui-grid.commands.ts` → All command functions (pure TS, depends only on grid.api + grid.core)
|
|
27
|
-
- `ui-grid.events.ts` → All event raisers (pure TS, depends only on grid.api)
|
|
28
|
-
- `ui-grid.host.ts` → `downloadGridCsvFile()` (pure TS), `observeGridHostSize()` (pure TS)
|
|
29
|
-
- `i18n/en-US.json` → Default labels
|
|
30
|
-
|
|
31
|
-
**Do NOT copy any core files. Import them.**
|
|
32
|
-
|
|
33
|
-
## File structure to create
|
|
34
|
-
|
|
35
|
-
```
|
|
36
|
-
projects/ui-grid-react/
|
|
37
|
-
package.json
|
|
38
|
-
tsconfig.json
|
|
39
|
-
CLAUDE.md ← this file
|
|
40
|
-
src/
|
|
41
|
-
index.ts ← public API exports
|
|
42
|
-
UiGrid.tsx ← main React component
|
|
43
|
-
useGridState.ts ← state management hook (replaces Angular signals)
|
|
44
|
-
useVirtualScroll.ts ← fixed-size row virtualization hook
|
|
45
|
-
ui-grid.css ← styles adapted from grid.core.styles.scss
|
|
46
|
-
UiGrid.test.tsx ← tests with vitest + @testing-library/react
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## package.json
|
|
50
|
-
|
|
51
|
-
```json
|
|
52
|
-
{
|
|
53
|
-
"name": "@ornery/ui-grid-react",
|
|
54
|
-
"version": "0.1.0",
|
|
55
|
-
"description": "React wrapper for @ornery/ui-grid",
|
|
56
|
-
"main": "dist/index.js",
|
|
57
|
-
"module": "dist/index.mjs",
|
|
58
|
-
"types": "dist/index.d.ts",
|
|
59
|
-
"exports": {
|
|
60
|
-
".": {
|
|
61
|
-
"import": "./dist/index.mjs",
|
|
62
|
-
"require": "./dist/index.js",
|
|
63
|
-
"types": "./dist/index.d.ts"
|
|
64
|
-
},
|
|
65
|
-
"./styles": "./dist/ui-grid.css"
|
|
66
|
-
},
|
|
67
|
-
"peerDependencies": {
|
|
68
|
-
"react": "^18.0.0 || ^19.0.0",
|
|
69
|
-
"react-dom": "^18.0.0 || ^19.0.0",
|
|
70
|
-
"@ornery/ui-grid": "^0.1.0"
|
|
71
|
-
},
|
|
72
|
-
"devDependencies": {
|
|
73
|
-
"react": "^19.1.0",
|
|
74
|
-
"react-dom": "^19.1.0",
|
|
75
|
-
"@testing-library/react": "^16.0.0",
|
|
76
|
-
"@types/react": "^19.0.0",
|
|
77
|
-
"@types/react-dom": "^19.0.0",
|
|
78
|
-
"typescript": "~5.8.0",
|
|
79
|
-
"vitest": "^4.1.0",
|
|
80
|
-
"jsdom": "^26.0.0",
|
|
81
|
-
"tsup": "^8.0.0"
|
|
82
|
-
},
|
|
83
|
-
"scripts": {
|
|
84
|
-
"build": "tsup src/index.ts --format esm,cjs --dts --external react --external react-dom --external @ornery/ui-grid",
|
|
85
|
-
"test": "vitest run",
|
|
86
|
-
"test:watch": "vitest"
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
## tsconfig.json
|
|
92
|
-
|
|
93
|
-
Extend the root tsconfig. Add path alias for `@ornery/ui-grid`:
|
|
94
|
-
|
|
95
|
-
```json
|
|
96
|
-
{
|
|
97
|
-
"extends": "../../tsconfig.json",
|
|
98
|
-
"compilerOptions": {
|
|
99
|
-
"jsx": "react-jsx",
|
|
100
|
-
"outDir": "./dist",
|
|
101
|
-
"declaration": true,
|
|
102
|
-
"declarationMap": true,
|
|
103
|
-
"paths": {
|
|
104
|
-
"@ornery/ui-grid": ["../ui-grid/src/public-api.ts"]
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
"include": ["src"]
|
|
108
|
-
}
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
## useVirtualScroll hook
|
|
112
|
-
|
|
113
|
-
A lightweight fixed-size virtualizer (~50 lines). No external dependency.
|
|
114
|
-
|
|
115
|
-
**Interface:**
|
|
116
|
-
```ts
|
|
117
|
-
interface UseVirtualScrollOptions {
|
|
118
|
-
itemCount: number;
|
|
119
|
-
itemSize: number; // row height in px
|
|
120
|
-
viewportHeight: number; // container height in px
|
|
121
|
-
overscan?: number; // extra items above/below (default: 3)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
interface UseVirtualScrollResult {
|
|
125
|
-
visibleRange: { start: number; end: number };
|
|
126
|
-
totalHeight: number;
|
|
127
|
-
offsetY: number;
|
|
128
|
-
onScroll: (event: React.UIEvent<HTMLDivElement>) => void;
|
|
129
|
-
viewportRef: React.RefObject<HTMLDivElement>;
|
|
130
|
-
scrollTop: number;
|
|
131
|
-
}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
**Implementation:**
|
|
135
|
-
- Track `scrollTop` via `useState`
|
|
136
|
-
- Calculate `start = Math.floor(scrollTop / itemSize) - overscan`
|
|
137
|
-
- Calculate `end = start + Math.ceil(viewportHeight / itemSize) + 2 * overscan`
|
|
138
|
-
- Clamp to `[0, itemCount)`
|
|
139
|
-
- `totalHeight = itemCount * itemSize`
|
|
140
|
-
- `offsetY = start * itemSize`
|
|
141
|
-
- Return a div ref and onScroll handler
|
|
142
|
-
|
|
143
|
-
## useGridState hook
|
|
144
|
-
|
|
145
|
-
Replaces Angular signals with React state. This is the core bridge.
|
|
146
|
-
|
|
147
|
-
**Maps the Angular component's state 1:1:**
|
|
148
|
-
|
|
149
|
-
| Angular signal | React state |
|
|
150
|
-
|----------------|-------------|
|
|
151
|
-
| `activeFilters` | `useState<Record<string, string>>({})` |
|
|
152
|
-
| `groupByColumns` | `useState<string[]>([])` |
|
|
153
|
-
| `collapsedGroups` | `useState<Record<string, boolean>>({})` |
|
|
154
|
-
| `columnOrder` | `useState<string[]>([])` |
|
|
155
|
-
| `hiddenRowReasons` | `useState<Record<string, string[]>>({})` |
|
|
156
|
-
| `sortState` | `useState<SortState>(...)` |
|
|
157
|
-
| `focusedCell` | `useState<GridCellPosition \| null>(null)` |
|
|
158
|
-
| `editingCell` | `useState<GridCellPosition \| null>(null)` |
|
|
159
|
-
| `editingValue` | `useState('')` |
|
|
160
|
-
| `expandedRows` | `useState<Record<string, boolean>>({})` |
|
|
161
|
-
| `expandedTreeRows` | `useState<Record<string, boolean>>({})` |
|
|
162
|
-
| `currentPage` | `useState(1)` |
|
|
163
|
-
| `pageSize` | `useState(0)` |
|
|
164
|
-
| `benchmarkResult` | `useState<GridBenchmarkResult \| null>(null)` |
|
|
165
|
-
| `infiniteScrollState` | `useState<GridInfiniteScrollState>(...)` |
|
|
166
|
-
|
|
167
|
-
**Key behaviors:**
|
|
168
|
-
- Call `createGridApi()` once in a `useRef` with bindings that dispatch state updates
|
|
169
|
-
- Memoize `buildGridPipeline()` via `useMemo` dependent on all state inputs
|
|
170
|
-
- Memoize `visibleColumns` via `useMemo`
|
|
171
|
-
- Resolve `labels` via `useMemo(() => resolveGridLabels(options.labels), [options.labels])`
|
|
172
|
-
- Reset state when `options.id` changes (same as the Angular effect)
|
|
173
|
-
- Call `options.onRegisterApi?.(gridApi)` once
|
|
174
|
-
|
|
175
|
-
**Returns:**
|
|
176
|
-
- All computed values: `pipeline`, `visibleColumns`, `labels`, `gridTemplateColumns`, etc.
|
|
177
|
-
- All action dispatchers: `toggleSort()`, `updateFilter()`, `toggleGrouping()`, etc.
|
|
178
|
-
- `gridApi` ref
|
|
179
|
-
|
|
180
|
-
## UiGrid.tsx component
|
|
181
|
-
|
|
182
|
-
**Props:**
|
|
183
|
-
```tsx
|
|
184
|
-
interface UiGridProps {
|
|
185
|
-
options: GridOptions;
|
|
186
|
-
onRegisterApi?: (api: UiGridApi) => void;
|
|
187
|
-
/** Render prop for custom cell content. Return null to use default text. */
|
|
188
|
-
cellRenderer?: (context: GridCellTemplateContext) => React.ReactNode;
|
|
189
|
-
/** Render prop for expandable row content. */
|
|
190
|
-
expandableRenderer?: (context: GridExpandableTemplateContext) => React.ReactNode;
|
|
191
|
-
className?: string;
|
|
192
|
-
}
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
**Rendering structure:** Match the Angular template exactly — same CSS classes, same `data-*` attributes, same ARIA roles/attributes, same part attributes. Reference the Angular template at `projects/ui-grid/src/lib/grid/ui-grid.component.html`.
|
|
196
|
-
|
|
197
|
-
**Key sections:**
|
|
198
|
-
1. Hero header with title, benchmark button, export button, stats
|
|
199
|
-
2. Metrics strip
|
|
200
|
-
3. Grid frame (`role="grid"`) with:
|
|
201
|
-
- Header row (`role="row"`) with column headers (`role="columnheader"`, `aria-sort`)
|
|
202
|
-
- Sort buttons with SVG icons
|
|
203
|
-
- Group toggle buttons with SVG icons
|
|
204
|
-
- Filter row
|
|
205
|
-
- Display items via `ng-template` equivalent (just inline JSX):
|
|
206
|
-
- Group rows with disclosure chevron SVGs
|
|
207
|
-
- Expandable rows
|
|
208
|
-
- Data rows with cells (`role="gridcell"`, `tabindex="0"`)
|
|
209
|
-
- Tree toggle buttons with chevron SVGs
|
|
210
|
-
- Expand toggle buttons with chevron SVGs
|
|
211
|
-
- Cell editor input (when editing)
|
|
212
|
-
- Cell template / display value
|
|
213
|
-
- Virtual scroll viewport OR plain list
|
|
214
|
-
- Empty state
|
|
215
|
-
- Pagination footer with arrow SVG icons
|
|
216
|
-
|
|
217
|
-
**Feature flag guards:** Same pattern as Angular template — wrap sections in `{FEATURE_SORTING && ...}` etc.
|
|
218
|
-
|
|
219
|
-
**Focus management:** For cell focus/editor focus, use `useRef` + `useEffect` instead of the Angular `focusGridRenderedCell`/`focusGridEditor` (those use shadowRoot which React won't have). Query the grid container ref directly.
|
|
220
|
-
|
|
221
|
-
**Keyboard handling:** Port `handleCellKeyDown` and `handleEditorKeyDown` logic directly — it's all pure key checking + command dispatch.
|
|
222
|
-
|
|
223
|
-
## Styles (ui-grid.css)
|
|
224
|
-
|
|
225
|
-
Adapt from `projects/ui-grid/src/lib/grid/grid.core.styles.scss`:
|
|
226
|
-
- Replace `:host` selectors with `.ui-grid-host`
|
|
227
|
-
- Replace `:host *` with `.ui-grid-host *`
|
|
228
|
-
- Keep ALL CSS custom properties identical (same `--ui-grid-*` variables)
|
|
229
|
-
- Keep all class names identical
|
|
230
|
-
- Add `.ui-grid-host { display: block; color: var(--ui-grid-cell-color); }`
|
|
231
|
-
- Ship as plain CSS (no modules needed — class names are scoped by convention)
|
|
232
|
-
|
|
233
|
-
## Tests
|
|
234
|
-
|
|
235
|
-
Use vitest + @testing-library/react + jsdom.
|
|
236
|
-
|
|
237
|
-
**Port these key scenarios from the Angular spec:**
|
|
238
|
-
1. Registers the API and renders headers and rows
|
|
239
|
-
2. Filters rows and renders empty state
|
|
240
|
-
3. Sorts rows and cycles sort state from header button
|
|
241
|
-
4. Groups rows and collapses groups
|
|
242
|
-
5. Exports visible rows as CSV
|
|
243
|
-
6. Virtualizes rows when count crosses threshold
|
|
244
|
-
7. Paginates rows
|
|
245
|
-
8. Keyboard cell editing (commit, navigate, cancel)
|
|
246
|
-
9. Resolves custom i18n label overrides
|
|
247
|
-
10. Feature flags disable unused template sections
|
|
248
|
-
|
|
249
|
-
## Public API (index.ts)
|
|
250
|
-
|
|
251
|
-
```ts
|
|
252
|
-
export { UiGrid } from './UiGrid';
|
|
253
|
-
export type { UiGridProps } from './UiGrid';
|
|
254
|
-
export { useGridState } from './useGridState';
|
|
255
|
-
export { useVirtualScroll } from './useVirtualScroll';
|
|
256
|
-
|
|
257
|
-
// Re-export core types consumers need
|
|
258
|
-
export type {
|
|
259
|
-
GridOptions,
|
|
260
|
-
GridColumnDef,
|
|
261
|
-
GridRow,
|
|
262
|
-
GridRecord,
|
|
263
|
-
GridLabels,
|
|
264
|
-
GridCellTemplateContext,
|
|
265
|
-
GridExpandableTemplateContext,
|
|
266
|
-
GridCellEditableContext,
|
|
267
|
-
GridBenchmarkResult,
|
|
268
|
-
GridSavedState,
|
|
269
|
-
SortState,
|
|
270
|
-
} from '@ornery/ui-grid';
|
|
271
|
-
|
|
272
|
-
export type { UiGridApi } from '@ornery/ui-grid';
|
|
273
|
-
export { DEFAULT_GRID_LABELS } from '@ornery/ui-grid';
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
## Important notes
|
|
277
|
-
|
|
278
|
-
- Do NOT use Shadow DOM — React doesn't support it well. Use a regular div with `className="ui-grid-host"`.
|
|
279
|
-
- Do NOT add any Angular dependencies.
|
|
280
|
-
- The `GridOptions.cellTemplate` field is `TemplateRef` (Angular-specific). The React wrapper ignores it and uses the `cellRenderer` prop instead.
|
|
281
|
-
- Same for `expandableRowTemplate` → use `expandableRenderer` prop.
|
|
282
|
-
- The `GridOptions.onRegisterApi` still works — it's called with the same `UiGridApi` object.
|
|
283
|
-
- Keep the `GridColumnDef.cellRenderer` function (returns string) — it's already framework-agnostic.
|