@dxos/react-ui-canvas 0.8.4-main.84f28bd → 0.8.4-main.8baae0fced

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