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

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