@dxos/react-ui-canvas 0.8.4-main.ead640a → 0.8.4-main.f466a3d56e

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