@dxos/react-ui-table 0.9.1-staging.ee54ba693a → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/dist/lib/chunk-types.mjs +65 -0
  2. package/dist/lib/chunk-types.mjs.map +1 -0
  3. package/dist/lib/chunk-useTableModel.mjs +1595 -0
  4. package/dist/lib/chunk-useTableModel.mjs.map +1 -0
  5. package/dist/lib/index.mjs +1133 -0
  6. package/dist/lib/index.mjs.map +1 -0
  7. package/dist/lib/testing.mjs +88 -0
  8. package/dist/lib/testing.mjs.map +1 -0
  9. package/dist/lib/translations.mjs +30 -0
  10. package/dist/lib/translations.mjs.map +1 -0
  11. package/dist/lib/types.mjs +2 -0
  12. package/dist/types/src/components/Table/DynamicTable.d.ts +4 -1
  13. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  14. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +3 -1
  15. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Table/Relations.stories.d.ts +1 -0
  17. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
  18. package/dist/types/src/components/Table/Table.d.ts +3 -9
  19. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  20. package/dist/types/src/components/Table/Table.stories.d.ts +2 -0
  21. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  22. package/dist/types/src/components/Table/TableContent.d.ts +1 -1
  23. package/dist/types/src/components/Table/TableContent.d.ts.map +1 -1
  24. package/dist/types/src/components/Table/TableContext.d.ts +10 -0
  25. package/dist/types/src/components/Table/TableContext.d.ts.map +1 -0
  26. package/dist/types/src/components/Table/TableToolbar.d.ts +1 -1
  27. package/dist/types/src/components/Table/TableToolbar.d.ts.map +1 -1
  28. package/dist/types/src/components/Table/index.d.ts +1 -0
  29. package/dist/types/src/components/Table/index.d.ts.map +1 -1
  30. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  31. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  32. package/dist/types/src/components/testing.d.ts +1 -1
  33. package/dist/types/src/components/testing.d.ts.map +1 -1
  34. package/dist/types/src/hooks/useProjectionModel.d.ts +1 -1
  35. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -1
  36. package/dist/types/src/hooks/useTableModel.d.ts +8 -1
  37. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  38. package/dist/types/src/model/modal-controller.d.ts +1 -1
  39. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  40. package/dist/types/src/model/selection-model.d.ts +1 -1
  41. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  42. package/dist/types/src/model/table-model.d.ts +42 -7
  43. package/dist/types/src/model/table-model.d.ts.map +1 -1
  44. package/dist/types/src/model/table-presentation.d.ts +20 -5
  45. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  46. package/dist/types/src/model/table-view-state.d.ts +5 -1
  47. package/dist/types/src/model/table-view-state.d.ts.map +1 -1
  48. package/dist/types/src/types/index.d.ts.map +1 -1
  49. package/dist/types/src/util/dynamic-table.d.ts +1 -1
  50. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  51. package/dist/types/src/util/table-controls.d.ts +5 -3
  52. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  53. package/dist/types/tsconfig.tsbuildinfo +1 -1
  54. package/package.json +48 -50
  55. package/src/components/Table/DynamicTable.stories.tsx +59 -1
  56. package/src/components/Table/DynamicTable.tsx +16 -8
  57. package/src/components/Table/Relations.stories.tsx +2 -2
  58. package/src/components/Table/Table.stories.tsx +76 -2
  59. package/src/components/Table/Table.tsx +3 -15
  60. package/src/components/Table/TableContent.tsx +6 -5
  61. package/src/components/Table/TableContext.ts +21 -0
  62. package/src/components/Table/index.ts +2 -0
  63. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +2 -3
  64. package/src/components/TableCellEditor/TableCellEditor.tsx +27 -22
  65. package/src/components/testing.ts +1 -1
  66. package/src/hooks/useProjectionModel.ts +1 -1
  67. package/src/hooks/useTableModel.ts +18 -6
  68. package/src/model/modal-controller.ts +1 -1
  69. package/src/model/selection-model.ts +1 -1
  70. package/src/model/table-model.test.ts +3 -3
  71. package/src/model/table-model.ts +148 -31
  72. package/src/model/table-presentation.test.ts +59 -1
  73. package/src/model/table-presentation.ts +171 -81
  74. package/src/model/table-view-state.ts +2 -2
  75. package/src/testing/useTestTableModel.ts +1 -1
  76. package/src/types/index.ts +4 -0
  77. package/src/util/dynamic-table.test.ts +1 -1
  78. package/src/util/dynamic-table.ts +1 -1
  79. package/src/util/table-buttons.ts +1 -1
  80. package/src/util/table-controls.ts +18 -10
  81. package/dist/lib/browser/chunk-J5LGTIGS.mjs +0 -10
  82. package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +0 -7
  83. package/dist/lib/browser/chunk-K4J2CKTZ.mjs +0 -1751
  84. package/dist/lib/browser/chunk-K4J2CKTZ.mjs.map +0 -7
  85. package/dist/lib/browser/chunk-VV3P3ADH.mjs +0 -67
  86. package/dist/lib/browser/chunk-VV3P3ADH.mjs.map +0 -7
  87. package/dist/lib/browser/index.mjs +0 -1416
  88. package/dist/lib/browser/index.mjs.map +0 -7
  89. package/dist/lib/browser/meta.json +0 -1
  90. package/dist/lib/browser/testing/index.mjs +0 -115
  91. package/dist/lib/browser/testing/index.mjs.map +0 -7
  92. package/dist/lib/browser/translations.mjs +0 -39
  93. package/dist/lib/browser/translations.mjs.map +0 -7
  94. package/dist/lib/browser/types/index.mjs +0 -8
  95. package/dist/lib/browser/types/index.mjs.map +0 -7
  96. package/dist/lib/node-esm/chunk-E6V274ZW.mjs +0 -1753
  97. package/dist/lib/node-esm/chunk-E6V274ZW.mjs.map +0 -7
  98. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  99. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  100. package/dist/lib/node-esm/chunk-RPEWNOZZ.mjs +0 -68
  101. package/dist/lib/node-esm/chunk-RPEWNOZZ.mjs.map +0 -7
  102. package/dist/lib/node-esm/index.mjs +0 -1417
  103. package/dist/lib/node-esm/index.mjs.map +0 -7
  104. package/dist/lib/node-esm/meta.json +0 -1
  105. package/dist/lib/node-esm/testing/index.mjs +0 -116
  106. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  107. package/dist/lib/node-esm/translations.mjs +0 -40
  108. package/dist/lib/node-esm/translations.mjs.map +0 -7
  109. package/dist/lib/node-esm/types/index.mjs +0 -9
  110. package/dist/lib/node-esm/types/index.mjs.map +0 -7
@@ -2,13 +2,13 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Atom, type Registry } from '@effect-atom/atom-react';
5
+ import { type Registry } from '@effect-atom/atom';
6
6
  import * as Predicate from 'effect/Predicate';
7
7
 
8
8
  import { Obj, type View } from '@dxos/echo';
9
9
  import { Format, TypeEnum } from '@dxos/echo/Format';
10
10
  import { SchemaEx } from '@dxos/effect';
11
- import { cellClassesForFieldType, formatForDisplay } from '@dxos/react-ui-form';
11
+ import { cellClassesForFieldType } from '@dxos/react-ui-form';
12
12
  import {
13
13
  type DxGridCellValue,
14
14
  type DxGridPlane,
@@ -16,6 +16,7 @@ import {
16
16
  type DxGridPlaneRange,
17
17
  toPlaneCellIndex,
18
18
  } from '@dxos/react-ui-grid';
19
+ import { formatForDisplay } from '@dxos/schema';
19
20
  import { VIEW_FIELD_LIMIT } from '@dxos/schema';
20
21
  import { mx } from '@dxos/ui-theme';
21
22
 
@@ -30,7 +31,6 @@ import { type TableModel, type TableRow } from './table-model';
30
31
  */
31
32
  export class TablePresentation<T extends TableRow = TableRow> {
32
33
  private readonly _registry: Registry.Registry;
33
- private readonly _visibleRange: Atom.Writable<DxGridPlaneRange>;
34
34
  private fieldProjectionCache = new Map<string, ReturnType<typeof this.model.projection.getFieldProjection>>();
35
35
 
36
36
  constructor(
@@ -38,10 +38,6 @@ export class TablePresentation<T extends TableRow = TableRow> {
38
38
  private readonly model: TableModel<T>,
39
39
  ) {
40
40
  this._registry = registry;
41
- this._visibleRange = Atom.make<DxGridPlaneRange>({
42
- start: { row: 0, col: 0 },
43
- end: { row: 0, col: 0 },
44
- });
45
41
  }
46
42
 
47
43
  public getCells(range: DxGridPlaneRange, plane: DxGridPlane): DxGridPlaneCells {
@@ -52,20 +48,21 @@ export class TablePresentation<T extends TableRow = TableRow> {
52
48
 
53
49
  switch (plane) {
54
50
  case 'grid':
55
- this._registry.set(this._visibleRange, range);
51
+ // Report the visible range to the model so it can (re)subscribe to those rows' mutations.
52
+ this.model.setVisibleRange(range);
56
53
  cells = this.getMainGridCells(range);
57
54
  break;
58
55
  case 'frozenRowsStart':
59
56
  cells = this.getHeaderCells(range);
60
57
  break;
61
58
  case 'frozenColsStart':
62
- cells = this.getSelectionColumnCells(range);
59
+ cells = this.getFrozenColsStartCells(range);
63
60
  break;
64
61
  case 'frozenColsEnd':
65
62
  cells = this.getActionColumnCells(range);
66
63
  break;
67
64
  case 'fixedStartStart':
68
- cells = this.getSelectAllCell();
65
+ cells = this.getFixedStartStartCells();
69
66
  break;
70
67
  case 'fixedStartEnd':
71
68
  cells = this.getNewColumnCell();
@@ -74,7 +71,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
74
71
  cells = this.getDraftRowCells(range);
75
72
  break;
76
73
  case 'fixedEndStart':
77
- cells = this.getDraftIconCells(range);
74
+ cells = this.getFixedEndStartCells(range);
78
75
  break;
79
76
  case 'fixedEndEnd':
80
77
  cells = this.getDraftActionCells(range);
@@ -110,6 +107,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
110
107
  field: View.FieldType,
111
108
  colIndex: number,
112
109
  displayIndex: number,
110
+ plane: DxGridPlane = 'grid',
113
111
  ): void {
114
112
  const { props } = this.getFieldProjection(field.id);
115
113
 
@@ -210,7 +208,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
210
208
  const targetObj = SchemaEx.getValue(obj, field.path)?.target;
211
209
  if (targetObj) {
212
210
  const uri = Obj.getURI(targetObj);
213
- cell.accessoryHtml = `<div role="none" class="absolute end-0 inset-y-0 p-(--dx-grid-cell-content-padding-block)"><dx-anchor uri=${uri} class="dx-button w-6 aspect-square min-h-0" data-dx-grid-action="accessory"><dx-icon icon="ph--link-simple--regular"/></dx-anchor></div>`;
211
+ cell.accessoryHtml = `<div role="none" class="dx-grid__cell__block"><dx-anchor uri=${uri} class="dx-button w-6 aspect-square min-h-0" data-dx-grid-action="accessory"><dx-icon icon="ph--link-simple--regular"/></dx-anchor></div>`;
214
212
  }
215
213
  }
216
214
 
@@ -219,7 +217,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
219
217
  const value = SchemaEx.getValue(obj, field.path);
220
218
  const href = typeof value === 'string' ? safeHttpUrl(value) : undefined;
221
219
  if (href) {
222
- cell.accessoryHtml = `<div role="none" class="absolute end-0 inset-y-0 p-(--dx-grid-cell-content-padding-block)"><a href="${escapeHtmlAttribute(href)}" target="_blank" rel="noopener noreferrer" class="dx-button w-6 aspect-square min-h-0" data-dx-grid-action="accessory"><dx-icon icon="ph--arrow-square-out--regular"/></a></div>`;
220
+ cell.accessoryHtml = `<div role="none" class="dx-grid__cell__block"><a href="${escapeHtmlAttribute(href)}" target="_blank" rel="noopener noreferrer" class="dx-button w-6 aspect-square min-h-0" data-dx-grid-action="accessory"><dx-icon icon="ph--arrow-square-out--regular"/></a></div>`;
223
221
  }
224
222
  }
225
223
 
@@ -229,6 +227,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
229
227
  cell.accessoryHtml = tableControls.switch.render({
230
228
  colIndex,
231
229
  rowIndex: displayIndex,
230
+ plane,
232
231
  checked: value ?? false,
233
232
  });
234
233
  cell.readonly = 'no-text-select';
@@ -275,15 +274,18 @@ export class TablePresentation<T extends TableRow = TableRow> {
275
274
  private getMainGridCells(range: DxGridPlaneRange): DxGridPlaneCells {
276
275
  const cells: DxGridPlaneCells = {};
277
276
  const fields = this.model.projection?.getFields() ?? [];
277
+ // Pinned fields live in the frozen-column planes; the scrolling grid holds the remainder.
278
+ const pin = this.model.pinColumns;
279
+ const scrollingFields = fields.length - pin;
278
280
 
279
281
  for (let row = range.start.row; row <= range.end.row && row < this.model.getRowCount(); row++) {
280
- for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
281
- const field = fields[col];
282
+ for (let col = range.start.col; col <= range.end.col && col < scrollingFields; col++) {
283
+ const field = fields[pin + col];
282
284
  if (!field) {
283
285
  continue;
284
286
  }
285
287
 
286
- this.createDataCell(cells, this.model.getRows()[row], field, col, row);
288
+ this.createDataCell(cells, this.model.getRows()[row], field, col, row, 'grid');
287
289
  }
288
290
  }
289
291
 
@@ -295,9 +297,13 @@ export class TablePresentation<T extends TableRow = TableRow> {
295
297
  const fields = this.model.projection?.getFields() ?? [];
296
298
  const draftRows = this.model.getDraftRows();
297
299
 
300
+ // Pinned fields' draft cells render in the fixedEndStart corner; the scrolling draft row holds the remainder.
301
+ const pin = this.model.pinColumns;
302
+ const scrollingFields = fields.length - pin;
303
+
298
304
  // Return cells of the CTA row if no draft row is active
299
305
  if (draftRows.length === 0) {
300
- for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
306
+ for (let col = range.start.col; col <= range.end.col && col < scrollingFields; col++) {
301
307
  cells[toPlaneCellIndex({ col, row: 0 })] = {
302
308
  value: '',
303
309
  readonly: true,
@@ -307,23 +313,13 @@ export class TablePresentation<T extends TableRow = TableRow> {
307
313
  } else {
308
314
  for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
309
315
  const draftRow = draftRows[row];
310
- for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
311
- const cellIndex = toPlaneCellIndex({ col, row });
312
- const field = fields[col];
316
+ for (let col = range.start.col; col <= range.end.col && col < scrollingFields; col++) {
317
+ const field = fields[pin + col];
313
318
  if (!field) {
314
319
  continue;
315
320
  }
316
321
 
317
- this.createDataCell(cells, draftRow.data, field, col, row);
318
-
319
- if (this.model.hasDraftRowValidationError(row, field.path)) {
320
- const cellValue = cells[cellIndex];
321
- if (cellValue) {
322
- const existingClasses = cellValue.className || '';
323
- const draftClasses = 'dx-grid__cell--flagged';
324
- cellValue.className = existingClasses ? `${existingClasses} ${draftClasses}` : draftClasses;
325
- }
326
- }
322
+ this.createDraftDataCell(cells, draftRow.data, field, col, row, 'frozenRowsEnd');
327
323
  }
328
324
  }
329
325
  }
@@ -331,41 +327,96 @@ export class TablePresentation<T extends TableRow = TableRow> {
331
327
  return cells;
332
328
  }
333
329
 
330
+ /**
331
+ * Renders a draft-row data cell and applies the validation-error flag when the field is invalid.
332
+ */
333
+ private createDraftDataCell(
334
+ cells: DxGridPlaneCells,
335
+ obj: T,
336
+ field: View.FieldType,
337
+ planeCol: number,
338
+ rowIndex: number,
339
+ plane: DxGridPlane,
340
+ ): void {
341
+ const cellIndex = toPlaneCellIndex({ col: planeCol, row: rowIndex });
342
+ this.createDataCell(cells, obj, field, planeCol, rowIndex, plane);
343
+
344
+ if (this.model.hasDraftRowValidationError(rowIndex, field.path)) {
345
+ const cellValue = cells[cellIndex];
346
+ if (cellValue) {
347
+ const existingClasses = cellValue.className || '';
348
+ const draftClasses = 'dx-grid__cell--flagged';
349
+ cellValue.className = existingClasses ? `${existingClasses} ${draftClasses}` : draftClasses;
350
+ }
351
+ }
352
+ }
353
+
334
354
  private getHeaderCells(range: DxGridPlaneRange): DxGridPlaneCells {
335
355
  const cells: DxGridPlaneCells = {};
336
356
  const fields = this.model.projection?.getFields() ?? [];
337
- const currentSort = this.model.getSorting();
338
- for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
339
- const { field, props } = this.model.projection.getFieldProjection(fields[col].id);
340
- const direction = currentSort?.fieldId === field.id ? currentSort.direction : undefined;
341
-
342
- cells[toPlaneCellIndex({ col, row: 0 })] = {
343
- // TODO(burdon): Use same logic as form for fallback title.
344
- value: '',
345
- resizeHandle: 'col',
346
- accessoryHtml: `
347
- <span class="grow min-w-0 truncate">${props.title ?? field.path}</span>
348
- ${direction !== undefined ? tableButtons.sort.render({ fieldId: field.id, direction }) : ''}
349
- ${tableButtons.columnSettings.render({ fieldId: field.id })}
350
- `,
351
- className: 'bg-axis-surface! text-axis-text! [&>div]:flex [&>div]:items-stretch',
352
- };
357
+ // Pinned fields' headers render in the fixedStartStart corner; the scrolling header holds the remainder.
358
+ const pin = this.model.pinColumns;
359
+ const scrollingFields = fields.length - pin;
360
+ for (let col = range.start.col; col <= range.end.col && col < scrollingFields; col++) {
361
+ this.createHeaderCell(cells, fields[pin + col].id, col);
353
362
  }
354
363
 
355
364
  return cells;
356
365
  }
357
366
 
358
- private getSelectionColumnCells(range: DxGridPlaneRange): DxGridPlaneCells {
367
+ private createHeaderCell(cells: DxGridPlaneCells, fieldId: string, planeCol: number): void {
368
+ const currentSort = this.model.getSorting();
369
+ const { field, props } = this.model.projection.getFieldProjection(fieldId);
370
+ const direction = currentSort?.fieldId === field.id ? currentSort.direction : undefined;
371
+
372
+ cells[toPlaneCellIndex({ col: planeCol, row: 0 })] = {
373
+ // TODO(burdon): Use same logic as form for fallback title.
374
+ value: '',
375
+ resizeHandle: 'col',
376
+ accessoryHtml: `
377
+ <span class="grow min-w-0 truncate">${props.title ?? field.path}</span>
378
+ ${direction !== undefined ? tableButtons.sort.render({ fieldId: field.id, direction }) : ''}
379
+ ${tableButtons.columnSettings.render({ fieldId: field.id })}
380
+ `,
381
+ className: 'bg-axis-surface! text-axis-text! [&>div]:flex [&>div]:items-stretch',
382
+ };
383
+ }
384
+
385
+ /**
386
+ * Renders the frozenColsStart plane: the selection column (if enabled) followed by the pinned data columns.
387
+ */
388
+ private getFrozenColsStartCells(range: DxGridPlaneRange): DxGridPlaneCells {
359
389
  const cells: DxGridPlaneCells = {};
390
+ const selectionColumns = this.model.selectionColumns;
391
+ const fields = this.model.projection?.getFields() ?? [];
392
+ const pin = this.model.pinColumns;
393
+
360
394
  for (let row = range.start.row; row <= range.end.row && row < this.model.getRowCount(); row++) {
361
- const isSelected = this.model.selection.isRowIndexSelected(row);
362
- const classes = cellClassesForRowSelection(isSelected, this.model.selection.selectionMode);
363
- cells[toPlaneCellIndex({ col: 0, row })] = {
364
- value: '',
365
- readonly: 'no-text-select',
366
- className: classes ? mx(classes) : undefined,
367
- accessoryHtml: tableControls.checkbox.render({ rowIndex: row, checked: isSelected }),
368
- };
395
+ if (selectionColumns > 0) {
396
+ const isSelected = this.model.selection.isRowIndexSelected(row);
397
+ const classes = cellClassesForRowSelection(isSelected, this.model.selection.selectionMode);
398
+ cells[toPlaneCellIndex({ col: 0, row })] = {
399
+ value: '',
400
+ readonly: 'no-text-select',
401
+ className: classes ? mx(classes) : undefined,
402
+ accessoryHtml: tableControls.checkbox.render({ rowIndex: row, checked: isSelected }),
403
+ };
404
+ }
405
+
406
+ for (let dataCol = 0; dataCol < pin; dataCol++) {
407
+ const field = fields[dataCol];
408
+ if (!field) {
409
+ continue;
410
+ }
411
+ this.createDataCell(
412
+ cells,
413
+ this.model.getRows()[row],
414
+ field,
415
+ selectionColumns + dataCol,
416
+ row,
417
+ 'frozenColsStart',
418
+ );
419
+ }
369
420
  }
370
421
 
371
422
  return cells;
@@ -387,29 +438,41 @@ export class TablePresentation<T extends TableRow = TableRow> {
387
438
  return cells;
388
439
  }
389
440
 
390
- private getSelectAllCell(): DxGridPlaneCells {
391
- if (!this.model.features.selection.enabled || this.model.selection.selectionMode === 'single') {
392
- return {
393
- [toPlaneCellIndex({ col: 0, row: 0 })]: {
394
- className: 'bg-axis-surface!',
395
- readonly: true,
396
- value: '',
397
- },
398
- };
399
- }
441
+ /**
442
+ * Renders the fixedStartStart corner (frozen header row x frozen columns): the select-all cell (if the
443
+ * selection column is enabled) followed by the pinned columns' header cells.
444
+ */
445
+ private getFixedStartStartCells(): DxGridPlaneCells {
446
+ const cells: DxGridPlaneCells = {};
447
+ const selectionColumns = this.model.selectionColumns;
400
448
 
401
- return {
402
- [toPlaneCellIndex({ col: 0, row: 0 })]: {
403
- accessoryHtml: tableControls.checkbox.render({
404
- rowIndex: 0,
405
- header: true,
406
- checked: this.model.selection.allRowsSelected,
407
- }),
449
+ if (selectionColumns > 0) {
450
+ const showSelectAll = this.model.features.selection.enabled && this.model.selection.selectionMode !== 'single';
451
+ cells[toPlaneCellIndex({ col: 0, row: 0 })] = {
452
+ accessoryHtml: showSelectAll
453
+ ? tableControls.checkbox.render({
454
+ rowIndex: 0,
455
+ header: true,
456
+ checked: this.model.selection.allRowsSelected,
457
+ })
458
+ : undefined,
408
459
  className: 'bg-axis-surface!',
409
460
  readonly: true,
410
461
  value: '',
411
- },
412
- };
462
+ };
463
+ }
464
+
465
+ const fields = this.model.projection?.getFields() ?? [];
466
+ const pin = this.model.pinColumns;
467
+ for (let dataCol = 0; dataCol < pin; dataCol++) {
468
+ const field = fields[dataCol];
469
+ if (!field) {
470
+ continue;
471
+ }
472
+ this.createHeaderCell(cells, field.id, selectionColumns + dataCol);
473
+ }
474
+
475
+ return cells;
413
476
  }
414
477
 
415
478
  private getNewColumnCell(): DxGridPlaneCells {
@@ -454,17 +517,44 @@ export class TablePresentation<T extends TableRow = TableRow> {
454
517
  return cells;
455
518
  }
456
519
 
457
- private getDraftIconCells(range: DxGridPlaneRange): DxGridPlaneCells {
520
+ /**
521
+ * Renders the fixedEndStart corner (draft row x frozen columns): the draft "+" icon (if the selection
522
+ * column is enabled) followed by the pinned columns' draft cells.
523
+ */
524
+ private getFixedEndStartCells(range: DxGridPlaneRange): DxGridPlaneCells {
458
525
  const cells: DxGridPlaneCells = {};
459
526
  const draftRows = this.model.getDraftRows();
527
+ const selectionColumns = this.model.selectionColumns;
528
+ const fields = this.model.projection?.getFields() ?? [];
529
+ const pin = this.model.pinColumns;
460
530
 
461
531
  for (let row = range.start.row; row <= range.end.row; row++) {
462
- cells[toPlaneCellIndex({ col: 0, row })] = {
463
- value: '',
464
- readonly: true,
465
- accessoryHtml: '<dx-icon icon="ph--plus--regular" class="contents"></dx-icon>',
466
- className: mx('[&>div]:grid [&>div]:place-content-center', draftRows.length < 1 && 'dx-grid__row--cta__cell'),
467
- };
532
+ if (selectionColumns > 0) {
533
+ cells[toPlaneCellIndex({ col: 0, row })] = {
534
+ value: '',
535
+ readonly: true,
536
+ accessoryHtml:
537
+ '<div role="none" class="dx-grid__cell__block"><dx-icon icon="ph--plus--regular" class="contents"></dx-icon></div>',
538
+ className: mx(draftRows.length < 1 && 'dx-grid__row--cta__cell'),
539
+ };
540
+ }
541
+
542
+ for (let dataCol = 0; dataCol < pin; dataCol++) {
543
+ const field = fields[dataCol];
544
+ if (!field) {
545
+ continue;
546
+ }
547
+ const planeCol = selectionColumns + dataCol;
548
+ if (draftRows.length === 0) {
549
+ cells[toPlaneCellIndex({ col: planeCol, row })] = {
550
+ value: '',
551
+ readonly: true,
552
+ className: 'dx-grid__row--cta__cell',
553
+ };
554
+ } else if (row < draftRows.length) {
555
+ this.createDraftDataCell(cells, draftRows[row].data, field, planeCol, row, 'fixedEndStart');
556
+ }
557
+ }
468
558
  }
469
559
 
470
560
  return cells;
@@ -5,7 +5,7 @@
5
5
  import * as Schema from 'effect/Schema';
6
6
 
7
7
  import { QueryAST } from '@dxos/echo';
8
- import { defineViewState } from '@dxos/react-ui-attention';
8
+ import { ViewState } from '@dxos/react-ui-attention';
9
9
 
10
10
  /**
11
11
  * Field sort configuration.
@@ -22,7 +22,7 @@ export type FieldSortType = Schema.Schema.Type<typeof FieldSortSchema>;
22
22
  * viewer's chosen sort survives remounts and reloads without writing to the shared view query
23
23
  * (which `TableModel.saveView` does explicitly).
24
24
  */
25
- export const tableSortAspect = defineViewState<FieldSortType | undefined>({
25
+ export const tableSortAspect = ViewState.define<FieldSortType | undefined>({
26
26
  key: 'table-sort',
27
27
  backend: 'local',
28
28
  schema: Schema.UndefinedOr(FieldSortSchema),
@@ -6,8 +6,8 @@ import { RegistryContext } from '@effect-atom/atom-react';
6
6
  import { type RefObject, useCallback, useContext, useMemo, useRef } from 'react';
7
7
 
8
8
  import { type Database, Filter, Type } from '@dxos/echo';
9
+ import { useQuery, useType } from '@dxos/echo-react';
9
10
  import { random } from '@dxos/random';
10
- import { useQuery, useType } from '@dxos/react-client/echo';
11
11
  import { useClientStory } from '@dxos/react-client/testing';
12
12
  import { useGlobalFilteredObjects } from '@dxos/react-ui-search';
13
13
  import { type ProjectionModel, getTypeURIFromQuery } from '@dxos/schema';
@@ -2,4 +2,8 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ // A UI-free entrypoint: the Table schema with no React attached, so operation handlers and node
6
+ // plugin variants can use it without pulling the table components.
7
+ // TODO(wittjosiah): Factor this out into a package that does not carry a UI dependency at all.
8
+
5
9
  export * as Table from './Table';
@@ -2,7 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Registry } from '@effect-atom/atom-react';
5
+ import { Registry } from '@effect-atom/atom';
6
6
  import { describe, expect, test } from 'vitest';
7
7
 
8
8
  import { Format } from '@dxos/echo';
@@ -2,7 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { type Registry } from '@effect-atom/atom-react';
5
+ import { type Registry } from '@effect-atom/atom';
6
6
  import type * as Types from 'effect/Types';
7
7
 
8
8
  import { Filter, type JsonSchema, Obj, Order, Query, type QueryAST, Ref, Type, type View } from '@dxos/echo';
@@ -39,7 +39,7 @@ const createButton = ({
39
39
  .map(([k, v]) => `${k}="${v}"`)
40
40
  .join(' ');
41
41
 
42
- return `<button ${attr} data-testid="${testId}" class="dx-button w-6 aspect-square px-0.5 min-h-0" ${dataAttrs} ${disabled ? 'disabled' : ''} data-dx-grid-action="accessory"><svg data-size="4"><use href="/icons.svg#${icon}"/></svg></button>`;
42
+ return `<div role="none" class="dx-grid__cell__block"><button ${attr} data-testid="${testId}" class="dx-button w-6 aspect-square px-0.5 min-h-0" ${dataAttrs} ${disabled ? 'disabled' : ''} data-dx-grid-action="accessory"><svg data-size="4"><use href="/icons.svg#${icon}"/></svg></button></div>`;
43
43
  };
44
44
 
45
45
  const addColumnButton = {
@@ -2,14 +2,13 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- // Core types.
6
- import { accessoryHandlesPointerdownAttrs } from '@dxos/lit-grid';
5
+ import { type DxGridPlane, accessoryHandlesPointerdownAttrs } from '@dxos/lit-grid';
7
6
 
8
7
  export type TableControl = 'checkbox' | 'switch';
9
8
 
10
9
  export type ControlData =
11
10
  | { type: 'checkbox'; rowIndex: number; header?: boolean }
12
- | { type: 'switch'; colIndex: number; rowIndex: number };
11
+ | { type: 'switch'; colIndex: number; rowIndex: number; plane: DxGridPlane };
13
12
 
14
13
  type CommonRenderProps = {
15
14
  checked?: boolean;
@@ -25,8 +24,8 @@ export const CONTROL_IDENTIFIERS = {
25
24
  } as const;
26
25
 
27
26
  const BASE_CLASSES = {
28
- checkbox: 'absolute inset-y-[.375rem] end-[.375rem] dx-checkbox',
29
- switch: 'absolute inset-y-[.25rem] end-[.25rem] dx-checkbox--switch',
27
+ checkbox: 'dx-checkbox',
28
+ switch: 'dx-checkbox--switch',
30
29
  } as const;
31
30
 
32
31
  const renderAttributes = (data: Record<string, string>) => {
@@ -43,10 +42,10 @@ const renderInput = (
43
42
  preventToggle = false,
44
43
  testId: string,
45
44
  ) => {
46
- return `<input type="checkbox" class="${baseClass}" ${renderAttributes(attrs)} ${checked ? 'checked' : ''} ${preventToggle ? 'onclick="return false"' : ''} ${disabled ? 'disabled' : ''} data-testid="${testId}" data-dx-grid-action="accessory"/>`;
45
+ return `<div role="none" class="dx-grid__cell__block"><input type="checkbox" class="${baseClass}" ${renderAttributes(attrs)} ${checked ? 'checked' : ''} ${preventToggle ? 'onclick="return false"' : ''} ${disabled ? 'disabled' : ''} data-testid="${testId}" data-dx-grid-action="accessory"/></div>`;
47
46
  };
48
47
 
49
- export const CheckboxStory = ({
48
+ export const CheckboxComponent = ({
50
49
  rowIndex,
51
50
  header = false,
52
51
  checked = false,
@@ -61,11 +60,18 @@ export const CheckboxStory = ({
61
60
  return renderInput(BASE_CLASSES.checkbox, attrs, checked, disabled, true, 'table-selection');
62
61
  };
63
62
 
64
- export const SwitchStory = ({ colIndex, rowIndex, checked = false, disabled = false }: RenderSwitchProps): string => {
63
+ export const SwitchComponent = ({
64
+ colIndex,
65
+ rowIndex,
66
+ plane,
67
+ checked = false,
68
+ disabled = false,
69
+ }: RenderSwitchProps): string => {
65
70
  const attrs = {
66
71
  [CONTROL_IDENTIFIERS.switch]: '',
67
72
  'data-row-index': rowIndex.toString(),
68
73
  'data-col-index': colIndex.toString(),
74
+ 'data-plane': plane,
69
75
  ...accessoryHandlesPointerdownAttrs,
70
76
  };
71
77
 
@@ -75,7 +81,7 @@ export const SwitchStory = ({ colIndex, rowIndex, checked = false, disabled = fa
75
81
  export const tableControls = {
76
82
  checkbox: {
77
83
  attr: CONTROL_IDENTIFIERS.checkbox,
78
- render: CheckboxStory,
84
+ render: CheckboxComponent,
79
85
  getData: (el: HTMLElement): Extract<ControlData, { type: 'checkbox' }> => ({
80
86
  type: 'checkbox',
81
87
  rowIndex: Number(el.getAttribute('data-row-index')),
@@ -84,11 +90,13 @@ export const tableControls = {
84
90
  },
85
91
  switch: {
86
92
  attr: CONTROL_IDENTIFIERS.switch,
87
- render: SwitchStory,
93
+ render: SwitchComponent,
88
94
  getData: (el: HTMLElement): Extract<ControlData, { type: 'switch' }> => ({
89
95
  type: 'switch',
90
96
  rowIndex: Number(el.getAttribute('data-row-index')),
91
97
  colIndex: Number(el.getAttribute('data-col-index')),
98
+ // The plane round-trips through a DOM attribute string written by the switch renderer.
99
+ plane: (el.getAttribute('data-plane') as DxGridPlane | null) ?? 'grid',
92
100
  }),
93
101
  },
94
102
  } as const;
@@ -1,10 +0,0 @@
1
- var __defProp = Object.defineProperty;
2
- var __export = (target, all) => {
3
- for (var name in all)
4
- __defProp(target, name, { get: all[name], enumerable: true });
5
- };
6
-
7
- export {
8
- __export
9
- };
10
- //# sourceMappingURL=chunk-J5LGTIGS.mjs.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": [],
4
- "sourcesContent": [],
5
- "mappings": "",
6
- "names": []
7
- }