@rovula/ui 0.1.28 → 0.1.29

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 (65) hide show
  1. package/dist/cjs/bundle.css +501 -67
  2. package/dist/cjs/bundle.js +589 -589
  3. package/dist/cjs/bundle.js.map +1 -1
  4. package/dist/cjs/types/components/DataTable/DataTable.d.ts +195 -4
  5. package/dist/cjs/types/components/DataTable/DataTable.editing.d.ts +20 -0
  6. package/dist/cjs/types/components/DataTable/DataTable.editing.types.d.ts +145 -0
  7. package/dist/cjs/types/components/DataTable/DataTable.stories.d.ts +268 -6
  8. package/dist/cjs/types/components/Dropdown/Dropdown.d.ts +22 -0
  9. package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +4 -0
  10. package/dist/cjs/types/components/ScrollArea/ScrollArea.d.ts +3 -3
  11. package/dist/cjs/types/components/ScrollArea/ScrollArea.stories.d.ts +4 -0
  12. package/dist/cjs/types/components/Table/Table.d.ts +33 -3
  13. package/dist/cjs/types/components/Table/Table.stories.d.ts +86 -4
  14. package/dist/cjs/types/components/TextInput/TextInput.stories.d.ts +8 -0
  15. package/dist/cjs/types/components/TextInput/TextInput.styles.d.ts +1 -0
  16. package/dist/components/DataTable/DataTable.editing.js +385 -0
  17. package/dist/components/DataTable/DataTable.editing.types.js +1 -0
  18. package/dist/components/DataTable/DataTable.js +983 -50
  19. package/dist/components/DataTable/DataTable.stories.js +1077 -25
  20. package/dist/components/Dropdown/Dropdown.js +8 -6
  21. package/dist/components/ScrollArea/ScrollArea.js +2 -2
  22. package/dist/components/ScrollArea/ScrollArea.stories.js +68 -2
  23. package/dist/components/Table/Table.js +103 -13
  24. package/dist/components/Table/Table.stories.js +226 -9
  25. package/dist/components/TextInput/TextInput.js +6 -4
  26. package/dist/components/TextInput/TextInput.stories.js +8 -0
  27. package/dist/components/TextInput/TextInput.styles.js +7 -1
  28. package/dist/esm/bundle.css +501 -67
  29. package/dist/esm/bundle.js +1545 -1545
  30. package/dist/esm/bundle.js.map +1 -1
  31. package/dist/esm/types/components/DataTable/DataTable.d.ts +195 -4
  32. package/dist/esm/types/components/DataTable/DataTable.editing.d.ts +20 -0
  33. package/dist/esm/types/components/DataTable/DataTable.editing.types.d.ts +145 -0
  34. package/dist/esm/types/components/DataTable/DataTable.stories.d.ts +268 -6
  35. package/dist/esm/types/components/Dropdown/Dropdown.d.ts +22 -0
  36. package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +4 -0
  37. package/dist/esm/types/components/ScrollArea/ScrollArea.d.ts +3 -3
  38. package/dist/esm/types/components/ScrollArea/ScrollArea.stories.d.ts +4 -0
  39. package/dist/esm/types/components/Table/Table.d.ts +33 -3
  40. package/dist/esm/types/components/Table/Table.stories.d.ts +86 -4
  41. package/dist/esm/types/components/TextInput/TextInput.stories.d.ts +8 -0
  42. package/dist/esm/types/components/TextInput/TextInput.styles.d.ts +1 -0
  43. package/dist/index.d.ts +493 -122
  44. package/dist/src/theme/global.css +747 -96
  45. package/package.json +14 -2
  46. package/src/components/DataTable/DataTable.editing.tsx +861 -0
  47. package/src/components/DataTable/DataTable.editing.types.ts +192 -0
  48. package/src/components/DataTable/DataTable.stories.tsx +2169 -31
  49. package/src/components/DataTable/DataTable.test.tsx +696 -0
  50. package/src/components/DataTable/DataTable.tsx +2260 -94
  51. package/src/components/Dropdown/Dropdown.tsx +22 -6
  52. package/src/components/ScrollArea/ScrollArea.stories.tsx +146 -3
  53. package/src/components/ScrollArea/ScrollArea.tsx +6 -6
  54. package/src/components/Table/Table.stories.tsx +789 -44
  55. package/src/components/Table/Table.tsx +294 -28
  56. package/src/components/TextInput/TextInput.stories.tsx +80 -0
  57. package/src/components/TextInput/TextInput.styles.ts +7 -1
  58. package/src/components/TextInput/TextInput.tsx +21 -14
  59. package/src/test/setup.ts +50 -0
  60. package/src/theme/global.css +81 -42
  61. package/src/theme/presets/colors.js +12 -0
  62. package/src/theme/themes/variable.css +27 -28
  63. package/src/theme/tokens/baseline.css +2 -1
  64. package/src/theme/tokens/components/scrollbar.css +9 -4
  65. package/src/theme/tokens/components/table.css +63 -0
@@ -0,0 +1,861 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from "react";
2
+ import type { ColumnDef, Row, RowData } from "@tanstack/react-table";
3
+
4
+ import TextInput from "@/components/TextInput/TextInput";
5
+ import NumberInput from "@/components/NumberInput";
6
+ import { Checkbox } from "@/components/Checkbox/Checkbox";
7
+ import Dropdown, { type Options } from "@/components/Dropdown/Dropdown";
8
+ import { customInputVariant } from "@/components/Dropdown/Dropdown.styles";
9
+ import { cn } from "@/utils/cn";
10
+
11
+ import type {
12
+ EditableColumnDef,
13
+ EditDisplayMode,
14
+ EditTrigger,
15
+ EditingState,
16
+ } from "./DataTable.editing.types";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // EditContext — shared between engine and DataTable render layer
20
+ // ---------------------------------------------------------------------------
21
+
22
+ export const EditContext = React.createContext<EditingState | null>(null);
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // useDataTableEditing — manages edit state (row / cell)
26
+ // ---------------------------------------------------------------------------
27
+
28
+ export function useDataTableEditing<TData extends RowData>(opts: {
29
+ enabled: boolean;
30
+ editDisplayMode: EditDisplayMode;
31
+ editTrigger: EditTrigger;
32
+ editableColumnIds: string[];
33
+ }): EditingState {
34
+ const { enabled, editDisplayMode, editTrigger, editableColumnIds } = opts;
35
+
36
+ const [editingRowId, setEditingRowId] = useState<string | null>(null);
37
+ const [editingCell, setEditingCell] = useState<{
38
+ rowId: string;
39
+ columnId: string;
40
+ } | null>(null);
41
+ const tabMovingRef = React.useRef(false);
42
+
43
+ const requestRowEdit = useCallback((rowId: string) => {
44
+ setEditingRowId(rowId);
45
+ setEditingCell(null);
46
+ }, []);
47
+
48
+ const requestCellEdit = useCallback((rowId: string, columnId: string) => {
49
+ setEditingCell({ rowId, columnId });
50
+ setEditingRowId(null);
51
+ }, []);
52
+
53
+ const clearEdit = useCallback(() => {
54
+ setEditingRowId(null);
55
+ setEditingCell(null);
56
+ }, []);
57
+
58
+ const onFieldBlur = useCallback(
59
+ (_rowId: string, _columnId: string) => {
60
+ if (tabMovingRef.current) return;
61
+ if (editDisplayMode === "cell") {
62
+ setEditingCell(null);
63
+ }
64
+ },
65
+ [editDisplayMode],
66
+ );
67
+
68
+ const moveCellEdit = useCallback(
69
+ (
70
+ rowId: string,
71
+ fromColumnId: string,
72
+ delta: -1 | 1,
73
+ ): "moved" | "exited" => {
74
+ const idx = editableColumnIds.indexOf(fromColumnId);
75
+ if (idx < 0) {
76
+ setEditingCell(null);
77
+ return "exited";
78
+ }
79
+ const next = idx + delta;
80
+ if (next < 0 || next >= editableColumnIds.length) {
81
+ setEditingCell(null);
82
+ return "exited";
83
+ }
84
+ tabMovingRef.current = true;
85
+ setEditingCell({ rowId, columnId: editableColumnIds[next]! });
86
+ requestAnimationFrame(() => {
87
+ tabMovingRef.current = false;
88
+ });
89
+ return "moved";
90
+ },
91
+ [editableColumnIds],
92
+ );
93
+
94
+ // Esc exits row edit
95
+ useEffect(() => {
96
+ if (!enabled || editDisplayMode !== "row" || !editingRowId) return;
97
+ const onKey = (e: KeyboardEvent) => {
98
+ if (e.key === "Escape") setEditingRowId(null);
99
+ };
100
+ document.addEventListener("keydown", onKey);
101
+ return () => document.removeEventListener("keydown", onKey);
102
+ }, [enabled, editDisplayMode, editingRowId]);
103
+
104
+ return useMemo<EditingState>(
105
+ () => ({
106
+ editingRowId,
107
+ editingCell,
108
+ editDisplayMode,
109
+ editTrigger,
110
+ requestRowEdit,
111
+ requestCellEdit,
112
+ clearEdit,
113
+ onFieldBlur,
114
+ moveCellEdit,
115
+ }),
116
+ [
117
+ editingRowId,
118
+ editingCell,
119
+ editDisplayMode,
120
+ editTrigger,
121
+ requestRowEdit,
122
+ requestCellEdit,
123
+ clearEdit,
124
+ onFieldBlur,
125
+ moveCellEdit,
126
+ ],
127
+ );
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // resolveEditableColumns — wraps EditableColumnDef[] → ColumnDef[]
132
+ // ---------------------------------------------------------------------------
133
+
134
+ const EDIT_SELECT_PH_PREFIX = "__edit_ph__";
135
+
136
+ function editPlaceholderOption(label: string): Options {
137
+ return { value: `${EDIT_SELECT_PH_PREFIX}:${label}`, label };
138
+ }
139
+
140
+ function isPlaceholderOption(opt: Options | null | undefined) {
141
+ return opt?.value.startsWith(`${EDIT_SELECT_PH_PREFIX}:`) ?? false;
142
+ }
143
+
144
+ type ResolveOpts<TData extends RowData> = {
145
+ editing: EditingState;
146
+ onCellCommit?: (
147
+ rowId: string,
148
+ columnId: string,
149
+ patch: Partial<TData>,
150
+ ) => void;
151
+ alwaysEditing?: (row: Row<TData>) => boolean;
152
+ enableCellTabTraversal: boolean;
153
+ editableColumnIds: string[];
154
+ };
155
+
156
+ function isEditingCell<TData extends RowData>(
157
+ editing: EditingState,
158
+ row: Row<TData>,
159
+ columnId: string,
160
+ ): boolean {
161
+ const rowId =
162
+ typeof (row.original as Record<string, unknown>).id === "string"
163
+ ? ((row.original as Record<string, unknown>).id as string)
164
+ : row.id;
165
+ if (editing.editDisplayMode === "row") return editing.editingRowId === rowId;
166
+ return (
167
+ editing.editingCell?.rowId === rowId &&
168
+ editing.editingCell.columnId === columnId
169
+ );
170
+ }
171
+
172
+ function getRowId<TData extends RowData>(row: Row<TData>): string {
173
+ const orig = row.original as Record<string, unknown>;
174
+ return typeof orig.id === "string" ? orig.id : row.id;
175
+ }
176
+
177
+ function coerceValue(variant: string | undefined, rawValue: string): unknown {
178
+ if (variant === "number") {
179
+ const n = Number(rawValue);
180
+ return Number.isNaN(n) ? 0 : n;
181
+ }
182
+ if (variant === "checkbox") {
183
+ return rawValue === "true";
184
+ }
185
+ return rawValue;
186
+ }
187
+
188
+ function buildPatch<TData extends RowData>(
189
+ col: EditableColumnDef<TData>,
190
+ row: Row<TData>,
191
+ rawValue: string,
192
+ columnId: string,
193
+ ): Partial<TData> {
194
+ if (col.onCommit) {
195
+ return col.onCommit(row, rawValue);
196
+ }
197
+ return { [columnId]: coerceValue(col.editVariant, rawValue) } as Partial<TData>;
198
+ }
199
+
200
+ export function resolveEditableColumns<TData extends RowData>(
201
+ columns: EditableColumnDef<TData>[],
202
+ opts: ResolveOpts<TData>,
203
+ ): ColumnDef<TData, unknown>[] {
204
+ const {
205
+ editing,
206
+ onCellCommit,
207
+ alwaysEditing,
208
+ enableCellTabTraversal,
209
+ editableColumnIds,
210
+ } = opts;
211
+
212
+ return columns.map((col) => {
213
+ const colEditing = col.enableEditing;
214
+ if (colEditing == null && col.editVariant == null) {
215
+ return col as ColumnDef<TData, unknown>;
216
+ }
217
+
218
+ const columnId =
219
+ (col as { accessorKey?: string }).accessorKey ??
220
+ (col as { id?: string }).id ??
221
+ "";
222
+
223
+ const originalCell = (col as ColumnDef<TData, unknown>).cell;
224
+
225
+ const wrappedCol: ColumnDef<TData, unknown> = {
226
+ ...(col as ColumnDef<TData, unknown>),
227
+ cell: (cellProps) => {
228
+ const { row } = cellProps;
229
+ const rowId = getRowId(row);
230
+
231
+ const canEdit =
232
+ typeof colEditing === "function"
233
+ ? colEditing(row)
234
+ : colEditing !== false;
235
+
236
+ if (!canEdit) {
237
+ const always = alwaysEditing?.(row) ?? false;
238
+ const isRowActive =
239
+ always ||
240
+ editing.editingRowId === rowId ||
241
+ editing.editingCell?.rowId === rowId;
242
+
243
+ const showDisabledField =
244
+ always || (col.editShowDisabledField && isRowActive);
245
+
246
+ if (showDisabledField && (col.editVariant || colEditing != null)) {
247
+ return <DisabledEditCell row={row} col={col} columnId={columnId} />;
248
+ }
249
+
250
+ if (col.displayCell) {
251
+ return col.displayCell(row);
252
+ }
253
+ if (originalCell) {
254
+ return typeof originalCell === "function"
255
+ ? originalCell(cellProps)
256
+ : originalCell;
257
+ }
258
+ const value = (row.original as Record<string, unknown>)[columnId];
259
+ return <>{String(value ?? "")}</>;
260
+ }
261
+
262
+ const always = alwaysEditing?.(row) ?? false;
263
+ const active = always || isEditingCell(editing, row, columnId);
264
+
265
+ if (!active) {
266
+ const previewContent = col.displayCell
267
+ ? col.displayCell(row)
268
+ : String((row.original as Record<string, unknown>)[columnId] ?? "");
269
+
270
+ const activate = () => {
271
+ if (editing.editDisplayMode === "row")
272
+ editing.requestRowEdit(rowId);
273
+ else editing.requestCellEdit(rowId, columnId);
274
+ };
275
+ const handler =
276
+ editing.editTrigger === "click"
277
+ ? { onClick: activate }
278
+ : { onDoubleClick: activate };
279
+
280
+ return (
281
+ <div
282
+ className={cn(
283
+ "-mx-1 flex min-h-[32px] w-full min-w-0 cursor-pointer items-center rounded-sm px-1",
284
+ "hover:bg-table-c-hover/35",
285
+ )}
286
+ {...handler}
287
+ >
288
+ {previewContent}
289
+ </div>
290
+ );
291
+ }
292
+
293
+ // ── Edit mode ──
294
+ const variant = col.editVariant ?? "text";
295
+ const autoFocus = !always && editing.editDisplayMode === "cell";
296
+ const errorMsg = col.editError?.(row);
297
+
298
+ const commitAndBlur = (value: string) => {
299
+ const patch = buildPatch(col, row, value, columnId);
300
+ onCellCommit?.(rowId, columnId, patch);
301
+ if (!always) editing.onFieldBlur(rowId, columnId);
302
+ };
303
+
304
+ const handleTab = enableCellTabTraversal
305
+ ? (
306
+ e: React.KeyboardEvent<HTMLInputElement>,
307
+ commitFn: () => void,
308
+ ) => {
309
+ if (e.key !== "Tab") return;
310
+
311
+ if (editing.editDisplayMode === "row") {
312
+ const idx = editableColumnIds.indexOf(columnId);
313
+ if (idx < 0) return;
314
+ const nextIdx = idx + (e.shiftKey ? -1 : 1);
315
+ if (nextIdx < 0 || nextIdx >= editableColumnIds.length) return;
316
+ const nextColId = editableColumnIds[nextIdx]!;
317
+
318
+ commitFn();
319
+ e.preventDefault();
320
+ requestAnimationFrame(() => {
321
+ const el = document.querySelector<HTMLElement>(
322
+ `[data-edit-cell="${rowId}:${nextColId}"] input, [data-edit-cell="${rowId}:${nextColId}"] button`,
323
+ );
324
+ el?.focus();
325
+ });
326
+ return;
327
+ }
328
+
329
+ commitFn();
330
+ if (
331
+ editing.moveCellEdit(rowId, columnId, e.shiftKey ? -1 : 1) ===
332
+ "moved"
333
+ ) {
334
+ e.preventDefault();
335
+ }
336
+ }
337
+ : undefined;
338
+
339
+ if (variant === "select") {
340
+ return (
341
+ <div data-edit-cell={`${rowId}:${columnId}`}>
342
+ <EditCellSelect
343
+ row={row}
344
+ columnId={columnId}
345
+ col={col}
346
+ autoFocus={autoFocus}
347
+ keepOpenAfterSelect={
348
+ editing.editDisplayMode === "row" || always
349
+ }
350
+ onCommit={(label) => {
351
+ const currentValue = String(
352
+ (row.original as Record<string, unknown>)[columnId] ?? "",
353
+ );
354
+ if (label.trim() === currentValue.trim()) return;
355
+ const patch = buildPatch(col, row, label, columnId);
356
+ onCellCommit?.(rowId, columnId, patch);
357
+ }}
358
+ onBlurField={
359
+ editing.editDisplayMode === "row"
360
+ ? undefined
361
+ : () => {
362
+ if (!always) editing.onFieldBlur(rowId, columnId);
363
+ }
364
+ }
365
+ onKeyDown={
366
+ handleTab
367
+ ? (e) =>
368
+ handleTab(
369
+ e as React.KeyboardEvent<HTMLInputElement>,
370
+ () => {},
371
+ )
372
+ : undefined
373
+ }
374
+ errorMessage={errorMsg}
375
+ />
376
+ </div>
377
+ );
378
+ }
379
+
380
+ if (variant === "number") {
381
+ return (
382
+ <div data-edit-cell={`${rowId}:${columnId}`}>
383
+ <EditCellNumber
384
+ columnId={columnId}
385
+ row={row}
386
+ col={col}
387
+ autoFocus={autoFocus}
388
+ errorMessage={errorMsg}
389
+ onBlur={(val) => commitAndBlur(val)}
390
+ onKeyDown={
391
+ handleTab
392
+ ? (e) =>
393
+ handleTab(e, () => {
394
+ const v = (
395
+ e.target as HTMLInputElement
396
+ ).value.trim();
397
+ const patch = buildPatch(col, row, v, columnId);
398
+ onCellCommit?.(rowId, columnId, patch);
399
+ })
400
+ : undefined
401
+ }
402
+ />
403
+ </div>
404
+ );
405
+ }
406
+
407
+ if (variant === "checkbox") {
408
+ return (
409
+ <div data-edit-cell={`${rowId}:${columnId}`}>
410
+ <EditCellCheckbox
411
+ columnId={columnId}
412
+ row={row}
413
+ col={col}
414
+ errorMessage={errorMsg}
415
+ onCommit={(checked) => {
416
+ const patch = buildPatch(
417
+ col,
418
+ row,
419
+ String(checked),
420
+ columnId,
421
+ );
422
+ onCellCommit?.(rowId, columnId, patch);
423
+ }}
424
+ onKeyDown={
425
+ handleTab
426
+ ? (e) =>
427
+ handleTab(
428
+ e as React.KeyboardEvent<HTMLInputElement>,
429
+ () => {},
430
+ )
431
+ : undefined
432
+ }
433
+ />
434
+ </div>
435
+ );
436
+ }
437
+
438
+ if (variant === "custom" && col.editCustomCell) {
439
+ const blur = () => {
440
+ if (!always) editing.onFieldBlur(rowId, columnId);
441
+ };
442
+
443
+ return (
444
+ <div data-edit-cell={`${rowId}:${columnId}`}>
445
+ {col.editCustomCell(row, {
446
+ commit: (rawValue: string) => {
447
+ const patch = buildPatch(col, row, rawValue, columnId);
448
+ onCellCommit?.(rowId, columnId, patch);
449
+ },
450
+ blur,
451
+ columnId,
452
+ autoFocus,
453
+ errorMessage: errorMsg,
454
+ onKeyDown: handleTab
455
+ ? (e) =>
456
+ handleTab(
457
+ e as React.KeyboardEvent<HTMLInputElement>,
458
+ () => {},
459
+ )
460
+ : undefined,
461
+ })}
462
+ </div>
463
+ );
464
+ }
465
+
466
+ // variant === "text"
467
+ return (
468
+ <div data-edit-cell={`${rowId}:${columnId}`}>
469
+ <EditCellText
470
+ columnId={columnId}
471
+ row={row}
472
+ placeholder={col.editTextProps?.placeholder ?? ""}
473
+ autoFocus={autoFocus}
474
+ errorMessage={errorMsg}
475
+ onBlur={(e) => commitAndBlur(e.currentTarget.value.trim())}
476
+ onKeyDown={
477
+ handleTab
478
+ ? (e) =>
479
+ handleTab(e, () => {
480
+ const v = (e.target as HTMLInputElement).value.trim();
481
+ const patch = buildPatch(col, row, v, columnId);
482
+ onCellCommit?.(rowId, columnId, patch);
483
+ })
484
+ : undefined
485
+ }
486
+ />
487
+ </div>
488
+ );
489
+ },
490
+ };
491
+
492
+ return wrappedCol;
493
+ });
494
+ }
495
+
496
+ // ---------------------------------------------------------------------------
497
+ // Built-in cell editors
498
+ // ---------------------------------------------------------------------------
499
+
500
+ function EditCellText<TData extends RowData>({
501
+ columnId,
502
+ row,
503
+ placeholder,
504
+ autoFocus,
505
+ errorMessage,
506
+ onBlur,
507
+ onKeyDown,
508
+ }: {
509
+ columnId: string;
510
+ row: Row<TData>;
511
+ placeholder: string;
512
+ autoFocus: boolean;
513
+ errorMessage?: string;
514
+ onBlur: React.FocusEventHandler<HTMLInputElement>;
515
+ onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
516
+ }) {
517
+ const value = String(
518
+ (row.original as Record<string, unknown>)[columnId] ?? "",
519
+ );
520
+ return (
521
+ <TextInput
522
+ size="sm"
523
+ variant="outline"
524
+ rounded="normal"
525
+ fullwidth
526
+ isFloatingLabel={false}
527
+ label=""
528
+ required={false}
529
+ hasClearIcon={false}
530
+ keepFooterSpace={false}
531
+ defaultValue={value}
532
+ placeholder={placeholder}
533
+ aria-label={placeholder || columnId}
534
+ autoFocus={autoFocus}
535
+ error={!!errorMessage}
536
+ errorMessage={errorMessage}
537
+ onBlur={onBlur}
538
+ onKeyDown={onKeyDown}
539
+ className="rounded-md px-2 py-2 typography-small2"
540
+ />
541
+ );
542
+ }
543
+
544
+ function EditCellSelect<TData extends RowData>({
545
+ row,
546
+ columnId,
547
+ col,
548
+ autoFocus,
549
+ onCommit,
550
+ onBlurField,
551
+ onKeyDown,
552
+ keepOpenAfterSelect,
553
+ errorMessage,
554
+ }: {
555
+ row: Row<TData>;
556
+ columnId: string;
557
+ col: EditableColumnDef<TData>;
558
+ autoFocus: boolean;
559
+ onCommit: (label: string) => void;
560
+ onBlurField?: () => void;
561
+ onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
562
+ keepOpenAfterSelect?: boolean;
563
+ errorMessage?: string;
564
+ }) {
565
+ const displayValue = String(
566
+ (row.original as Record<string, unknown>)[columnId] ?? "",
567
+ );
568
+
569
+ const rawOptions =
570
+ typeof col.editSelectProps?.options === "function"
571
+ ? col.editSelectProps.options(row)
572
+ : col.editSelectProps?.options ?? [];
573
+
574
+ const options = useMemo<Options[]>(() => {
575
+ const ph = col.editSelectProps?.placeholder ?? columnId;
576
+ return [editPlaceholderOption(ph), ...rawOptions];
577
+ }, [rawOptions, col.editSelectProps?.placeholder, columnId]);
578
+
579
+ const selected = useMemo(() => {
580
+ const cur = displayValue.trim();
581
+ if (!cur) return options[0]!;
582
+ return options.find((o) => o.label === cur) ?? options[0]!;
583
+ }, [displayValue, options]);
584
+
585
+ return (
586
+ <div>
587
+ <Dropdown
588
+ id={`edit-dd-${columnId}-${row.id}`}
589
+ options={options}
590
+ value={selected}
591
+ onSelect={(opt) => {
592
+ if (isPlaceholderOption(opt)) return;
593
+ onCommit(opt.label);
594
+ }}
595
+ size="sm"
596
+ variant="outline"
597
+ rounded="normal"
598
+ fullwidth
599
+ required={false}
600
+ isFloatingLabel={false}
601
+ label=""
602
+ keepFooterSpace={false}
603
+ segmentedInput
604
+ autoFocus={autoFocus}
605
+ onBlur={onBlurField}
606
+ onKeyDown={onKeyDown}
607
+ keepOpenAfterSelect={keepOpenAfterSelect}
608
+ portal
609
+ error={!!errorMessage}
610
+ errorMessage={errorMessage}
611
+ className={cn(
612
+ "rounded-md",
613
+ customInputVariant({ size: "sm" }),
614
+ displayValue.trim().length > 0 && "font-medium",
615
+ )}
616
+ />
617
+ </div>
618
+ );
619
+ }
620
+
621
+ // ---------------------------------------------------------------------------
622
+ // EditCellNumber — inline number editor
623
+ // ---------------------------------------------------------------------------
624
+
625
+ function EditCellNumber<TData extends RowData>({
626
+ columnId,
627
+ row,
628
+ col,
629
+ autoFocus,
630
+ errorMessage,
631
+ onBlur,
632
+ onKeyDown,
633
+ }: {
634
+ columnId: string;
635
+ row: Row<TData>;
636
+ col: EditableColumnDef<TData>;
637
+ autoFocus: boolean;
638
+ errorMessage?: string;
639
+ onBlur: (value: string) => void;
640
+ onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
641
+ }) {
642
+ const rawValue = (row.original as Record<string, unknown>)[columnId];
643
+ const numericValue =
644
+ rawValue === "" || rawValue == null ? undefined : Number(rawValue);
645
+ const np = col.editNumberProps;
646
+
647
+ return (
648
+ <NumberInput
649
+ size="sm"
650
+ variant="outline"
651
+ rounded="normal"
652
+ fullwidth
653
+ isFloatingLabel={false}
654
+ label=""
655
+ required={false}
656
+ hasClearIcon={false}
657
+ keepFooterSpace={false}
658
+ defaultValue={numericValue}
659
+ placeholder={np?.placeholder ?? columnId}
660
+ aria-label={np?.placeholder || columnId}
661
+ autoFocus={autoFocus}
662
+ min={np?.min}
663
+ max={np?.max}
664
+ step={np?.step}
665
+ precision={np?.precision}
666
+ allowDecimal={np?.allowDecimal}
667
+ allowNegative={np?.allowNegative}
668
+ hideControls
669
+ error={!!errorMessage}
670
+ errorMessage={errorMessage}
671
+ onBlur={(e) => onBlur(e.currentTarget.value.trim())}
672
+ onKeyDown={onKeyDown}
673
+ className="rounded-md px-2 py-2 typography-small2"
674
+ />
675
+ );
676
+ }
677
+
678
+ // ---------------------------------------------------------------------------
679
+ // EditCellCheckbox — inline checkbox editor
680
+ // ---------------------------------------------------------------------------
681
+
682
+ function EditCellCheckbox<TData extends RowData>({
683
+ columnId,
684
+ row,
685
+ col,
686
+ errorMessage,
687
+ onCommit,
688
+ onKeyDown,
689
+ }: {
690
+ columnId: string;
691
+ row: Row<TData>;
692
+ col: EditableColumnDef<TData>;
693
+ errorMessage?: string;
694
+ onCommit: (checked: boolean) => void;
695
+ onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
696
+ }) {
697
+ const rawValue = (row.original as Record<string, unknown>)[columnId];
698
+ const checked = rawValue === true || rawValue === "true";
699
+ const label = col.editCheckboxProps?.label;
700
+
701
+ return (
702
+ <div className="flex flex-col gap-0.5">
703
+ <div className="flex items-center gap-2 min-h-[32px]">
704
+ <Checkbox
705
+ checked={checked}
706
+ onCheckedChange={(val) => {
707
+ onCommit(val === true);
708
+ }}
709
+ onKeyDown={
710
+ onKeyDown
711
+ ? (e) =>
712
+ onKeyDown(
713
+ e as unknown as React.KeyboardEvent<HTMLInputElement>,
714
+ )
715
+ : undefined
716
+ }
717
+ aria-label={label || columnId}
718
+ />
719
+ {label && (
720
+ <span className="typography-small2 text-text-contrast-high select-none">
721
+ {label}
722
+ </span>
723
+ )}
724
+ </div>
725
+ {errorMessage && (
726
+ <span className="typography-small3 text-error-500">{errorMessage}</span>
727
+ )}
728
+ </div>
729
+ );
730
+ }
731
+
732
+ // ---------------------------------------------------------------------------
733
+ // Disabled cell editor — shown in alwaysEditing rows when canEdit is false
734
+ // ---------------------------------------------------------------------------
735
+
736
+ function DisabledEditCell<TData extends RowData>({
737
+ row,
738
+ col,
739
+ columnId,
740
+ }: {
741
+ row: Row<TData>;
742
+ col: EditableColumnDef<TData>;
743
+ columnId: string;
744
+ }) {
745
+ const variant = col.editVariant ?? "text";
746
+
747
+ if (variant === "select") {
748
+ const rawOptions =
749
+ typeof col.editSelectProps?.options === "function"
750
+ ? col.editSelectProps.options(row)
751
+ : col.editSelectProps?.options ?? [];
752
+
753
+ const ph = col.editSelectProps?.placeholder ?? columnId;
754
+ const options = [editPlaceholderOption(ph), ...rawOptions];
755
+ const displayValue = String(
756
+ (row.original as Record<string, unknown>)[columnId] ?? "",
757
+ );
758
+ const selected =
759
+ options.find((o) => o.label === displayValue.trim()) ?? options[0]!;
760
+
761
+ return (
762
+ <Dropdown
763
+ id={`edit-dd-disabled-${columnId}-${row.id}`}
764
+ options={options}
765
+ value={selected}
766
+ onSelect={() => {}}
767
+ size="sm"
768
+ variant="outline"
769
+ rounded="normal"
770
+ fullwidth
771
+ required={false}
772
+ isFloatingLabel={false}
773
+ label=""
774
+ keepFooterSpace={false}
775
+ segmentedInput
776
+ disabled
777
+ className={cn("rounded-md", customInputVariant({ size: "sm" }))}
778
+ />
779
+ );
780
+ }
781
+
782
+ if (variant === "number") {
783
+ const rawValue = (row.original as Record<string, unknown>)[columnId];
784
+ const numericValue =
785
+ rawValue === "" || rawValue == null ? undefined : Number(rawValue);
786
+ return (
787
+ <NumberInput
788
+ size="sm"
789
+ variant="outline"
790
+ rounded="normal"
791
+ fullwidth
792
+ isFloatingLabel={false}
793
+ label=""
794
+ required={false}
795
+ hasClearIcon={false}
796
+ keepFooterSpace={false}
797
+ defaultValue={numericValue}
798
+ disabled
799
+ hideControls
800
+ className="rounded-md px-2 py-2 typography-small2"
801
+ />
802
+ );
803
+ }
804
+
805
+ if (variant === "checkbox") {
806
+ const rawValue = (row.original as Record<string, unknown>)[columnId];
807
+ const checked = rawValue === true || rawValue === "true";
808
+ return (
809
+ <div className="flex items-center gap-2 min-h-[32px]">
810
+ <Checkbox checked={checked} disabled />
811
+ {col.editCheckboxProps?.label && (
812
+ <span className="typography-small2 text-text-g-contrast-medium select-none">
813
+ {col.editCheckboxProps.label}
814
+ </span>
815
+ )}
816
+ </div>
817
+ );
818
+ }
819
+
820
+ const value = String(
821
+ (row.original as Record<string, unknown>)[columnId] ?? "",
822
+ );
823
+ const placeholder = col.editTextProps?.placeholder ?? "";
824
+
825
+ return (
826
+ <TextInput
827
+ size="sm"
828
+ variant="outline"
829
+ rounded="normal"
830
+ fullwidth
831
+ isFloatingLabel={false}
832
+ label=""
833
+ required={false}
834
+ hasClearIcon={false}
835
+ keepFooterSpace={false}
836
+ defaultValue={value}
837
+ placeholder={placeholder}
838
+ aria-label={placeholder || columnId}
839
+ disabled
840
+ className="rounded-md px-2 py-2 typography-small2"
841
+ />
842
+ );
843
+ }
844
+
845
+ // ---------------------------------------------------------------------------
846
+ // Utility: detect editable column ids from column defs
847
+ // ---------------------------------------------------------------------------
848
+
849
+ export function detectEditableColumnIds<TData extends RowData>(
850
+ columns: EditableColumnDef<TData>[],
851
+ ): string[] {
852
+ return columns
853
+ .filter((col) => col.enableEditing != null || col.editVariant != null)
854
+ .map(
855
+ (col) =>
856
+ (col as { accessorKey?: string }).accessorKey ??
857
+ (col as { id?: string }).id ??
858
+ "",
859
+ )
860
+ .filter(Boolean);
861
+ }