@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/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: end;
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
  }
@@ -88,19 +88,10 @@ import {
88
88
  FEATURE_AUTO_RESIZE,
89
89
  FEATURE_SAVE_STATE,
90
90
  FEATURE_PINNING,
91
- } from '@ornery/ui-grid';
92
- import type {
93
- DisplayItem,
94
- GroupItem,
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
- } from '../../ui-grid/src/lib/grid/ui-grid.events';
149
- import { downloadGridCsvFile, observeGridHostSize } from '../../ui-grid/src/lib/grid/ui-grid.host';
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
- return orderVisibleColumns(options.columnDefs, columnOrder);
402
- }, [options.columnDefs, columnOrder]);
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;
@@ -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,6 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "paths": {}
5
+ }
6
+ }
@@ -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.