@dxos/react-ui-canvas 0.8.4-staging.ac66bdf99f → 0.9.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 (77) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/browser/index.mjs +815 -19
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/node-esm/index.mjs +815 -19
  6. package/dist/lib/node-esm/index.mjs.map +4 -4
  7. package/dist/lib/node-esm/meta.json +1 -1
  8. package/dist/types/src/components/Canvas/Canvas.d.ts +1 -1
  9. package/dist/types/src/components/Canvas/Canvas.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/CellGrid/CellGrid.d.ts +21 -0
  11. package/dist/types/src/components/CellGrid/CellGrid.d.ts.map +1 -0
  12. package/dist/types/src/components/CellGrid/CellGrid.stories.d.ts +21 -0
  13. package/dist/types/src/components/CellGrid/CellGrid.stories.d.ts.map +1 -0
  14. package/dist/types/src/components/CellGrid/headers/Ruler.d.ts +15 -0
  15. package/dist/types/src/components/CellGrid/headers/Ruler.d.ts.map +1 -0
  16. package/dist/types/src/components/CellGrid/headers/TrackHeader.d.ts +19 -0
  17. package/dist/types/src/components/CellGrid/headers/TrackHeader.d.ts.map +1 -0
  18. package/dist/types/src/components/CellGrid/headers/index.d.ts +3 -0
  19. package/dist/types/src/components/CellGrid/headers/index.d.ts.map +1 -0
  20. package/dist/types/src/components/CellGrid/index.d.ts +6 -0
  21. package/dist/types/src/components/CellGrid/index.d.ts.map +1 -0
  22. package/dist/types/src/components/CellGrid/input/index.d.ts +3 -0
  23. package/dist/types/src/components/CellGrid/input/index.d.ts.map +1 -0
  24. package/dist/types/src/components/CellGrid/input/pointer.d.ts +33 -0
  25. package/dist/types/src/components/CellGrid/input/pointer.d.ts.map +1 -0
  26. package/dist/types/src/components/CellGrid/input/wheel.d.ts +14 -0
  27. package/dist/types/src/components/CellGrid/input/wheel.d.ts.map +1 -0
  28. package/dist/types/src/components/CellGrid/render/index.d.ts +3 -0
  29. package/dist/types/src/components/CellGrid/render/index.d.ts.map +1 -0
  30. package/dist/types/src/components/CellGrid/render/overlay-layer.d.ts +21 -0
  31. package/dist/types/src/components/CellGrid/render/overlay-layer.d.ts.map +1 -0
  32. package/dist/types/src/components/CellGrid/render/static-layer.d.ts +36 -0
  33. package/dist/types/src/components/CellGrid/render/static-layer.d.ts.map +1 -0
  34. package/dist/types/src/components/CellGrid/state/atoms.d.ts +23 -0
  35. package/dist/types/src/components/CellGrid/state/atoms.d.ts.map +1 -0
  36. package/dist/types/src/components/CellGrid/state/index.d.ts +4 -0
  37. package/dist/types/src/components/CellGrid/state/index.d.ts.map +1 -0
  38. package/dist/types/src/components/CellGrid/state/types.d.ts +39 -0
  39. package/dist/types/src/components/CellGrid/state/types.d.ts.map +1 -0
  40. package/dist/types/src/components/CellGrid/state/viewport.d.ts +52 -0
  41. package/dist/types/src/components/CellGrid/state/viewport.d.ts.map +1 -0
  42. package/dist/types/src/components/CellGrid/state/viewport.test.d.ts +2 -0
  43. package/dist/types/src/components/CellGrid/state/viewport.test.d.ts.map +1 -0
  44. package/dist/types/src/components/FPS.d.ts.map +1 -1
  45. package/dist/types/src/components/Grid/Grid.d.ts.map +1 -1
  46. package/dist/types/src/components/Grid/Grid.stories.d.ts.map +1 -1
  47. package/dist/types/src/components/index.d.ts +1 -0
  48. package/dist/types/src/components/index.d.ts.map +1 -1
  49. package/dist/types/src/hooks/projection.d.ts.map +1 -1
  50. package/dist/types/src/hooks/useDrag.d.ts.map +1 -1
  51. package/dist/types/src/hooks/useWheel.d.ts.map +1 -1
  52. package/dist/types/src/util/svg.d.ts +1 -1
  53. package/dist/types/src/util/svg.d.ts.map +1 -1
  54. package/dist/types/src/util/svg.stories.d.ts.map +1 -1
  55. package/dist/types/src/util/util.d.ts.map +1 -1
  56. package/dist/types/tsconfig.tsbuildinfo +1 -1
  57. package/package.json +16 -18
  58. package/src/components/Canvas/Canvas.tsx +1 -1
  59. package/src/components/CellGrid/CellGrid.stories.tsx +238 -0
  60. package/src/components/CellGrid/CellGrid.tsx +270 -0
  61. package/src/components/CellGrid/headers/Ruler.tsx +71 -0
  62. package/src/components/CellGrid/headers/TrackHeader.tsx +58 -0
  63. package/src/components/CellGrid/headers/index.ts +6 -0
  64. package/src/components/CellGrid/index.ts +9 -0
  65. package/src/components/CellGrid/input/index.ts +6 -0
  66. package/src/components/CellGrid/input/pointer.ts +236 -0
  67. package/src/components/CellGrid/input/wheel.ts +68 -0
  68. package/src/components/CellGrid/render/index.ts +6 -0
  69. package/src/components/CellGrid/render/overlay-layer.ts +66 -0
  70. package/src/components/CellGrid/render/static-layer.ts +112 -0
  71. package/src/components/CellGrid/state/atoms.ts +43 -0
  72. package/src/components/CellGrid/state/index.ts +7 -0
  73. package/src/components/CellGrid/state/types.ts +40 -0
  74. package/src/components/CellGrid/state/viewport.test.ts +50 -0
  75. package/src/components/CellGrid/state/viewport.ts +94 -0
  76. package/src/components/Grid/Grid.stories.tsx +1 -1
  77. package/src/components/index.ts +1 -0
@@ -0,0 +1,50 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { describe, test } from 'vitest';
6
+
7
+ import { defaultViewport } from './atoms';
8
+ import { hitTestCell, screenToWorld, visibleCellRange, visibleCells, worldToScreen } from './viewport';
9
+
10
+ const headers = { left: 80, top: 24 };
11
+
12
+ describe('viewport', () => {
13
+ test('worldToScreen / screenToWorld round-trip', ({ expect }) => {
14
+ const viewport = { ...defaultViewport(), scrollX: 100, scrollY: 50 };
15
+ const { x, y } = worldToScreen(viewport, headers, { col: 5, row: 3 });
16
+ const back = screenToWorld(viewport, headers, { x, y });
17
+ expect(back.col).toBeCloseTo(5);
18
+ expect(back.row).toBeCloseTo(3);
19
+ });
20
+
21
+ test('hitTestCell returns null in header region', ({ expect }) => {
22
+ const viewport = defaultViewport();
23
+ expect(hitTestCell(viewport, headers, { x: 10, y: 10 })).toBeNull();
24
+ expect(hitTestCell(viewport, headers, { x: 200, y: 10 })).toBeNull();
25
+ expect(hitTestCell(viewport, headers, { x: 10, y: 200 })).toBeNull();
26
+ });
27
+
28
+ test('hitTestCell floors fractional coords', ({ expect }) => {
29
+ const viewport = { ...defaultViewport(), baseCellWidth: 20, cellHeight: 20 };
30
+ const coord = hitTestCell(viewport, headers, { x: 80 + 25, y: 24 + 45 });
31
+ expect(coord).toEqual({ col: 1, row: 2 });
32
+ });
33
+
34
+ test('visibleCellRange respects scroll', ({ expect }) => {
35
+ const viewport = { ...defaultViewport(), scrollX: 240, baseCellWidth: 24, cellHeight: 24 };
36
+ const range = visibleCellRange(viewport, headers, { width: 400, height: 200 });
37
+ expect(range.minCol).toBe(10);
38
+ });
39
+
40
+ test('visibleCells filters by row and column extent', ({ expect }) => {
41
+ const cells = new Map([
42
+ ['0,0', { col: 0, row: 0, length: 1 }],
43
+ ['100,0', { col: 100, row: 0, length: 1 }],
44
+ ['5,5', { col: 5, row: 5, length: 1 }],
45
+ ['8,1', { col: 8, row: 1, length: 10 }],
46
+ ]);
47
+ const result = Array.from(visibleCells(cells, { minCol: 0, maxCol: 12, minRow: 0, maxRow: 2 }));
48
+ expect(result.map((cell) => `${cell.col},${cell.row}`).sort()).toEqual(['0,0', '8,1']);
49
+ });
50
+ });
@@ -0,0 +1,94 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import type { Cell, CellCoord, Headers, Viewport } from './types';
6
+
7
+ export const cellKey = (col: number, row: number): string => `${col},${row}`;
8
+
9
+ export const cellWidth = (viewport: Viewport): number => viewport.baseCellWidth * viewport.zoomX;
10
+
11
+ /**
12
+ * Convert a cell's world coordinates to screen-space pixel rectangle (relative to canvas origin).
13
+ */
14
+ export const worldToScreen = (
15
+ viewport: Viewport,
16
+ headers: Headers,
17
+ coord: { col: number; row: number; length?: number },
18
+ ): { x: number; y: number; w: number; h: number } => {
19
+ const w = cellWidth(viewport);
20
+ return {
21
+ x: headers.left + coord.col * w - viewport.scrollX,
22
+ y: headers.top + coord.row * viewport.cellHeight - viewport.scrollY,
23
+ w: (coord.length ?? 1) * w,
24
+ h: viewport.cellHeight,
25
+ };
26
+ };
27
+
28
+ /**
29
+ * Convert screen-space pixels (relative to canvas origin) to fractional cell coordinates.
30
+ */
31
+ export const screenToWorld = (
32
+ viewport: Viewport,
33
+ headers: Headers,
34
+ point: { x: number; y: number },
35
+ ): { col: number; row: number } => {
36
+ const w = cellWidth(viewport);
37
+ return {
38
+ col: (point.x - headers.left + viewport.scrollX) / w,
39
+ row: (point.y - headers.top + viewport.scrollY) / viewport.cellHeight,
40
+ };
41
+ };
42
+
43
+ export const hitTestCell = (
44
+ viewport: Viewport,
45
+ headers: Headers,
46
+ point: { x: number; y: number },
47
+ ): CellCoord | null => {
48
+ if (point.x < headers.left || point.y < headers.top) {
49
+ return null;
50
+ }
51
+ const { col, row } = screenToWorld(viewport, headers, point);
52
+ if (col < 0 || row < 0) {
53
+ return null;
54
+ }
55
+ return { col: Math.floor(col), row: Math.floor(row) };
56
+ };
57
+
58
+ /**
59
+ * Compute the inclusive range of cell coordinates intersecting the visible content rect.
60
+ */
61
+ export const visibleCellRange = (
62
+ viewport: Viewport,
63
+ headers: Headers,
64
+ size: { width: number; height: number },
65
+ ): { minCol: number; maxCol: number; minRow: number; maxRow: number } => {
66
+ const w = cellWidth(viewport);
67
+ const innerW = Math.max(0, size.width - headers.left);
68
+ const innerH = Math.max(0, size.height - headers.top);
69
+ const minCol = Math.max(0, Math.floor(viewport.scrollX / w));
70
+ const maxCol = Math.floor((viewport.scrollX + innerW) / w);
71
+ const minRow = Math.max(0, Math.floor(viewport.scrollY / viewport.cellHeight));
72
+ const maxRow = Math.floor((viewport.scrollY + innerH) / viewport.cellHeight);
73
+ return { minCol, maxCol, minRow, maxRow };
74
+ };
75
+
76
+ /**
77
+ * Iterate sparse cell map, yielding only cells whose horizontal extent intersects visible cols and whose row is visible.
78
+ */
79
+ export const visibleCells = function* <T>(
80
+ cells: ReadonlyMap<string, Cell<T>>,
81
+ range: { minCol: number; maxCol: number; minRow: number; maxRow: number },
82
+ ): Generator<Cell<T>> {
83
+ for (const cell of cells.values()) {
84
+ if (cell.row < range.minRow || cell.row > range.maxRow) {
85
+ continue;
86
+ }
87
+ const start = cell.col;
88
+ const end = cell.col + cell.length - 1;
89
+ if (end < range.minCol || start > range.maxCol) {
90
+ continue;
91
+ }
92
+ yield cell;
93
+ }
94
+ };
@@ -15,7 +15,7 @@ const DefaultStory = (props: GridProps) => {
15
15
  const [{ scale, offset }] = useState<ProjectionState>({ scale: 1, offset: { x: 0, y: 0 } });
16
16
 
17
17
  return (
18
- <div role='none' ref={ref} className='grow'>
18
+ <div ref={ref} className='grow'>
19
19
  <GridComponent scale={scale} offset={offset} {...props} />
20
20
  </div>
21
21
  );
@@ -3,5 +3,6 @@
3
3
  //
4
4
 
5
5
  export * from './Canvas';
6
+ export * from './CellGrid';
6
7
  export * from './FPS';
7
8
  export * from './Grid';