@dxos/react-ui-canvas 0.8.4-main.fd6878d → 0.8.4-staging.60fe92afc8

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 (95) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/browser/index.mjs +1157 -363
  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 +1157 -363
  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 +2 -2
  9. package/dist/types/src/components/Canvas/Canvas.stories.d.ts +12 -4
  10. package/dist/types/src/components/Canvas/Canvas.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/CellGrid/CellGrid.d.ts +21 -0
  12. package/dist/types/src/components/CellGrid/CellGrid.d.ts.map +1 -0
  13. package/dist/types/src/components/CellGrid/CellGrid.stories.d.ts +21 -0
  14. package/dist/types/src/components/CellGrid/CellGrid.stories.d.ts.map +1 -0
  15. package/dist/types/src/components/CellGrid/headers/Ruler.d.ts +15 -0
  16. package/dist/types/src/components/CellGrid/headers/Ruler.d.ts.map +1 -0
  17. package/dist/types/src/components/CellGrid/headers/TrackHeader.d.ts +19 -0
  18. package/dist/types/src/components/CellGrid/headers/TrackHeader.d.ts.map +1 -0
  19. package/dist/types/src/components/CellGrid/headers/index.d.ts +3 -0
  20. package/dist/types/src/components/CellGrid/headers/index.d.ts.map +1 -0
  21. package/dist/types/src/components/CellGrid/index.d.ts +6 -0
  22. package/dist/types/src/components/CellGrid/index.d.ts.map +1 -0
  23. package/dist/types/src/components/CellGrid/input/index.d.ts +3 -0
  24. package/dist/types/src/components/CellGrid/input/index.d.ts.map +1 -0
  25. package/dist/types/src/components/CellGrid/input/pointer.d.ts +33 -0
  26. package/dist/types/src/components/CellGrid/input/pointer.d.ts.map +1 -0
  27. package/dist/types/src/components/CellGrid/input/wheel.d.ts +14 -0
  28. package/dist/types/src/components/CellGrid/input/wheel.d.ts.map +1 -0
  29. package/dist/types/src/components/CellGrid/render/index.d.ts +3 -0
  30. package/dist/types/src/components/CellGrid/render/index.d.ts.map +1 -0
  31. package/dist/types/src/components/CellGrid/render/overlay-layer.d.ts +21 -0
  32. package/dist/types/src/components/CellGrid/render/overlay-layer.d.ts.map +1 -0
  33. package/dist/types/src/components/CellGrid/render/static-layer.d.ts +36 -0
  34. package/dist/types/src/components/CellGrid/render/static-layer.d.ts.map +1 -0
  35. package/dist/types/src/components/CellGrid/state/atoms.d.ts +23 -0
  36. package/dist/types/src/components/CellGrid/state/atoms.d.ts.map +1 -0
  37. package/dist/types/src/components/CellGrid/state/index.d.ts +4 -0
  38. package/dist/types/src/components/CellGrid/state/index.d.ts.map +1 -0
  39. package/dist/types/src/components/CellGrid/state/types.d.ts +39 -0
  40. package/dist/types/src/components/CellGrid/state/types.d.ts.map +1 -0
  41. package/dist/types/src/components/CellGrid/state/viewport.d.ts +52 -0
  42. package/dist/types/src/components/CellGrid/state/viewport.d.ts.map +1 -0
  43. package/dist/types/src/components/CellGrid/state/viewport.test.d.ts +2 -0
  44. package/dist/types/src/components/CellGrid/state/viewport.test.d.ts.map +1 -0
  45. package/dist/types/src/components/FPS.d.ts.map +1 -1
  46. package/dist/types/src/components/Grid/Grid.d.ts +2 -2
  47. package/dist/types/src/components/Grid/Grid.d.ts.map +1 -1
  48. package/dist/types/src/components/Grid/Grid.stories.d.ts +19 -4
  49. package/dist/types/src/components/Grid/Grid.stories.d.ts.map +1 -1
  50. package/dist/types/src/components/index.d.ts +1 -0
  51. package/dist/types/src/components/index.d.ts.map +1 -1
  52. package/dist/types/src/hooks/index.d.ts +1 -0
  53. package/dist/types/src/hooks/index.d.ts.map +1 -1
  54. package/dist/types/src/hooks/projection.d.ts.map +1 -1
  55. package/dist/types/src/hooks/useDrag.d.ts +6 -0
  56. package/dist/types/src/hooks/useDrag.d.ts.map +1 -0
  57. package/dist/types/src/hooks/useWheel.d.ts.map +1 -1
  58. package/dist/types/src/types.d.ts +1 -1
  59. package/dist/types/src/types.d.ts.map +1 -1
  60. package/dist/types/src/util/svg.d.ts +1 -1
  61. package/dist/types/src/util/svg.d.ts.map +1 -1
  62. package/dist/types/src/util/svg.stories.d.ts +12 -4
  63. package/dist/types/src/util/svg.stories.d.ts.map +1 -1
  64. package/dist/types/src/util/util.d.ts.map +1 -1
  65. package/dist/types/tsconfig.tsbuildinfo +1 -1
  66. package/package.json +26 -26
  67. package/src/components/Canvas/Canvas.stories.tsx +12 -11
  68. package/src/components/Canvas/Canvas.tsx +4 -4
  69. package/src/components/CellGrid/CellGrid.stories.tsx +238 -0
  70. package/src/components/CellGrid/CellGrid.tsx +270 -0
  71. package/src/components/CellGrid/headers/Ruler.tsx +71 -0
  72. package/src/components/CellGrid/headers/TrackHeader.tsx +58 -0
  73. package/src/components/CellGrid/headers/index.ts +6 -0
  74. package/src/components/CellGrid/index.ts +9 -0
  75. package/src/components/CellGrid/input/index.ts +6 -0
  76. package/src/components/CellGrid/input/pointer.ts +236 -0
  77. package/src/components/CellGrid/input/wheel.ts +68 -0
  78. package/src/components/CellGrid/render/index.ts +6 -0
  79. package/src/components/CellGrid/render/overlay-layer.ts +66 -0
  80. package/src/components/CellGrid/render/static-layer.ts +112 -0
  81. package/src/components/CellGrid/state/atoms.ts +43 -0
  82. package/src/components/CellGrid/state/index.ts +7 -0
  83. package/src/components/CellGrid/state/types.ts +40 -0
  84. package/src/components/CellGrid/state/viewport.test.ts +50 -0
  85. package/src/components/CellGrid/state/viewport.ts +94 -0
  86. package/src/components/FPS.tsx +3 -3
  87. package/src/components/Grid/Grid.stories.tsx +9 -9
  88. package/src/components/Grid/Grid.tsx +13 -15
  89. package/src/components/index.ts +1 -0
  90. package/src/hooks/index.ts +1 -0
  91. package/src/hooks/useDrag.tsx +96 -0
  92. package/src/hooks/useWheel.tsx +0 -28
  93. package/src/types.ts +1 -1
  94. package/src/util/svg.stories.tsx +9 -9
  95. package/src/util/svg.tsx +1 -1
@@ -1,10 +1,9 @@
1
1
  import { createRequire } from 'node:module';const require = createRequire(import.meta.url);
2
2
 
3
3
  // src/components/Canvas/Canvas.tsx
4
- import { useSignals as _useSignals2 } from "@preact-signals/safe-react/tracking";
5
- import React2, { forwardRef, useEffect as useEffect2, useImperativeHandle, useMemo, useState } from "react";
4
+ import React2, { forwardRef, useEffect as useEffect3, useImperativeHandle, useMemo, useState } from "react";
6
5
  import { useResizeDetector } from "react-resize-detector";
7
- import { mx as mx2 } from "@dxos/react-ui-theme";
6
+ import { mx as mx2 } from "@dxos/ui-theme";
8
7
 
9
8
  // src/hooks/projection.tsx
10
9
  import { easeSinOut, interpolate, interpolateObject, transition } from "d3";
@@ -106,14 +105,81 @@ var useCanvasContext = () => {
106
105
  return useContext(CanvasContext) ?? raise(new Error("Missing CanvasContext"));
107
106
  };
108
107
 
108
+ // src/hooks/useDrag.tsx
109
+ import { bind } from "bind-event-listener";
110
+ import { useEffect, useRef } from "react";
111
+ var useDrag = (_options = {}) => {
112
+ const { root, setProjection } = useCanvasContext();
113
+ const state = useRef({
114
+ panning: false,
115
+ x: 0,
116
+ y: 0
117
+ });
118
+ useEffect(() => {
119
+ if (!root) {
120
+ return;
121
+ }
122
+ return bind(root, {
123
+ type: "pointerdown",
124
+ listener: (ev) => {
125
+ if (ev.button !== 0) {
126
+ return;
127
+ }
128
+ if (ev.defaultPrevented) {
129
+ return;
130
+ }
131
+ if (ev.target !== root || ev.shiftKey) {
132
+ return;
133
+ }
134
+ ev.preventDefault();
135
+ root.setPointerCapture(ev.pointerId);
136
+ state.current = {
137
+ panning: true,
138
+ x: ev.clientX,
139
+ y: ev.clientY
140
+ };
141
+ const moveUnbind = bind(root, {
142
+ type: "pointermove",
143
+ listener: (ev2) => {
144
+ if (!state.current.panning) {
145
+ return;
146
+ }
147
+ const dx = ev2.clientX - state.current.x;
148
+ const dy = ev2.clientY - state.current.y;
149
+ state.current.x = ev2.clientX;
150
+ state.current.y = ev2.clientY;
151
+ setProjection((prev) => ({
152
+ ...prev,
153
+ offset: {
154
+ x: prev.offset.x + dx,
155
+ y: prev.offset.y + dy
156
+ }
157
+ }));
158
+ }
159
+ });
160
+ const upUnbind = bind(root, {
161
+ type: "pointerup",
162
+ listener: (ev2) => {
163
+ state.current.panning = false;
164
+ root.releasePointerCapture(ev2.pointerId);
165
+ moveUnbind();
166
+ upUnbind();
167
+ }
168
+ });
169
+ }
170
+ });
171
+ }, [
172
+ root
173
+ ]);
174
+ };
175
+
109
176
  // src/hooks/useWheel.tsx
110
177
  import { bindAll } from "bind-event-listener";
111
- import { useEffect } from "react";
178
+ import { useEffect as useEffect2 } from "react";
112
179
 
113
180
  // src/util/svg.tsx
114
- import { useSignals as _useSignals } from "@preact-signals/safe-react/tracking";
115
181
  import React from "react";
116
- import { mx } from "@dxos/react-ui-theme";
182
+ import { mx } from "@dxos/ui-theme";
117
183
  var createPath = (points, join = false) => {
118
184
  return [
119
185
  "M",
@@ -122,144 +188,118 @@ var createPath = (points, join = false) => {
122
188
  ].join(" ");
123
189
  };
124
190
  var Markers = ({ id = "dx-marker", classNames }) => {
125
- var _effect = _useSignals();
126
- try {
127
- return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Arrow, {
128
- id: `${id}-arrow-start`,
129
- dir: "start",
130
- classNames
131
- }), /* @__PURE__ */ React.createElement(Arrow, {
132
- id: `${id}-arrow-end`,
133
- dir: "end",
134
- classNames
135
- }), /* @__PURE__ */ React.createElement(Arrow, {
136
- id: `${id}-triangle-start`,
137
- dir: "start",
138
- closed: true,
139
- classNames
140
- }), /* @__PURE__ */ React.createElement(Arrow, {
141
- id: `${id}-triangle-end`,
142
- dir: "end",
143
- closed: true,
144
- classNames
145
- }), /* @__PURE__ */ React.createElement(Marker, {
146
- id: `${id}-circle`,
147
- pos: {
148
- x: 8,
149
- y: 8
150
- },
151
- size: {
152
- width: 16,
153
- height: 16
154
- }
155
- }, /* @__PURE__ */ React.createElement("circle", {
156
- cx: 8,
157
- cy: 8,
158
- r: 5,
159
- stroke: "context-stroke",
160
- className: mx(classNames)
161
- })));
162
- } finally {
163
- _effect.f();
164
- }
165
- };
166
- var Marker = ({ id, className, children, pos: { x: refX, y: refY }, size: { width: markerWidth, height: markerHeight }, fill, ...rest }) => {
167
- var _effect = _useSignals();
168
- try {
169
- return /* @__PURE__ */ React.createElement("marker", {
170
- id,
171
- className,
172
- refX,
173
- refY,
174
- markerWidth,
175
- markerHeight,
176
- markerUnits: "strokeWidth",
177
- orient: "auto",
178
- ...rest
179
- }, children);
180
- } finally {
181
- _effect.f();
182
- }
183
- };
184
- var Arrow = ({ classNames, id, size = 16, dir = "end", closed = false }) => {
185
- var _effect = _useSignals();
186
- try {
187
- return /* @__PURE__ */ React.createElement(Marker, {
188
- id,
189
- size: {
190
- width: size,
191
- height: size
192
- },
193
- pos: dir === "end" ? {
194
- x: size,
195
- y: size / 2
196
- } : {
197
- x: 0,
198
- y: size / 2
199
- }
200
- }, /* @__PURE__ */ React.createElement("path", {
201
- fill: closed ? void 0 : "none",
202
- stroke: "context-stroke",
203
- className: mx(classNames),
204
- d: createPath(dir === "end" ? [
205
- {
206
- x: 1,
207
- y: 1
208
- },
209
- {
210
- x: size,
211
- y: size / 2
212
- },
213
- {
214
- x: 1,
215
- y: size - 1
216
- }
217
- ] : [
218
- {
219
- x: size - 1,
220
- y: 1
221
- },
222
- {
223
- x: 0,
224
- y: size / 2
225
- },
226
- {
227
- x: size - 1,
228
- y: size - 1
229
- }
230
- ], closed)
231
- }));
232
- } finally {
233
- _effect.f();
234
- }
191
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Arrow, {
192
+ id: `${id}-arrow-start`,
193
+ dir: "start",
194
+ classNames
195
+ }), /* @__PURE__ */ React.createElement(Arrow, {
196
+ id: `${id}-arrow-end`,
197
+ dir: "end",
198
+ classNames
199
+ }), /* @__PURE__ */ React.createElement(Arrow, {
200
+ id: `${id}-triangle-start`,
201
+ dir: "start",
202
+ closed: true,
203
+ classNames
204
+ }), /* @__PURE__ */ React.createElement(Arrow, {
205
+ id: `${id}-triangle-end`,
206
+ dir: "end",
207
+ closed: true,
208
+ classNames
209
+ }), /* @__PURE__ */ React.createElement(Marker, {
210
+ id: `${id}-circle`,
211
+ pos: {
212
+ x: 8,
213
+ y: 8
214
+ },
215
+ size: {
216
+ width: 16,
217
+ height: 16
218
+ }
219
+ }, /* @__PURE__ */ React.createElement("circle", {
220
+ cx: 8,
221
+ cy: 8,
222
+ r: 5,
223
+ stroke: "context-stroke",
224
+ className: mx(classNames)
225
+ })));
235
226
  };
236
- var GridPattern = ({ classNames, id, size, offset }) => {
237
- var _effect = _useSignals();
238
- try {
239
- return /* @__PURE__ */ React.createElement("pattern", {
240
- id,
241
- x: (size / 2 + offset.x) % size,
242
- y: (size / 2 + offset.y) % size,
243
- width: size,
244
- height: size,
245
- patternUnits: "userSpaceOnUse"
246
- }, /* @__PURE__ */ React.createElement("g", {
247
- className: mx(classNames)
248
- }, /* @__PURE__ */ React.createElement("line", {
249
- x1: 0,
250
- y1: size / 2,
251
- x2: size,
252
- y2: size / 2
253
- }), /* @__PURE__ */ React.createElement("line", {
254
- x1: size / 2,
255
- y1: 0,
256
- x2: size / 2,
257
- y2: size
258
- })));
259
- } finally {
260
- _effect.f();
227
+ var Marker = ({ id, className, children, pos: { x: refX, y: refY }, size: { width: markerWidth, height: markerHeight }, fill, ...rest }) => /* @__PURE__ */ React.createElement("marker", {
228
+ id,
229
+ className,
230
+ refX,
231
+ refY,
232
+ markerWidth,
233
+ markerHeight,
234
+ markerUnits: "strokeWidth",
235
+ orient: "auto",
236
+ ...rest
237
+ }, children);
238
+ var Arrow = ({ classNames, id, size = 16, dir = "end", closed = false }) => /* @__PURE__ */ React.createElement(Marker, {
239
+ id,
240
+ size: {
241
+ width: size,
242
+ height: size
243
+ },
244
+ pos: dir === "end" ? {
245
+ x: size,
246
+ y: size / 2
247
+ } : {
248
+ x: 0,
249
+ y: size / 2
261
250
  }
262
- };
251
+ }, /* @__PURE__ */ React.createElement("path", {
252
+ fill: closed ? void 0 : "none",
253
+ stroke: "context-stroke",
254
+ className: mx(classNames),
255
+ d: createPath(dir === "end" ? [
256
+ {
257
+ x: 1,
258
+ y: 1
259
+ },
260
+ {
261
+ x: size,
262
+ y: size / 2
263
+ },
264
+ {
265
+ x: 1,
266
+ y: size - 1
267
+ }
268
+ ] : [
269
+ {
270
+ x: size - 1,
271
+ y: 1
272
+ },
273
+ {
274
+ x: 0,
275
+ y: size / 2
276
+ },
277
+ {
278
+ x: size - 1,
279
+ y: size - 1
280
+ }
281
+ ], closed)
282
+ }));
283
+ var GridPattern = ({ classNames, id, size, offset }) => /* @__PURE__ */ React.createElement("pattern", {
284
+ id,
285
+ x: (size / 2 + offset.x) % size,
286
+ y: (size / 2 + offset.y) % size,
287
+ width: size,
288
+ height: size,
289
+ patternUnits: "userSpaceOnUse"
290
+ }, /* @__PURE__ */ React.createElement("g", {
291
+ className: mx(classNames)
292
+ }, /* @__PURE__ */ React.createElement("line", {
293
+ x1: 0,
294
+ y1: size / 2,
295
+ x2: size,
296
+ y2: size / 2
297
+ }), /* @__PURE__ */ React.createElement("line", {
298
+ x1: size / 2,
299
+ y1: 0,
300
+ x2: size / 2,
301
+ y2: size
302
+ })));
263
303
 
264
304
  // src/util/util.ts
265
305
  var logged = false;
@@ -302,7 +342,7 @@ var defaultOptions = {
302
342
  };
303
343
  var useWheel = (options = defaultOptions) => {
304
344
  const { root, setProjection } = useCanvasContext();
305
- useEffect(() => {
345
+ useEffect2(() => {
306
346
  if (!root) {
307
347
  return;
308
348
  }
@@ -315,9 +355,6 @@ var useWheel = (options = defaultOptions) => {
315
355
  },
316
356
  listener: (ev) => {
317
357
  const zooming = isWheelZooming(ev);
318
- if (!hasFocus(root) && !zooming) {
319
- return;
320
- }
321
358
  ev.preventDefault();
322
359
  if (zooming && !options.zoom) {
323
360
  return;
@@ -361,191 +398,940 @@ var isWheelZooming = (ev) => {
361
398
  }
362
399
  return false;
363
400
  };
364
- var hasFocus = (element) => {
365
- const activeElement = document.activeElement;
366
- if (!activeElement) {
367
- return false;
401
+
402
+ // src/components/Canvas/Canvas.tsx
403
+ var Canvas = /* @__PURE__ */ forwardRef(({ children, classNames, scale: scaleProp = 1, offset: offsetProp = defaultOrigin, ...props }, forwardedRef) => {
404
+ const { ref, width = 0, height = 0 } = useResizeDetector();
405
+ const [ready, setReady] = useState(false);
406
+ const [{ scale, offset }, setProjection] = useState({
407
+ scale: scaleProp,
408
+ offset: offsetProp
409
+ });
410
+ useEffect3(() => {
411
+ if (width && height && offset === defaultOrigin) {
412
+ setProjection({
413
+ scale,
414
+ offset: {
415
+ x: width / 2,
416
+ y: height / 2
417
+ }
418
+ });
419
+ }
420
+ }, [
421
+ width,
422
+ height,
423
+ scale,
424
+ offset
425
+ ]);
426
+ const projection = useMemo(() => new ProjectionMapper(), []);
427
+ useEffect3(() => {
428
+ projection.update({
429
+ width,
430
+ height
431
+ }, scale, offset);
432
+ if (offset !== defaultOrigin) {
433
+ setReady(true);
434
+ }
435
+ }, [
436
+ projection,
437
+ scale,
438
+ offset,
439
+ width,
440
+ height
441
+ ]);
442
+ const styles = useMemo(() => {
443
+ return {
444
+ // NOTE: Order is important.
445
+ transform: `translate(${offset.x}px, ${offset.y}px) scale(${scale})`,
446
+ visibility: width && height ? "visible" : "hidden"
447
+ };
448
+ }, [
449
+ scale,
450
+ offset
451
+ ]);
452
+ useImperativeHandle(forwardedRef, () => {
453
+ return {
454
+ setProjection: async (projection2) => {
455
+ setProjection(projection2);
456
+ }
457
+ };
458
+ }, [
459
+ ref
460
+ ]);
461
+ return /* @__PURE__ */ React2.createElement(CanvasContext.Provider, {
462
+ value: {
463
+ root: ref.current,
464
+ ready,
465
+ width,
466
+ height,
467
+ scale,
468
+ offset,
469
+ styles,
470
+ projection,
471
+ setProjection
472
+ }
473
+ }, /* @__PURE__ */ React2.createElement("div", {
474
+ ...props,
475
+ className: mx2("absolute inset-0 overflow-hidden", classNames),
476
+ ref
477
+ }, ready ? children : null));
478
+ });
479
+
480
+ // src/components/CellGrid/CellGrid.tsx
481
+ import { RegistryContext } from "@effect-atom/atom-react";
482
+ import React5, { useContext as useContext2, useEffect as useEffect4, useMemo as useMemo3, useRef as useRef2, useState as useState2 } from "react";
483
+ import { useResizeDetector as useResizeDetector2 } from "react-resize-detector";
484
+ import { mx as mx5 } from "@dxos/ui-theme";
485
+
486
+ // src/components/CellGrid/headers/Ruler.tsx
487
+ import React3, { useMemo as useMemo2 } from "react";
488
+ import { mx as mx3 } from "@dxos/ui-theme";
489
+
490
+ // src/components/CellGrid/state/viewport.ts
491
+ var cellKey = (col, row) => `${col},${row}`;
492
+ var cellWidth = (viewport) => viewport.baseCellWidth * viewport.zoomX;
493
+ var worldToScreen = (viewport, headers, coord) => {
494
+ const w = cellWidth(viewport);
495
+ return {
496
+ x: headers.left + coord.col * w - viewport.scrollX,
497
+ y: headers.top + coord.row * viewport.cellHeight - viewport.scrollY,
498
+ w: (coord.length ?? 1) * w,
499
+ h: viewport.cellHeight
500
+ };
501
+ };
502
+ var screenToWorld = (viewport, headers, point) => {
503
+ const w = cellWidth(viewport);
504
+ return {
505
+ col: (point.x - headers.left + viewport.scrollX) / w,
506
+ row: (point.y - headers.top + viewport.scrollY) / viewport.cellHeight
507
+ };
508
+ };
509
+ var hitTestCell = (viewport, headers, point) => {
510
+ if (point.x < headers.left || point.y < headers.top) {
511
+ return null;
368
512
  }
369
- let shadowActive = activeElement;
370
- while (shadowActive?.shadowRoot?.activeElement) {
371
- shadowActive = shadowActive.shadowRoot.activeElement;
513
+ const { col, row } = screenToWorld(viewport, headers, point);
514
+ if (col < 0 || row < 0) {
515
+ return null;
372
516
  }
373
- let current = element;
374
- while (current) {
375
- if (current === activeElement || current === shadowActive) {
376
- return true;
517
+ return {
518
+ col: Math.floor(col),
519
+ row: Math.floor(row)
520
+ };
521
+ };
522
+ var visibleCellRange = (viewport, headers, size) => {
523
+ const w = cellWidth(viewport);
524
+ const innerW = Math.max(0, size.width - headers.left);
525
+ const innerH = Math.max(0, size.height - headers.top);
526
+ const minCol = Math.max(0, Math.floor(viewport.scrollX / w));
527
+ const maxCol = Math.floor((viewport.scrollX + innerW) / w);
528
+ const minRow = Math.max(0, Math.floor(viewport.scrollY / viewport.cellHeight));
529
+ const maxRow = Math.floor((viewport.scrollY + innerH) / viewport.cellHeight);
530
+ return {
531
+ minCol,
532
+ maxCol,
533
+ minRow,
534
+ maxRow
535
+ };
536
+ };
537
+ var visibleCells = function* (cells, range) {
538
+ for (const cell of cells.values()) {
539
+ if (cell.row < range.minRow || cell.row > range.maxRow) {
540
+ continue;
377
541
  }
378
- current = current.parentElement;
542
+ const start = cell.col;
543
+ const end = cell.col + cell.length - 1;
544
+ if (end < range.minCol || start > range.maxCol) {
545
+ continue;
546
+ }
547
+ yield cell;
379
548
  }
380
- return false;
381
549
  };
382
550
 
383
- // src/components/Canvas/Canvas.tsx
384
- var Canvas = /* @__PURE__ */ forwardRef(({ children, classNames, scale: _scale = 1, offset: _offset = defaultOrigin, ...props }, forwardedRef) => {
385
- var _effect = _useSignals2();
386
- try {
387
- const { ref, width = 0, height = 0 } = useResizeDetector();
388
- const [ready, setReady] = useState(false);
389
- const [{ scale, offset }, setProjection] = useState({
390
- scale: _scale,
391
- offset: _offset
392
- });
393
- useEffect2(() => {
394
- if (width && height && offset === defaultOrigin) {
395
- setProjection({
396
- scale,
397
- offset: {
398
- x: width / 2,
399
- y: height / 2
551
+ // src/components/CellGrid/headers/Ruler.tsx
552
+ var Ruler = ({ viewport, headers, width, majorEvery = 4, classNames }) => {
553
+ const safeMajorEvery = Math.max(1, Math.floor(majorEvery));
554
+ const ticks = useMemo2(() => {
555
+ const w = cellWidth(viewport);
556
+ if (w < 1 || width <= headers.left) {
557
+ return [];
558
+ }
559
+ const innerWidth = width - headers.left;
560
+ const startCol = Math.floor(viewport.scrollX / w);
561
+ const endCol = Math.ceil((viewport.scrollX + innerWidth) / w);
562
+ const result = [];
563
+ for (let col = startCol; col <= endCol; col++) {
564
+ result.push({
565
+ col,
566
+ x: headers.left + col * w - viewport.scrollX,
567
+ major: col % safeMajorEvery === 0
568
+ });
569
+ }
570
+ return result;
571
+ }, [
572
+ viewport,
573
+ headers.left,
574
+ width,
575
+ safeMajorEvery
576
+ ]);
577
+ return /* @__PURE__ */ React3.createElement("div", {
578
+ className: mx3("absolute top-0 left-0 right-0 border-b border-neutral-200 dark:border-neutral-700 bg-baseSurface select-none overflow-hidden", classNames),
579
+ style: {
580
+ height: headers.top
581
+ }
582
+ }, ticks.map(({ col, x, major }) => /* @__PURE__ */ React3.createElement("div", {
583
+ key: col,
584
+ className: mx3("absolute top-0 bottom-0 text-[10px] text-neutral-500 dark:text-neutral-400", major ? "border-l border-neutral-400 dark:border-neutral-500" : "border-l border-neutral-200 dark:border-neutral-700"),
585
+ style: {
586
+ transform: `translateX(${x}px)`
587
+ }
588
+ }, major ? /* @__PURE__ */ React3.createElement("span", {
589
+ className: "absolute left-1 top-0"
590
+ }, col) : null)));
591
+ };
592
+
593
+ // src/components/CellGrid/headers/TrackHeader.tsx
594
+ import React4 from "react";
595
+ import { mx as mx4 } from "@dxos/ui-theme";
596
+ var TrackHeader = ({ viewport, headers, rows, height, classNames }) => {
597
+ return /* @__PURE__ */ React4.createElement("div", {
598
+ className: mx4("absolute left-0 border-r border-neutral-200 dark:border-neutral-700 select-none overflow-hidden", classNames),
599
+ style: {
600
+ top: headers.top,
601
+ width: headers.left,
602
+ height: Math.max(0, height - headers.top)
603
+ }
604
+ }, /* @__PURE__ */ React4.createElement("div", {
605
+ style: {
606
+ transform: `translateY(${-viewport.scrollY}px)`
607
+ }
608
+ }, rows.map((row, index) => /* @__PURE__ */ React4.createElement("div", {
609
+ key: row.id,
610
+ className: "flex items-center px-2 text-xs text-neutral-700 dark:text-neutral-300",
611
+ style: {
612
+ height: viewport.cellHeight,
613
+ // Match the canvas's row-band: a translucent gray overlay on odd rows,
614
+ // transparent on even rows. The container's overall background bleeds
615
+ // through, so the labels stay legible in both themes.
616
+ backgroundColor: index % 2 === 0 ? "transparent" : "rgba(128, 128, 128, 0.08)",
617
+ // Match the canvas gridline color (rgba(128, 128, 128, 0.25)). Use a
618
+ // half-pixel inset to keep crisp single-pixel rendering on retina.
619
+ boxShadow: "inset 0 -1px 0 rgba(128, 128, 128, 0.25)"
620
+ }
621
+ }, row.label ?? row.id))));
622
+ };
623
+
624
+ // src/components/CellGrid/input/pointer.ts
625
+ var attachPointerHandlers = (element, { registry, atoms, headers, handlers }) => {
626
+ let drag = null;
627
+ const local = (event) => {
628
+ const rect = element.getBoundingClientRect();
629
+ return {
630
+ x: event.clientX - rect.left,
631
+ y: event.clientY - rect.top
632
+ };
633
+ };
634
+ const tryCapture = (pointerId) => {
635
+ try {
636
+ element.setPointerCapture(pointerId);
637
+ } catch {
638
+ }
639
+ };
640
+ const onPointerDown = (event) => {
641
+ if (event.button === 1 || event.button === 0 && event.altKey) {
642
+ drag = {
643
+ kind: "pan",
644
+ lastX: event.clientX,
645
+ lastY: event.clientY
646
+ };
647
+ tryCapture(event.pointerId);
648
+ event.preventDefault();
649
+ return;
650
+ }
651
+ if (event.button !== 0) {
652
+ return;
653
+ }
654
+ const viewport = registry.get(atoms.viewport);
655
+ const point = local(event);
656
+ const coord = hitTestCell(viewport, headers, point);
657
+ if (!coord) {
658
+ return;
659
+ }
660
+ const tool = registry.get(atoms.tool);
661
+ tryCapture(event.pointerId);
662
+ switch (tool) {
663
+ case "toggle":
664
+ case "resize": {
665
+ const cells = registry.get(atoms.cells);
666
+ const key = cellKey(coord.col, coord.row);
667
+ const mode = cells.has(key) ? "unset" : "set";
668
+ handlers.onCellToggle?.(coord, mode);
669
+ drag = {
670
+ kind: "toggle",
671
+ mode,
672
+ touched: /* @__PURE__ */ new Set([
673
+ key
674
+ ])
675
+ };
676
+ break;
677
+ }
678
+ case "edit": {
679
+ drag = {
680
+ kind: "draw",
681
+ startCoord: coord,
682
+ endCoord: coord
683
+ };
684
+ handlers.onDrawUpdate?.(coord, coord);
685
+ break;
686
+ }
687
+ case "delete": {
688
+ const key = cellKey(coord.col, coord.row);
689
+ handlers.onCellToggle?.(coord, "unset");
690
+ drag = {
691
+ kind: "toggle",
692
+ mode: "unset",
693
+ touched: /* @__PURE__ */ new Set([
694
+ key
695
+ ])
696
+ };
697
+ break;
698
+ }
699
+ case "select": {
700
+ drag = {
701
+ kind: "select",
702
+ origin: coord
703
+ };
704
+ registry.set(atoms.selection, {
705
+ range: {
706
+ col0: coord.col,
707
+ row0: coord.row,
708
+ col1: coord.col,
709
+ row1: coord.row
400
710
  }
401
711
  });
712
+ break;
402
713
  }
403
- }, [
404
- width,
405
- height,
406
- scale,
407
- offset
408
- ]);
409
- const projection = useMemo(() => new ProjectionMapper(), []);
410
- useEffect2(() => {
411
- projection.update({
412
- width,
413
- height
414
- }, scale, offset);
415
- if (offset !== defaultOrigin) {
416
- setReady(true);
714
+ }
715
+ };
716
+ const onPointerMove = (event) => {
717
+ if (!drag) {
718
+ return;
719
+ }
720
+ if (drag.kind === "pan") {
721
+ const dx = event.clientX - drag.lastX;
722
+ const dy = event.clientY - drag.lastY;
723
+ drag.lastX = event.clientX;
724
+ drag.lastY = event.clientY;
725
+ registry.update(atoms.viewport, (current) => ({
726
+ ...current,
727
+ scrollX: Math.max(0, current.scrollX - dx),
728
+ scrollY: Math.max(0, current.scrollY - dy)
729
+ }));
730
+ return;
731
+ }
732
+ const viewport = registry.get(atoms.viewport);
733
+ const coord = hitTestCell(viewport, headers, local(event));
734
+ if (!coord) {
735
+ return;
736
+ }
737
+ if (drag.kind === "toggle") {
738
+ const key = cellKey(coord.col, coord.row);
739
+ if (!drag.touched.has(key)) {
740
+ drag.touched.add(key);
741
+ handlers.onCellToggle?.(coord, drag.mode);
417
742
  }
418
- }, [
419
- projection,
420
- scale,
421
- offset,
422
- width,
423
- height
424
- ]);
425
- const styles = useMemo(() => {
426
- return {
427
- // NOTE: Order is important.
428
- transform: `translate(${offset.x}px, ${offset.y}px) scale(${scale})`,
429
- visibility: width && height ? "visible" : "hidden"
430
- };
431
- }, [
432
- scale,
433
- offset
434
- ]);
435
- useImperativeHandle(forwardedRef, () => {
436
- return {
437
- setProjection: async (projection2) => {
438
- setProjection(projection2);
743
+ } else if (drag.kind === "draw") {
744
+ const constrainedCol = coord.col;
745
+ if (constrainedCol !== drag.endCoord.col) {
746
+ drag.endCoord = {
747
+ col: constrainedCol,
748
+ row: drag.startCoord.row
749
+ };
750
+ handlers.onDrawUpdate?.(drag.startCoord, drag.endCoord);
751
+ }
752
+ } else if (drag.kind === "select") {
753
+ registry.set(atoms.selection, {
754
+ range: {
755
+ col0: drag.origin.col,
756
+ row0: drag.origin.row,
757
+ col1: coord.col,
758
+ row1: coord.row
439
759
  }
440
- };
441
- }, [
442
- ref
443
- ]);
444
- return /* @__PURE__ */ React2.createElement(CanvasContext.Provider, {
445
- value: {
446
- root: ref.current,
447
- ready,
448
- width,
449
- height,
450
- scale,
451
- offset,
452
- styles,
453
- projection,
454
- setProjection
760
+ });
761
+ }
762
+ };
763
+ const releaseCapture = (event) => {
764
+ if (element.hasPointerCapture(event.pointerId)) {
765
+ element.releasePointerCapture(event.pointerId);
766
+ }
767
+ };
768
+ const onPointerUp = (event) => {
769
+ if (!drag) {
770
+ return;
771
+ }
772
+ if (drag.kind === "draw") {
773
+ handlers.onDrawCommit?.(drag.startCoord, drag.endCoord);
774
+ } else if (drag.kind === "select") {
775
+ const range = registry.get(atoms.selection).range;
776
+ if (range) {
777
+ handlers.onSelectionCommit?.(range);
455
778
  }
456
- }, /* @__PURE__ */ React2.createElement("div", {
457
- role: "none",
458
- ...props,
459
- className: mx2("absolute inset-0 overflow-hidden", classNames),
460
- ref
461
- }, ready ? children : null));
462
- } finally {
463
- _effect.f();
464
- }
465
- });
779
+ }
780
+ drag = null;
781
+ releaseCapture(event);
782
+ };
783
+ const onPointerCancel = (event) => {
784
+ drag = null;
785
+ releaseCapture(event);
786
+ };
787
+ element.addEventListener("pointerdown", onPointerDown);
788
+ element.addEventListener("pointermove", onPointerMove);
789
+ element.addEventListener("pointerup", onPointerUp);
790
+ element.addEventListener("pointercancel", onPointerCancel);
791
+ return () => {
792
+ element.removeEventListener("pointerdown", onPointerDown);
793
+ element.removeEventListener("pointermove", onPointerMove);
794
+ element.removeEventListener("pointerup", onPointerUp);
795
+ element.removeEventListener("pointercancel", onPointerCancel);
796
+ };
797
+ };
798
+ var toggleCell = (registry, atoms, coord, factory, mode = "toggle") => {
799
+ registry.update(atoms.cells, (current) => {
800
+ const next = new Map(current);
801
+ const key = cellKey(coord.col, coord.row);
802
+ const exists = next.has(key);
803
+ if (mode === "set" || mode === "toggle" && !exists) {
804
+ next.set(key, factory(coord));
805
+ } else if (mode === "unset" || mode === "toggle" && exists) {
806
+ next.delete(key);
807
+ }
808
+ return next;
809
+ });
810
+ };
466
811
 
467
- // src/components/FPS.tsx
468
- import { useSignals as _useSignals3 } from "@preact-signals/safe-react/tracking";
469
- import React3, { useEffect as useEffect3, useReducer, useRef } from "react";
470
- import { mx as mx3 } from "@dxos/react-ui-theme";
471
- var SEC = 1e3;
472
- var FPS = ({ classNames, width = 60, height = 30, bar = "bg-cyan-500" }) => {
473
- var _effect = _useSignals3();
474
- try {
475
- const [{ fps, max, len }, dispatch] = useReducer((state) => {
476
- const currentTime = Date.now();
477
- if (currentTime > state.prevTime + SEC) {
478
- const nextFPS = [
479
- ...new Array(Math.floor((currentTime - state.prevTime - SEC) / SEC)).fill(0),
480
- Math.max(1, Math.round(state.frames * SEC / (currentTime - state.prevTime)))
481
- ];
812
+ // src/components/CellGrid/input/wheel.ts
813
+ var MIN_ZOOM = 0.25;
814
+ var MAX_ZOOM = 8;
815
+ var attachWheelHandlers = (element, { registry, atoms, headers }) => {
816
+ const onWheel = (event) => {
817
+ if (event.ctrlKey || event.metaKey) {
818
+ event.preventDefault();
819
+ const rect = element.getBoundingClientRect();
820
+ const x = event.clientX - rect.left;
821
+ const factor = Math.exp(-event.deltaY / 200);
822
+ registry.update(atoms.viewport, (current2) => {
823
+ const nextZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, current2.zoomX * factor));
824
+ if (nextZoom === current2.zoomX) {
825
+ return current2;
826
+ }
827
+ const w = cellWidth(current2);
828
+ const worldX = (x - headers.left + current2.scrollX) / w;
829
+ const nextW = current2.baseCellWidth * nextZoom;
830
+ const nextScrollX2 = Math.max(0, worldX * nextW - (x - headers.left));
482
831
  return {
483
- max: Math.max(state.max, ...nextFPS),
484
- len: Math.min(state.len + nextFPS.length, width),
485
- fps: [
486
- ...state.fps,
487
- ...nextFPS
488
- ].slice(-width),
489
- frames: 1,
490
- prevTime: currentTime
832
+ ...current2,
833
+ zoomX: nextZoom,
834
+ scrollX: nextScrollX2
491
835
  };
836
+ });
837
+ return;
838
+ }
839
+ const dx = event.shiftKey ? event.deltaY : event.deltaX;
840
+ const dy = event.shiftKey ? 0 : event.deltaY;
841
+ const current = registry.get(atoms.viewport);
842
+ const nextScrollX = Math.max(0, current.scrollX + dx);
843
+ const nextScrollY = Math.max(0, current.scrollY + dy);
844
+ if (nextScrollX === current.scrollX && nextScrollY === current.scrollY) {
845
+ return;
846
+ }
847
+ event.preventDefault();
848
+ registry.set(atoms.viewport, {
849
+ ...current,
850
+ scrollX: nextScrollX,
851
+ scrollY: nextScrollY
852
+ });
853
+ };
854
+ element.addEventListener("wheel", onWheel, {
855
+ passive: false
856
+ });
857
+ return () => element.removeEventListener("wheel", onWheel);
858
+ };
859
+
860
+ // src/components/CellGrid/render/overlay-layer.ts
861
+ var drawOverlay = ({ ctx, size, viewport, headers, selection, playhead, style }) => {
862
+ ctx.clearRect(0, 0, size.width, size.height);
863
+ ctx.save();
864
+ ctx.beginPath();
865
+ ctx.rect(headers.left, headers.top, size.width - headers.left, size.height - headers.top);
866
+ ctx.clip();
867
+ if (selection.range) {
868
+ const { col0, row0, col1, row1 } = selection.range;
869
+ const minCol = Math.min(col0, col1);
870
+ const maxCol = Math.max(col0, col1);
871
+ const minRow = Math.min(row0, row1);
872
+ const maxRow = Math.max(row0, row1);
873
+ const tl = worldToScreen(viewport, headers, {
874
+ col: minCol,
875
+ row: minRow
876
+ });
877
+ const br = worldToScreen(viewport, headers, {
878
+ col: maxCol + 1,
879
+ row: maxRow + 1
880
+ });
881
+ ctx.fillStyle = style.selectionFill;
882
+ ctx.fillRect(tl.x, tl.y, br.x - tl.x, br.y - tl.y);
883
+ ctx.strokeStyle = style.selectionStroke;
884
+ ctx.setLineDash([
885
+ 4,
886
+ 3
887
+ ]);
888
+ ctx.lineWidth = 1;
889
+ ctx.strokeRect(tl.x + 0.5, tl.y + 0.5, br.x - tl.x - 1, br.y - tl.y - 1);
890
+ ctx.setLineDash([]);
891
+ }
892
+ if (playhead !== null) {
893
+ const w = cellWidth(viewport);
894
+ const x = headers.left + playhead * w - viewport.scrollX;
895
+ if (x >= headers.left && x <= size.width) {
896
+ ctx.strokeStyle = style.playhead;
897
+ ctx.lineWidth = 2;
898
+ ctx.beginPath();
899
+ ctx.moveTo(x, headers.top);
900
+ ctx.lineTo(x, size.height);
901
+ ctx.stroke();
902
+ }
903
+ }
904
+ ctx.restore();
905
+ };
906
+
907
+ // src/components/CellGrid/render/static-layer.ts
908
+ var drawCells = ({ ctx, size, viewport, headers, rows, cells, renderCell, style }) => {
909
+ ctx.clearRect(0, 0, size.width, size.height);
910
+ if (style.background) {
911
+ ctx.fillStyle = style.background;
912
+ ctx.fillRect(0, 0, size.width, size.height);
913
+ }
914
+ const range = visibleCellRange(viewport, headers, size);
915
+ const w = cellWidth(viewport);
916
+ const h = viewport.cellHeight;
917
+ if (style.rowBand) {
918
+ ctx.fillStyle = style.rowBand;
919
+ for (let row = range.minRow; row <= Math.min(range.maxRow, rows.length - 1); row++) {
920
+ if (row % 2 === 0) {
921
+ continue;
922
+ }
923
+ const y = headers.top + row * h - viewport.scrollY;
924
+ ctx.fillRect(headers.left, y, size.width - headers.left, h);
925
+ }
926
+ }
927
+ ctx.strokeStyle = style.gridLine;
928
+ ctx.lineWidth = 1;
929
+ ctx.beginPath();
930
+ for (let col = range.minCol; col <= range.maxCol + 1; col++) {
931
+ const x = Math.floor(headers.left + col * w - viewport.scrollX) + 0.5;
932
+ if (x < headers.left) {
933
+ continue;
934
+ }
935
+ ctx.moveTo(x, headers.top);
936
+ ctx.lineTo(x, size.height);
937
+ }
938
+ for (let row = range.minRow; row <= Math.min(range.maxRow + 1, rows.length); row++) {
939
+ const y = Math.floor(headers.top + row * h - viewport.scrollY) + 0.5;
940
+ if (y < headers.top) {
941
+ continue;
942
+ }
943
+ ctx.moveTo(headers.left, y);
944
+ ctx.lineTo(size.width, y);
945
+ }
946
+ ctx.stroke();
947
+ ctx.save();
948
+ ctx.beginPath();
949
+ ctx.rect(headers.left, headers.top, size.width - headers.left, size.height - headers.top);
950
+ ctx.clip();
951
+ for (const cell of visibleCells(cells, range)) {
952
+ if (cell.row >= rows.length) {
953
+ continue;
954
+ }
955
+ const rect = worldToScreen(viewport, headers, cell);
956
+ renderCell({
957
+ ctx,
958
+ ...rect,
959
+ cell
960
+ });
961
+ }
962
+ ctx.restore();
963
+ };
964
+
965
+ // src/components/CellGrid/CellGrid.tsx
966
+ var defaultHeaders = {
967
+ left: 80,
968
+ top: 24
969
+ };
970
+ var defaultStaticStyle = {
971
+ gridLine: "rgba(128,128,128,0.25)",
972
+ rowBand: "rgba(128,128,128,0.06)"
973
+ };
974
+ var defaultOverlayStyle = {
975
+ playhead: "rgb(220, 38, 38)",
976
+ selectionFill: "rgba(59, 130, 246, 0.15)",
977
+ selectionStroke: "rgb(59, 130, 246)"
978
+ };
979
+ var setupCanvas = (canvas, width, height) => {
980
+ const dpr = window.devicePixelRatio || 1;
981
+ canvas.width = Math.max(1, Math.floor(width * dpr));
982
+ canvas.height = Math.max(1, Math.floor(height * dpr));
983
+ canvas.style.width = `${width}px`;
984
+ canvas.style.height = `${height}px`;
985
+ const ctx = canvas.getContext("2d");
986
+ if (!ctx) {
987
+ return null;
988
+ }
989
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
990
+ return ctx;
991
+ };
992
+ var CellGrid = ({ atoms, rows, renderCell, headers: headersProp, staticStyle: staticStyleProp, overlayStyle: overlayStyleProp, classNames, onCellToggle, onSelectionCommit, onDrawUpdate, onDrawCommit }) => {
993
+ const registry = useContext2(RegistryContext);
994
+ const headers = useMemo3(() => {
995
+ if (headersProp === false) {
996
+ return {
997
+ left: 0,
998
+ top: 0
999
+ };
1000
+ }
1001
+ return {
1002
+ ...defaultHeaders,
1003
+ ...headersProp ?? {}
1004
+ };
1005
+ }, [
1006
+ headersProp
1007
+ ]);
1008
+ const staticStyle = useMemo3(() => ({
1009
+ ...defaultStaticStyle,
1010
+ ...staticStyleProp ?? {}
1011
+ }), [
1012
+ staticStyleProp
1013
+ ]);
1014
+ const overlayStyle = useMemo3(() => ({
1015
+ ...defaultOverlayStyle,
1016
+ ...overlayStyleProp ?? {}
1017
+ }), [
1018
+ overlayStyleProp
1019
+ ]);
1020
+ const { ref: containerRef, width = 0, height = 0 } = useResizeDetector2();
1021
+ const staticCanvasRef = useRef2(null);
1022
+ const overlayCanvasRef = useRef2(null);
1023
+ const overlayInputRef = useRef2(null);
1024
+ const [staticCtx, setStaticCtx] = useState2(null);
1025
+ const [overlayCtx, setOverlayCtx] = useState2(null);
1026
+ const [viewportState, setViewportState] = useState2(() => registry.get(atoms.viewport));
1027
+ useEffect4(() => {
1028
+ if (!width || !height) {
1029
+ return;
1030
+ }
1031
+ if (staticCanvasRef.current) {
1032
+ const ctx = setupCanvas(staticCanvasRef.current, width, height);
1033
+ setStaticCtx(ctx);
1034
+ }
1035
+ if (overlayCanvasRef.current) {
1036
+ const ctx = setupCanvas(overlayCanvasRef.current, width, height);
1037
+ setOverlayCtx(ctx);
1038
+ }
1039
+ }, [
1040
+ width,
1041
+ height
1042
+ ]);
1043
+ useEffect4(() => registry.subscribe(atoms.viewport, (next) => setViewportState(next)), [
1044
+ registry,
1045
+ atoms.viewport
1046
+ ]);
1047
+ useEffect4(() => {
1048
+ if (!staticCtx || !width || !height) {
1049
+ return;
1050
+ }
1051
+ let raf = null;
1052
+ const schedule = () => {
1053
+ if (raf !== null) {
1054
+ return;
1055
+ }
1056
+ raf = requestAnimationFrame(() => {
1057
+ raf = null;
1058
+ drawCells({
1059
+ ctx: staticCtx,
1060
+ size: {
1061
+ width,
1062
+ height
1063
+ },
1064
+ viewport: registry.get(atoms.viewport),
1065
+ headers,
1066
+ rows,
1067
+ cells: registry.get(atoms.cells),
1068
+ renderCell,
1069
+ style: staticStyle
1070
+ });
1071
+ });
1072
+ };
1073
+ schedule();
1074
+ const unsubCells = registry.subscribe(atoms.cells, schedule);
1075
+ const unsubViewport = registry.subscribe(atoms.viewport, schedule);
1076
+ return () => {
1077
+ if (raf !== null) {
1078
+ cancelAnimationFrame(raf);
1079
+ }
1080
+ unsubCells();
1081
+ unsubViewport();
1082
+ };
1083
+ }, [
1084
+ staticCtx,
1085
+ width,
1086
+ height,
1087
+ registry,
1088
+ atoms.cells,
1089
+ atoms.viewport,
1090
+ headers,
1091
+ rows,
1092
+ renderCell,
1093
+ staticStyle
1094
+ ]);
1095
+ useEffect4(() => {
1096
+ if (!overlayCtx || !width || !height) {
1097
+ return;
1098
+ }
1099
+ let raf = null;
1100
+ let stopped = false;
1101
+ const paint = () => {
1102
+ drawOverlay({
1103
+ ctx: overlayCtx,
1104
+ size: {
1105
+ width,
1106
+ height
1107
+ },
1108
+ viewport: registry.get(atoms.viewport),
1109
+ headers,
1110
+ selection: registry.get(atoms.selection),
1111
+ playhead: registry.get(atoms.playhead),
1112
+ style: overlayStyle
1113
+ });
1114
+ };
1115
+ const isAnimating = () => registry.get(atoms.playhead) !== null;
1116
+ const loop = () => {
1117
+ if (stopped) {
1118
+ return;
1119
+ }
1120
+ paint();
1121
+ if (isAnimating()) {
1122
+ raf = requestAnimationFrame(loop);
492
1123
  } else {
493
- return {
494
- ...state,
495
- frames: state.frames + 1
496
- };
1124
+ raf = null;
1125
+ }
1126
+ };
1127
+ const kick = () => {
1128
+ paint();
1129
+ if (raf === null && isAnimating()) {
1130
+ raf = requestAnimationFrame(loop);
1131
+ }
1132
+ };
1133
+ kick();
1134
+ const unsubSelection = registry.subscribe(atoms.selection, () => paint());
1135
+ const unsubPlayhead = registry.subscribe(atoms.playhead, kick);
1136
+ const unsubViewport = registry.subscribe(atoms.viewport, () => paint());
1137
+ return () => {
1138
+ stopped = true;
1139
+ if (raf !== null) {
1140
+ cancelAnimationFrame(raf);
1141
+ }
1142
+ unsubSelection();
1143
+ unsubPlayhead();
1144
+ unsubViewport();
1145
+ };
1146
+ }, [
1147
+ overlayCtx,
1148
+ width,
1149
+ height,
1150
+ registry,
1151
+ atoms.selection,
1152
+ atoms.playhead,
1153
+ atoms.viewport,
1154
+ headers,
1155
+ overlayStyle
1156
+ ]);
1157
+ const callbacksRef = useRef2({
1158
+ onCellToggle,
1159
+ onSelectionCommit,
1160
+ onDrawUpdate,
1161
+ onDrawCommit
1162
+ });
1163
+ callbacksRef.current = {
1164
+ onCellToggle,
1165
+ onSelectionCommit,
1166
+ onDrawUpdate,
1167
+ onDrawCommit
1168
+ };
1169
+ useEffect4(() => {
1170
+ const element = overlayInputRef.current;
1171
+ if (!element) {
1172
+ return;
1173
+ }
1174
+ const detachPointer = attachPointerHandlers(element, {
1175
+ registry,
1176
+ atoms,
1177
+ headers,
1178
+ handlers: {
1179
+ onCellToggle: (coord, mode) => callbacksRef.current.onCellToggle?.(coord, mode),
1180
+ onSelectionCommit: (range) => callbacksRef.current.onSelectionCommit?.(range),
1181
+ onDrawUpdate: (start, end) => callbacksRef.current.onDrawUpdate?.(start, end),
1182
+ onDrawCommit: (start, end) => callbacksRef.current.onDrawCommit?.(start, end)
497
1183
  }
498
- }, {
499
- max: 0,
500
- len: 0,
501
- fps: [],
502
- frames: 0,
503
- prevTime: Date.now()
504
1184
  });
505
- const requestRef = useRef();
506
- const tick = () => {
507
- dispatch();
508
- requestRef.current = requestAnimationFrame(tick);
1185
+ const detachWheel = attachWheelHandlers(element, {
1186
+ registry,
1187
+ atoms,
1188
+ headers
1189
+ });
1190
+ return () => {
1191
+ detachPointer();
1192
+ detachWheel();
509
1193
  };
510
- useEffect3(() => {
511
- requestRef.current = requestAnimationFrame(tick);
512
- return () => {
513
- if (requestRef.current) {
514
- cancelAnimationFrame(requestRef.current);
515
- }
1194
+ }, [
1195
+ registry,
1196
+ atoms,
1197
+ headers
1198
+ ]);
1199
+ return /* @__PURE__ */ React5.createElement("div", {
1200
+ ref: containerRef,
1201
+ className: mx5("relative w-full h-full overflow-hidden bg-baseSurface", classNames)
1202
+ }, /* @__PURE__ */ React5.createElement("canvas", {
1203
+ ref: staticCanvasRef,
1204
+ className: "absolute inset-0 pointer-events-none",
1205
+ style: {
1206
+ top: -1,
1207
+ left: -1
1208
+ }
1209
+ }), /* @__PURE__ */ React5.createElement("canvas", {
1210
+ ref: overlayCanvasRef,
1211
+ className: "absolute inset-0 pointer-events-none",
1212
+ style: {
1213
+ top: -1,
1214
+ left: -1
1215
+ }
1216
+ }), /* @__PURE__ */ React5.createElement("div", {
1217
+ ref: overlayInputRef,
1218
+ className: "absolute inset-0 touch-none",
1219
+ style: {
1220
+ paddingLeft: headers.left,
1221
+ paddingTop: headers.top
1222
+ }
1223
+ }), headers.top > 0 && /* @__PURE__ */ React5.createElement(Ruler, {
1224
+ viewport: viewportState,
1225
+ headers,
1226
+ width
1227
+ }), headers.left > 0 && /* @__PURE__ */ React5.createElement(TrackHeader, {
1228
+ viewport: viewportState,
1229
+ headers,
1230
+ rows,
1231
+ height
1232
+ }), headers.top > 0 && headers.left > 0 && /* @__PURE__ */ React5.createElement("div", {
1233
+ className: "absolute top-0 left-0 border-b border-r border-neutral-200 dark:border-neutral-700 bg-baseSurface",
1234
+ style: {
1235
+ width: headers.left,
1236
+ height: headers.top
1237
+ }
1238
+ }));
1239
+ };
1240
+
1241
+ // src/components/CellGrid/state/atoms.ts
1242
+ import { Atom } from "@effect-atom/atom-react";
1243
+ var defaultViewport = (options = {}) => ({
1244
+ scrollX: 0,
1245
+ scrollY: 0,
1246
+ baseCellWidth: options.cellWidth ?? 24,
1247
+ cellHeight: options.cellHeight ?? 24,
1248
+ zoomX: 1
1249
+ });
1250
+ var createCellGridAtoms = (options = {}) => ({
1251
+ cells: Atom.keepAlive(Atom.make(/* @__PURE__ */ new Map())),
1252
+ viewport: Atom.keepAlive(Atom.make(defaultViewport(options))),
1253
+ selection: Atom.keepAlive(Atom.make({
1254
+ range: null
1255
+ })),
1256
+ playhead: Atom.keepAlive(Atom.make(null)),
1257
+ tool: Atom.keepAlive(Atom.make("toggle"))
1258
+ });
1259
+
1260
+ // src/components/FPS.tsx
1261
+ import React6, { useEffect as useEffect5, useReducer, useRef as useRef3 } from "react";
1262
+ import { mx as mx6 } from "@dxos/ui-theme";
1263
+ var SEC = 1e3;
1264
+ var FPS = ({ classNames, width = 60, height = 30, bar = "bg-cyan-500" }) => {
1265
+ const [{ fps, max, len }, dispatch] = useReducer((state) => {
1266
+ const currentTime = Date.now();
1267
+ if (currentTime > state.prevTime + SEC) {
1268
+ const nextFPS = [
1269
+ ...new Array(Math.floor((currentTime - state.prevTime - SEC) / SEC)).fill(0),
1270
+ Math.max(1, Math.round(state.frames * SEC / (currentTime - state.prevTime)))
1271
+ ];
1272
+ return {
1273
+ max: Math.max(state.max, ...nextFPS),
1274
+ len: Math.min(state.len + nextFPS.length, width),
1275
+ fps: [
1276
+ ...state.fps,
1277
+ ...nextFPS
1278
+ ].slice(-width),
1279
+ frames: 1,
1280
+ prevTime: currentTime
516
1281
  };
517
- }, []);
518
- return /* @__PURE__ */ React3.createElement("div", {
519
- style: {
520
- width: width + 6
521
- },
522
- className: mx3("relative flex flex-col p-0.5", "bg-baseSurface text-xs text-subdued font-thin pointer-events-none border border-separator", classNames)
523
- }, /* @__PURE__ */ React3.createElement("div", null, fps[len - 1], " FPS"), /* @__PURE__ */ React3.createElement("div", {
524
- className: "w-full relative",
525
- style: {
526
- height
527
- }
528
- }, fps.map((frame, i) => /* @__PURE__ */ React3.createElement("div", {
529
- key: `fps-${i}`,
530
- className: bar,
531
- style: {
532
- position: "absolute",
533
- bottom: 0,
534
- right: `${len - 1 - i}px`,
535
- height: `${height * frame / max}px`,
536
- width: 1
1282
+ } else {
1283
+ return {
1284
+ ...state,
1285
+ frames: state.frames + 1
1286
+ };
1287
+ }
1288
+ }, {
1289
+ max: 0,
1290
+ len: 0,
1291
+ fps: [],
1292
+ frames: 0,
1293
+ prevTime: Date.now()
1294
+ });
1295
+ const requestRef = useRef3(null);
1296
+ const tick = () => {
1297
+ dispatch();
1298
+ requestRef.current = requestAnimationFrame(tick);
1299
+ };
1300
+ useEffect5(() => {
1301
+ requestRef.current = requestAnimationFrame(tick);
1302
+ return () => {
1303
+ if (requestRef.current) {
1304
+ cancelAnimationFrame(requestRef.current);
537
1305
  }
538
- }))));
539
- } finally {
540
- _effect.f();
541
- }
1306
+ };
1307
+ }, []);
1308
+ return /* @__PURE__ */ React6.createElement("div", {
1309
+ style: {
1310
+ width: width + 6
1311
+ },
1312
+ className: mx6("relative flex flex-col p-0.5", "bg-base-surface text-xs text-subdued font-thin pointer-events-none border border-separator", classNames)
1313
+ }, /* @__PURE__ */ React6.createElement("div", null, fps[len - 1], " FPS"), /* @__PURE__ */ React6.createElement("div", {
1314
+ className: "w-full relative",
1315
+ style: {
1316
+ height
1317
+ }
1318
+ }, fps.map((frame, i) => /* @__PURE__ */ React6.createElement("div", {
1319
+ key: `fps-${i}`,
1320
+ className: bar,
1321
+ style: {
1322
+ position: "absolute",
1323
+ bottom: 0,
1324
+ right: `${len - 1 - i}px`,
1325
+ height: `${height * frame / max}px`,
1326
+ width: 1
1327
+ }
1328
+ }))));
542
1329
  };
543
1330
 
544
1331
  // src/components/Grid/Grid.tsx
545
- import { useSignals as _useSignals4 } from "@preact-signals/safe-react/tracking";
546
- import React4, { forwardRef as forwardRef2, useId, useMemo as useMemo2 } from "react";
1332
+ import React7, { forwardRef as forwardRef2, useId, useMemo as useMemo4 } from "react";
547
1333
  import { useForwardedRef } from "@dxos/react-ui";
548
- import { mx as mx4 } from "@dxos/react-ui-theme";
1334
+ import { mx as mx7 } from "@dxos/ui-theme";
549
1335
  var gridRatios = [
550
1336
  1 / 4,
551
1337
  1,
@@ -558,67 +1344,57 @@ var defaultOffset = {
558
1344
  y: 0
559
1345
  };
560
1346
  var createId = (parent, grid) => `dx-canvas-grid-${parent}-${grid}`;
561
- var GridComponent = /* @__PURE__ */ forwardRef2(({ size: gridSize = defaultGridSize, scale = 1, offset = defaultOffset, showAxes = true, classNames }, forwardedRef) => {
562
- var _effect = _useSignals4();
563
- try {
564
- const svgRef = useForwardedRef(forwardedRef);
565
- const instanceId = useId();
566
- const grids = useMemo2(() => gridRatios.map((ratio) => ({
567
- id: ratio,
568
- size: ratio * gridSize * scale
569
- })).filter(({ size }) => size >= gridSize && size <= 256), [
570
- gridSize,
571
- scale
572
- ]);
573
- const { width = 0, height = 0 } = svgRef.current?.getBoundingClientRect() ?? {};
574
- return /* @__PURE__ */ React4.createElement("svg", {
575
- ...testId("dx-canvas-grid"),
576
- ref: svgRef,
577
- className: mx4("absolute inset-0 w-full h-full pointer-events-none touch-none select-none", "stroke-neutral-500", classNames)
578
- }, /* @__PURE__ */ React4.createElement("defs", null, grids.map(({ id, size }) => /* @__PURE__ */ React4.createElement(GridPattern, {
579
- key: id,
580
- id: createId(instanceId, id),
581
- offset,
582
- size
583
- }))), showAxes && /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement("line", {
584
- x1: 0,
585
- y1: offset.y,
586
- x2: width,
587
- y2: offset.y,
588
- className: "stroke-neutral-500 opacity-40"
589
- }), /* @__PURE__ */ React4.createElement("line", {
590
- x1: offset.x,
591
- y1: 0,
592
- x2: offset.x,
593
- y2: height,
594
- className: "stroke-neutral-500 opacity-40"
595
- })), /* @__PURE__ */ React4.createElement("g", null, grids.map(({ id }, i) => /* @__PURE__ */ React4.createElement("rect", {
596
- key: id,
597
- opacity: 0.1 + i * 0.05,
598
- fill: `url(#${createId(instanceId, id)})`,
599
- width: "100%",
600
- height: "100%"
601
- }))));
602
- } finally {
603
- _effect.f();
604
- }
605
- });
606
1347
  var Grid = (props) => {
607
- var _effect = _useSignals4();
608
- try {
609
- const { scale, offset } = useCanvasContext();
610
- return /* @__PURE__ */ React4.createElement(GridComponent, {
611
- ...props,
612
- scale,
613
- offset
614
- });
615
- } finally {
616
- _effect.f();
617
- }
1348
+ const { scale, offset } = useCanvasContext();
1349
+ return /* @__PURE__ */ React7.createElement(GridComponent, {
1350
+ ...props,
1351
+ scale,
1352
+ offset
1353
+ });
618
1354
  };
1355
+ var GridComponent = /* @__PURE__ */ forwardRef2(({ size: gridSize = defaultGridSize, scale = 1, offset = defaultOffset, showAxes = true, classNames }, forwardedRef) => {
1356
+ const svgRef = useForwardedRef(forwardedRef);
1357
+ const { width = 0, height = 0 } = svgRef.current?.getBoundingClientRect() ?? {};
1358
+ const instanceId = useId();
1359
+ const grids = useMemo4(() => gridRatios.map((ratio) => ({
1360
+ id: ratio,
1361
+ size: ratio * gridSize * scale
1362
+ })).filter(({ size }) => size >= gridSize && size <= 128), [
1363
+ gridSize,
1364
+ scale
1365
+ ]);
1366
+ return /* @__PURE__ */ React7.createElement("svg", {
1367
+ ...testId("dx-canvas-grid"),
1368
+ ref: svgRef,
1369
+ className: mx7("dx-fullscreen pointer-events-none touch-none select-none", "stroke-neutral-500", classNames)
1370
+ }, /* @__PURE__ */ React7.createElement("defs", null, grids.map(({ id, size }) => /* @__PURE__ */ React7.createElement(GridPattern, {
1371
+ key: id,
1372
+ id: createId(instanceId, id),
1373
+ offset,
1374
+ size
1375
+ }))), showAxes && /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("line", {
1376
+ x1: 0,
1377
+ y1: offset.y,
1378
+ x2: width,
1379
+ y2: offset.y,
1380
+ className: "stroke-neutral-500 opacity-40"
1381
+ }), /* @__PURE__ */ React7.createElement("line", {
1382
+ x1: offset.x,
1383
+ y1: 0,
1384
+ x2: offset.x,
1385
+ y2: height,
1386
+ className: "stroke-neutral-500 opacity-40"
1387
+ })), /* @__PURE__ */ React7.createElement("g", null, grids.map(({ id }, i) => /* @__PURE__ */ React7.createElement("rect", {
1388
+ key: id,
1389
+ opacity: 0.1 + i * 0.05,
1390
+ fill: `url(#${createId(instanceId, id)})`,
1391
+ width: "100%",
1392
+ height: "100%"
1393
+ }))));
1394
+ });
619
1395
 
620
1396
  // src/types.ts
621
- import { Schema } from "effect";
1397
+ import * as Schema from "effect/Schema";
622
1398
  var Point = Schema.Struct({
623
1399
  x: Schema.Number,
624
1400
  y: Schema.Number
@@ -632,6 +1408,7 @@ export {
632
1408
  Arrow,
633
1409
  Canvas,
634
1410
  CanvasContext,
1411
+ CellGrid,
635
1412
  DATA_TEST_ID,
636
1413
  Dimension,
637
1414
  FPS,
@@ -643,14 +1420,31 @@ export {
643
1420
  Point,
644
1421
  ProjectionMapper,
645
1422
  Rect,
1423
+ Ruler,
1424
+ TrackHeader,
1425
+ attachPointerHandlers,
1426
+ attachWheelHandlers,
1427
+ cellKey,
1428
+ cellWidth,
1429
+ createCellGridAtoms,
646
1430
  createPath,
647
1431
  defaultOrigin,
1432
+ defaultViewport,
1433
+ drawCells,
1434
+ drawOverlay,
648
1435
  getRelativePoint,
649
1436
  getZoomTransform,
1437
+ hitTestCell,
650
1438
  inspectElement,
1439
+ screenToWorld,
651
1440
  testId,
1441
+ toggleCell,
652
1442
  useCanvasContext,
1443
+ useDrag,
653
1444
  useWheel,
1445
+ visibleCellRange,
1446
+ visibleCells,
1447
+ worldToScreen,
654
1448
  zoomInPlace,
655
1449
  zoomTo
656
1450
  };