@dxos/react-ui-canvas 0.8.4-staging.ac66bdf99f → 0.9.1-main.c7dcc2e112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/browser/index.mjs +815 -19
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/node-esm/index.mjs +815 -19
  6. package/dist/lib/node-esm/index.mjs.map +4 -4
  7. package/dist/lib/node-esm/meta.json +1 -1
  8. package/dist/types/src/components/Canvas/Canvas.d.ts +1 -1
  9. package/dist/types/src/components/Canvas/Canvas.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/CellGrid/CellGrid.d.ts +21 -0
  11. package/dist/types/src/components/CellGrid/CellGrid.d.ts.map +1 -0
  12. package/dist/types/src/components/CellGrid/CellGrid.stories.d.ts +21 -0
  13. package/dist/types/src/components/CellGrid/CellGrid.stories.d.ts.map +1 -0
  14. package/dist/types/src/components/CellGrid/headers/Ruler.d.ts +15 -0
  15. package/dist/types/src/components/CellGrid/headers/Ruler.d.ts.map +1 -0
  16. package/dist/types/src/components/CellGrid/headers/TrackHeader.d.ts +19 -0
  17. package/dist/types/src/components/CellGrid/headers/TrackHeader.d.ts.map +1 -0
  18. package/dist/types/src/components/CellGrid/headers/index.d.ts +3 -0
  19. package/dist/types/src/components/CellGrid/headers/index.d.ts.map +1 -0
  20. package/dist/types/src/components/CellGrid/index.d.ts +6 -0
  21. package/dist/types/src/components/CellGrid/index.d.ts.map +1 -0
  22. package/dist/types/src/components/CellGrid/input/index.d.ts +3 -0
  23. package/dist/types/src/components/CellGrid/input/index.d.ts.map +1 -0
  24. package/dist/types/src/components/CellGrid/input/pointer.d.ts +33 -0
  25. package/dist/types/src/components/CellGrid/input/pointer.d.ts.map +1 -0
  26. package/dist/types/src/components/CellGrid/input/wheel.d.ts +14 -0
  27. package/dist/types/src/components/CellGrid/input/wheel.d.ts.map +1 -0
  28. package/dist/types/src/components/CellGrid/render/index.d.ts +3 -0
  29. package/dist/types/src/components/CellGrid/render/index.d.ts.map +1 -0
  30. package/dist/types/src/components/CellGrid/render/overlay-layer.d.ts +21 -0
  31. package/dist/types/src/components/CellGrid/render/overlay-layer.d.ts.map +1 -0
  32. package/dist/types/src/components/CellGrid/render/static-layer.d.ts +36 -0
  33. package/dist/types/src/components/CellGrid/render/static-layer.d.ts.map +1 -0
  34. package/dist/types/src/components/CellGrid/state/atoms.d.ts +23 -0
  35. package/dist/types/src/components/CellGrid/state/atoms.d.ts.map +1 -0
  36. package/dist/types/src/components/CellGrid/state/index.d.ts +4 -0
  37. package/dist/types/src/components/CellGrid/state/index.d.ts.map +1 -0
  38. package/dist/types/src/components/CellGrid/state/types.d.ts +39 -0
  39. package/dist/types/src/components/CellGrid/state/types.d.ts.map +1 -0
  40. package/dist/types/src/components/CellGrid/state/viewport.d.ts +52 -0
  41. package/dist/types/src/components/CellGrid/state/viewport.d.ts.map +1 -0
  42. package/dist/types/src/components/CellGrid/state/viewport.test.d.ts +2 -0
  43. package/dist/types/src/components/CellGrid/state/viewport.test.d.ts.map +1 -0
  44. package/dist/types/src/components/FPS.d.ts.map +1 -1
  45. package/dist/types/src/components/Grid/Grid.d.ts.map +1 -1
  46. package/dist/types/src/components/Grid/Grid.stories.d.ts.map +1 -1
  47. package/dist/types/src/components/index.d.ts +1 -0
  48. package/dist/types/src/components/index.d.ts.map +1 -1
  49. package/dist/types/src/hooks/projection.d.ts.map +1 -1
  50. package/dist/types/src/hooks/useDrag.d.ts.map +1 -1
  51. package/dist/types/src/hooks/useWheel.d.ts.map +1 -1
  52. package/dist/types/src/util/svg.d.ts +1 -1
  53. package/dist/types/src/util/svg.d.ts.map +1 -1
  54. package/dist/types/src/util/svg.stories.d.ts.map +1 -1
  55. package/dist/types/src/util/util.d.ts.map +1 -1
  56. package/dist/types/tsconfig.tsbuildinfo +1 -1
  57. package/package.json +16 -18
  58. package/src/components/Canvas/Canvas.tsx +1 -1
  59. package/src/components/CellGrid/CellGrid.stories.tsx +238 -0
  60. package/src/components/CellGrid/CellGrid.tsx +270 -0
  61. package/src/components/CellGrid/headers/Ruler.tsx +71 -0
  62. package/src/components/CellGrid/headers/TrackHeader.tsx +58 -0
  63. package/src/components/CellGrid/headers/index.ts +6 -0
  64. package/src/components/CellGrid/index.ts +9 -0
  65. package/src/components/CellGrid/input/index.ts +6 -0
  66. package/src/components/CellGrid/input/pointer.ts +236 -0
  67. package/src/components/CellGrid/input/wheel.ts +68 -0
  68. package/src/components/CellGrid/render/index.ts +6 -0
  69. package/src/components/CellGrid/render/overlay-layer.ts +66 -0
  70. package/src/components/CellGrid/render/static-layer.ts +112 -0
  71. package/src/components/CellGrid/state/atoms.ts +43 -0
  72. package/src/components/CellGrid/state/index.ts +7 -0
  73. package/src/components/CellGrid/state/types.ts +40 -0
  74. package/src/components/CellGrid/state/viewport.test.ts +50 -0
  75. package/src/components/CellGrid/state/viewport.ts +94 -0
  76. package/src/components/Grid/Grid.stories.tsx +1 -1
  77. package/src/components/index.ts +1 -0
@@ -471,16 +471,795 @@ var Canvas = /* @__PURE__ */ forwardRef(({ children, classNames, scale: scalePro
471
471
  setProjection
472
472
  }
473
473
  }, /* @__PURE__ */ React2.createElement("div", {
474
- role: "none",
475
474
  ...props,
476
475
  className: mx2("absolute inset-0 overflow-hidden", classNames),
477
476
  ref
478
477
  }, ready ? children : null));
479
478
  });
480
479
 
481
- // src/components/FPS.tsx
482
- import React3, { useEffect as useEffect4, useReducer, useRef as useRef2 } from "react";
480
+ // src/components/CellGrid/CellGrid.tsx
481
+ import { RegistryContext } from "@effect-atom/atom-react";
482
+ import React5, { useContext as useContext2, useEffect as useEffect4, useMemo as useMemo3, useRef as useRef2, useState as useState2 } from "react";
483
+ import { useResizeDetector as useResizeDetector2 } from "react-resize-detector";
484
+ import { mx as mx5 } from "@dxos/ui-theme";
485
+
486
+ // src/components/CellGrid/headers/Ruler.tsx
487
+ import React3, { useMemo as useMemo2 } from "react";
483
488
  import { mx as mx3 } from "@dxos/ui-theme";
489
+
490
+ // src/components/CellGrid/state/viewport.ts
491
+ var cellKey = (col, row) => `${col},${row}`;
492
+ var cellWidth = (viewport) => viewport.baseCellWidth * viewport.zoomX;
493
+ var worldToScreen = (viewport, headers, coord) => {
494
+ const w = cellWidth(viewport);
495
+ return {
496
+ x: headers.left + coord.col * w - viewport.scrollX,
497
+ y: headers.top + coord.row * viewport.cellHeight - viewport.scrollY,
498
+ w: (coord.length ?? 1) * w,
499
+ h: viewport.cellHeight
500
+ };
501
+ };
502
+ var screenToWorld = (viewport, headers, point) => {
503
+ const w = cellWidth(viewport);
504
+ return {
505
+ col: (point.x - headers.left + viewport.scrollX) / w,
506
+ row: (point.y - headers.top + viewport.scrollY) / viewport.cellHeight
507
+ };
508
+ };
509
+ var hitTestCell = (viewport, headers, point) => {
510
+ if (point.x < headers.left || point.y < headers.top) {
511
+ return null;
512
+ }
513
+ const { col, row } = screenToWorld(viewport, headers, point);
514
+ if (col < 0 || row < 0) {
515
+ return null;
516
+ }
517
+ return {
518
+ col: Math.floor(col),
519
+ row: Math.floor(row)
520
+ };
521
+ };
522
+ var visibleCellRange = (viewport, headers, size) => {
523
+ const w = cellWidth(viewport);
524
+ const innerW = Math.max(0, size.width - headers.left);
525
+ const innerH = Math.max(0, size.height - headers.top);
526
+ const minCol = Math.max(0, Math.floor(viewport.scrollX / w));
527
+ const maxCol = Math.floor((viewport.scrollX + innerW) / w);
528
+ const minRow = Math.max(0, Math.floor(viewport.scrollY / viewport.cellHeight));
529
+ const maxRow = Math.floor((viewport.scrollY + innerH) / viewport.cellHeight);
530
+ return {
531
+ minCol,
532
+ maxCol,
533
+ minRow,
534
+ maxRow
535
+ };
536
+ };
537
+ var visibleCells = function* (cells, range) {
538
+ for (const cell of cells.values()) {
539
+ if (cell.row < range.minRow || cell.row > range.maxRow) {
540
+ continue;
541
+ }
542
+ const start = cell.col;
543
+ const end = cell.col + cell.length - 1;
544
+ if (end < range.minCol || start > range.maxCol) {
545
+ continue;
546
+ }
547
+ yield cell;
548
+ }
549
+ };
550
+
551
+ // src/components/CellGrid/headers/Ruler.tsx
552
+ var Ruler = ({ viewport, headers, width, majorEvery = 4, classNames }) => {
553
+ const safeMajorEvery = Math.max(1, Math.floor(majorEvery));
554
+ const ticks = useMemo2(() => {
555
+ const w = cellWidth(viewport);
556
+ if (w < 1 || width <= headers.left) {
557
+ return [];
558
+ }
559
+ const innerWidth = width - headers.left;
560
+ const startCol = Math.floor(viewport.scrollX / w);
561
+ const endCol = Math.ceil((viewport.scrollX + innerWidth) / w);
562
+ const result = [];
563
+ for (let col = startCol; col <= endCol; col++) {
564
+ result.push({
565
+ col,
566
+ x: headers.left + col * w - viewport.scrollX,
567
+ major: col % safeMajorEvery === 0
568
+ });
569
+ }
570
+ return result;
571
+ }, [
572
+ viewport,
573
+ headers.left,
574
+ width,
575
+ safeMajorEvery
576
+ ]);
577
+ return /* @__PURE__ */ React3.createElement("div", {
578
+ className: mx3("absolute top-0 left-0 right-0 border-b border-neutral-200 dark:border-neutral-700 bg-baseSurface select-none overflow-hidden", classNames),
579
+ style: {
580
+ height: headers.top
581
+ }
582
+ }, ticks.map(({ col, x, major }) => /* @__PURE__ */ React3.createElement("div", {
583
+ key: col,
584
+ className: mx3("absolute top-0 bottom-0 text-[10px] text-neutral-500 dark:text-neutral-400", major ? "border-l border-neutral-400 dark:border-neutral-500" : "border-l border-neutral-200 dark:border-neutral-700"),
585
+ style: {
586
+ transform: `translateX(${x}px)`
587
+ }
588
+ }, major ? /* @__PURE__ */ React3.createElement("span", {
589
+ className: "absolute left-1 top-0"
590
+ }, col) : null)));
591
+ };
592
+
593
+ // src/components/CellGrid/headers/TrackHeader.tsx
594
+ import React4 from "react";
595
+ import { mx as mx4 } from "@dxos/ui-theme";
596
+ var TrackHeader = ({ viewport, headers, rows, height, classNames }) => {
597
+ return /* @__PURE__ */ React4.createElement("div", {
598
+ className: mx4("absolute left-0 border-r border-neutral-200 dark:border-neutral-700 select-none overflow-hidden", classNames),
599
+ style: {
600
+ top: headers.top,
601
+ width: headers.left,
602
+ height: Math.max(0, height - headers.top)
603
+ }
604
+ }, /* @__PURE__ */ React4.createElement("div", {
605
+ style: {
606
+ transform: `translateY(${-viewport.scrollY}px)`
607
+ }
608
+ }, rows.map((row, index) => /* @__PURE__ */ React4.createElement("div", {
609
+ key: row.id,
610
+ className: "flex items-center px-2 text-xs text-neutral-700 dark:text-neutral-300",
611
+ style: {
612
+ height: viewport.cellHeight,
613
+ // Match the canvas's row-band: a translucent gray overlay on odd rows,
614
+ // transparent on even rows. The container's overall background bleeds
615
+ // through, so the labels stay legible in both themes.
616
+ backgroundColor: index % 2 === 0 ? "transparent" : "rgba(128, 128, 128, 0.08)",
617
+ // Match the canvas gridline color (rgba(128, 128, 128, 0.25)). Use a
618
+ // half-pixel inset to keep crisp single-pixel rendering on retina.
619
+ boxShadow: "inset 0 -1px 0 rgba(128, 128, 128, 0.25)"
620
+ }
621
+ }, row.label ?? row.id))));
622
+ };
623
+
624
+ // src/components/CellGrid/input/pointer.ts
625
+ var attachPointerHandlers = (element, { registry, atoms, headers, handlers }) => {
626
+ let drag = null;
627
+ const local = (event) => {
628
+ const rect = element.getBoundingClientRect();
629
+ return {
630
+ x: event.clientX - rect.left,
631
+ y: event.clientY - rect.top
632
+ };
633
+ };
634
+ const tryCapture = (pointerId) => {
635
+ try {
636
+ element.setPointerCapture(pointerId);
637
+ } catch {
638
+ }
639
+ };
640
+ const onPointerDown = (event) => {
641
+ if (event.button === 1 || event.button === 0 && event.altKey) {
642
+ drag = {
643
+ kind: "pan",
644
+ lastX: event.clientX,
645
+ lastY: event.clientY
646
+ };
647
+ tryCapture(event.pointerId);
648
+ event.preventDefault();
649
+ return;
650
+ }
651
+ if (event.button !== 0) {
652
+ return;
653
+ }
654
+ const viewport = registry.get(atoms.viewport);
655
+ const point = local(event);
656
+ const coord = hitTestCell(viewport, headers, point);
657
+ if (!coord) {
658
+ return;
659
+ }
660
+ const tool = registry.get(atoms.tool);
661
+ tryCapture(event.pointerId);
662
+ switch (tool) {
663
+ case "toggle":
664
+ case "resize": {
665
+ const cells = registry.get(atoms.cells);
666
+ const key = cellKey(coord.col, coord.row);
667
+ const mode = cells.has(key) ? "unset" : "set";
668
+ handlers.onCellToggle?.(coord, mode);
669
+ drag = {
670
+ kind: "toggle",
671
+ mode,
672
+ touched: /* @__PURE__ */ new Set([
673
+ key
674
+ ])
675
+ };
676
+ break;
677
+ }
678
+ case "edit": {
679
+ drag = {
680
+ kind: "draw",
681
+ startCoord: coord,
682
+ endCoord: coord
683
+ };
684
+ handlers.onDrawUpdate?.(coord, coord);
685
+ break;
686
+ }
687
+ case "delete": {
688
+ const key = cellKey(coord.col, coord.row);
689
+ handlers.onCellToggle?.(coord, "unset");
690
+ drag = {
691
+ kind: "toggle",
692
+ mode: "unset",
693
+ touched: /* @__PURE__ */ new Set([
694
+ key
695
+ ])
696
+ };
697
+ break;
698
+ }
699
+ case "select": {
700
+ drag = {
701
+ kind: "select",
702
+ origin: coord
703
+ };
704
+ registry.set(atoms.selection, {
705
+ range: {
706
+ col0: coord.col,
707
+ row0: coord.row,
708
+ col1: coord.col,
709
+ row1: coord.row
710
+ }
711
+ });
712
+ break;
713
+ }
714
+ }
715
+ };
716
+ const onPointerMove = (event) => {
717
+ if (!drag) {
718
+ return;
719
+ }
720
+ if (drag.kind === "pan") {
721
+ const dx = event.clientX - drag.lastX;
722
+ const dy = event.clientY - drag.lastY;
723
+ drag.lastX = event.clientX;
724
+ drag.lastY = event.clientY;
725
+ registry.update(atoms.viewport, (current) => ({
726
+ ...current,
727
+ scrollX: Math.max(0, current.scrollX - dx),
728
+ scrollY: Math.max(0, current.scrollY - dy)
729
+ }));
730
+ return;
731
+ }
732
+ const viewport = registry.get(atoms.viewport);
733
+ const coord = hitTestCell(viewport, headers, local(event));
734
+ if (!coord) {
735
+ return;
736
+ }
737
+ if (drag.kind === "toggle") {
738
+ const key = cellKey(coord.col, coord.row);
739
+ if (!drag.touched.has(key)) {
740
+ drag.touched.add(key);
741
+ handlers.onCellToggle?.(coord, drag.mode);
742
+ }
743
+ } else if (drag.kind === "draw") {
744
+ const constrainedCol = coord.col;
745
+ if (constrainedCol !== drag.endCoord.col) {
746
+ drag.endCoord = {
747
+ col: constrainedCol,
748
+ row: drag.startCoord.row
749
+ };
750
+ handlers.onDrawUpdate?.(drag.startCoord, drag.endCoord);
751
+ }
752
+ } else if (drag.kind === "select") {
753
+ registry.set(atoms.selection, {
754
+ range: {
755
+ col0: drag.origin.col,
756
+ row0: drag.origin.row,
757
+ col1: coord.col,
758
+ row1: coord.row
759
+ }
760
+ });
761
+ }
762
+ };
763
+ const releaseCapture = (event) => {
764
+ if (element.hasPointerCapture(event.pointerId)) {
765
+ element.releasePointerCapture(event.pointerId);
766
+ }
767
+ };
768
+ const onPointerUp = (event) => {
769
+ if (!drag) {
770
+ return;
771
+ }
772
+ if (drag.kind === "draw") {
773
+ handlers.onDrawCommit?.(drag.startCoord, drag.endCoord);
774
+ } else if (drag.kind === "select") {
775
+ const range = registry.get(atoms.selection).range;
776
+ if (range) {
777
+ handlers.onSelectionCommit?.(range);
778
+ }
779
+ }
780
+ drag = null;
781
+ releaseCapture(event);
782
+ };
783
+ const onPointerCancel = (event) => {
784
+ drag = null;
785
+ releaseCapture(event);
786
+ };
787
+ element.addEventListener("pointerdown", onPointerDown);
788
+ element.addEventListener("pointermove", onPointerMove);
789
+ element.addEventListener("pointerup", onPointerUp);
790
+ element.addEventListener("pointercancel", onPointerCancel);
791
+ return () => {
792
+ element.removeEventListener("pointerdown", onPointerDown);
793
+ element.removeEventListener("pointermove", onPointerMove);
794
+ element.removeEventListener("pointerup", onPointerUp);
795
+ element.removeEventListener("pointercancel", onPointerCancel);
796
+ };
797
+ };
798
+ var toggleCell = (registry, atoms, coord, factory, mode = "toggle") => {
799
+ registry.update(atoms.cells, (current) => {
800
+ const next = new Map(current);
801
+ const key = cellKey(coord.col, coord.row);
802
+ const exists = next.has(key);
803
+ if (mode === "set" || mode === "toggle" && !exists) {
804
+ next.set(key, factory(coord));
805
+ } else if (mode === "unset" || mode === "toggle" && exists) {
806
+ next.delete(key);
807
+ }
808
+ return next;
809
+ });
810
+ };
811
+
812
+ // src/components/CellGrid/input/wheel.ts
813
+ var MIN_ZOOM = 0.25;
814
+ var MAX_ZOOM = 8;
815
+ var attachWheelHandlers = (element, { registry, atoms, headers }) => {
816
+ const onWheel = (event) => {
817
+ if (event.ctrlKey || event.metaKey) {
818
+ event.preventDefault();
819
+ const rect = element.getBoundingClientRect();
820
+ const x = event.clientX - rect.left;
821
+ const factor = Math.exp(-event.deltaY / 200);
822
+ registry.update(atoms.viewport, (current2) => {
823
+ const nextZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, current2.zoomX * factor));
824
+ if (nextZoom === current2.zoomX) {
825
+ return current2;
826
+ }
827
+ const w = cellWidth(current2);
828
+ const worldX = (x - headers.left + current2.scrollX) / w;
829
+ const nextW = current2.baseCellWidth * nextZoom;
830
+ const nextScrollX2 = Math.max(0, worldX * nextW - (x - headers.left));
831
+ return {
832
+ ...current2,
833
+ zoomX: nextZoom,
834
+ scrollX: nextScrollX2
835
+ };
836
+ });
837
+ return;
838
+ }
839
+ const dx = event.shiftKey ? event.deltaY : event.deltaX;
840
+ const dy = event.shiftKey ? 0 : event.deltaY;
841
+ const current = registry.get(atoms.viewport);
842
+ const nextScrollX = Math.max(0, current.scrollX + dx);
843
+ const nextScrollY = Math.max(0, current.scrollY + dy);
844
+ if (nextScrollX === current.scrollX && nextScrollY === current.scrollY) {
845
+ return;
846
+ }
847
+ event.preventDefault();
848
+ registry.set(atoms.viewport, {
849
+ ...current,
850
+ scrollX: nextScrollX,
851
+ scrollY: nextScrollY
852
+ });
853
+ };
854
+ element.addEventListener("wheel", onWheel, {
855
+ passive: false
856
+ });
857
+ return () => element.removeEventListener("wheel", onWheel);
858
+ };
859
+
860
+ // src/components/CellGrid/render/overlay-layer.ts
861
+ var drawOverlay = ({ ctx, size, viewport, headers, selection, playhead, style }) => {
862
+ ctx.clearRect(0, 0, size.width, size.height);
863
+ ctx.save();
864
+ ctx.beginPath();
865
+ ctx.rect(headers.left, headers.top, size.width - headers.left, size.height - headers.top);
866
+ ctx.clip();
867
+ if (selection.range) {
868
+ const { col0, row0, col1, row1 } = selection.range;
869
+ const minCol = Math.min(col0, col1);
870
+ const maxCol = Math.max(col0, col1);
871
+ const minRow = Math.min(row0, row1);
872
+ const maxRow = Math.max(row0, row1);
873
+ const tl = worldToScreen(viewport, headers, {
874
+ col: minCol,
875
+ row: minRow
876
+ });
877
+ const br = worldToScreen(viewport, headers, {
878
+ col: maxCol + 1,
879
+ row: maxRow + 1
880
+ });
881
+ ctx.fillStyle = style.selectionFill;
882
+ ctx.fillRect(tl.x, tl.y, br.x - tl.x, br.y - tl.y);
883
+ ctx.strokeStyle = style.selectionStroke;
884
+ ctx.setLineDash([
885
+ 4,
886
+ 3
887
+ ]);
888
+ ctx.lineWidth = 1;
889
+ ctx.strokeRect(tl.x + 0.5, tl.y + 0.5, br.x - tl.x - 1, br.y - tl.y - 1);
890
+ ctx.setLineDash([]);
891
+ }
892
+ if (playhead !== null) {
893
+ const w = cellWidth(viewport);
894
+ const x = headers.left + playhead * w - viewport.scrollX;
895
+ if (x >= headers.left && x <= size.width) {
896
+ ctx.strokeStyle = style.playhead;
897
+ ctx.lineWidth = 2;
898
+ ctx.beginPath();
899
+ ctx.moveTo(x, headers.top);
900
+ ctx.lineTo(x, size.height);
901
+ ctx.stroke();
902
+ }
903
+ }
904
+ ctx.restore();
905
+ };
906
+
907
+ // src/components/CellGrid/render/static-layer.ts
908
+ var drawCells = ({ ctx, size, viewport, headers, rows, cells, renderCell, style }) => {
909
+ ctx.clearRect(0, 0, size.width, size.height);
910
+ if (style.background) {
911
+ ctx.fillStyle = style.background;
912
+ ctx.fillRect(0, 0, size.width, size.height);
913
+ }
914
+ const range = visibleCellRange(viewport, headers, size);
915
+ const w = cellWidth(viewport);
916
+ const h = viewport.cellHeight;
917
+ if (style.rowBand) {
918
+ ctx.fillStyle = style.rowBand;
919
+ for (let row = range.minRow; row <= Math.min(range.maxRow, rows.length - 1); row++) {
920
+ if (row % 2 === 0) {
921
+ continue;
922
+ }
923
+ const y = headers.top + row * h - viewport.scrollY;
924
+ ctx.fillRect(headers.left, y, size.width - headers.left, h);
925
+ }
926
+ }
927
+ ctx.strokeStyle = style.gridLine;
928
+ ctx.lineWidth = 1;
929
+ ctx.beginPath();
930
+ for (let col = range.minCol; col <= range.maxCol + 1; col++) {
931
+ const x = Math.floor(headers.left + col * w - viewport.scrollX) + 0.5;
932
+ if (x < headers.left) {
933
+ continue;
934
+ }
935
+ ctx.moveTo(x, headers.top);
936
+ ctx.lineTo(x, size.height);
937
+ }
938
+ for (let row = range.minRow; row <= Math.min(range.maxRow + 1, rows.length); row++) {
939
+ const y = Math.floor(headers.top + row * h - viewport.scrollY) + 0.5;
940
+ if (y < headers.top) {
941
+ continue;
942
+ }
943
+ ctx.moveTo(headers.left, y);
944
+ ctx.lineTo(size.width, y);
945
+ }
946
+ ctx.stroke();
947
+ ctx.save();
948
+ ctx.beginPath();
949
+ ctx.rect(headers.left, headers.top, size.width - headers.left, size.height - headers.top);
950
+ ctx.clip();
951
+ for (const cell of visibleCells(cells, range)) {
952
+ if (cell.row >= rows.length) {
953
+ continue;
954
+ }
955
+ const rect = worldToScreen(viewport, headers, cell);
956
+ renderCell({
957
+ ctx,
958
+ ...rect,
959
+ cell
960
+ });
961
+ }
962
+ ctx.restore();
963
+ };
964
+
965
+ // src/components/CellGrid/CellGrid.tsx
966
+ var defaultHeaders = {
967
+ left: 80,
968
+ top: 24
969
+ };
970
+ var defaultStaticStyle = {
971
+ gridLine: "rgba(128,128,128,0.25)",
972
+ rowBand: "rgba(128,128,128,0.06)"
973
+ };
974
+ var defaultOverlayStyle = {
975
+ playhead: "rgb(220, 38, 38)",
976
+ selectionFill: "rgba(59, 130, 246, 0.15)",
977
+ selectionStroke: "rgb(59, 130, 246)"
978
+ };
979
+ var setupCanvas = (canvas, width, height) => {
980
+ const dpr = window.devicePixelRatio || 1;
981
+ canvas.width = Math.max(1, Math.floor(width * dpr));
982
+ canvas.height = Math.max(1, Math.floor(height * dpr));
983
+ canvas.style.width = `${width}px`;
984
+ canvas.style.height = `${height}px`;
985
+ const ctx = canvas.getContext("2d");
986
+ if (!ctx) {
987
+ return null;
988
+ }
989
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
990
+ return ctx;
991
+ };
992
+ var CellGrid = ({ atoms, rows, renderCell, headers: headersProp, staticStyle: staticStyleProp, overlayStyle: overlayStyleProp, classNames, onCellToggle, onSelectionCommit, onDrawUpdate, onDrawCommit }) => {
993
+ const registry = useContext2(RegistryContext);
994
+ const headers = useMemo3(() => {
995
+ if (headersProp === false) {
996
+ return {
997
+ left: 0,
998
+ top: 0
999
+ };
1000
+ }
1001
+ return {
1002
+ ...defaultHeaders,
1003
+ ...headersProp ?? {}
1004
+ };
1005
+ }, [
1006
+ headersProp
1007
+ ]);
1008
+ const staticStyle = useMemo3(() => ({
1009
+ ...defaultStaticStyle,
1010
+ ...staticStyleProp ?? {}
1011
+ }), [
1012
+ staticStyleProp
1013
+ ]);
1014
+ const overlayStyle = useMemo3(() => ({
1015
+ ...defaultOverlayStyle,
1016
+ ...overlayStyleProp ?? {}
1017
+ }), [
1018
+ overlayStyleProp
1019
+ ]);
1020
+ const { ref: containerRef, width = 0, height = 0 } = useResizeDetector2();
1021
+ const staticCanvasRef = useRef2(null);
1022
+ const overlayCanvasRef = useRef2(null);
1023
+ const overlayInputRef = useRef2(null);
1024
+ const [staticCtx, setStaticCtx] = useState2(null);
1025
+ const [overlayCtx, setOverlayCtx] = useState2(null);
1026
+ const [viewportState, setViewportState] = useState2(() => registry.get(atoms.viewport));
1027
+ useEffect4(() => {
1028
+ if (!width || !height) {
1029
+ return;
1030
+ }
1031
+ if (staticCanvasRef.current) {
1032
+ const ctx = setupCanvas(staticCanvasRef.current, width, height);
1033
+ setStaticCtx(ctx);
1034
+ }
1035
+ if (overlayCanvasRef.current) {
1036
+ const ctx = setupCanvas(overlayCanvasRef.current, width, height);
1037
+ setOverlayCtx(ctx);
1038
+ }
1039
+ }, [
1040
+ width,
1041
+ height
1042
+ ]);
1043
+ useEffect4(() => registry.subscribe(atoms.viewport, (next) => setViewportState(next)), [
1044
+ registry,
1045
+ atoms.viewport
1046
+ ]);
1047
+ useEffect4(() => {
1048
+ if (!staticCtx || !width || !height) {
1049
+ return;
1050
+ }
1051
+ let raf = null;
1052
+ const schedule = () => {
1053
+ if (raf !== null) {
1054
+ return;
1055
+ }
1056
+ raf = requestAnimationFrame(() => {
1057
+ raf = null;
1058
+ drawCells({
1059
+ ctx: staticCtx,
1060
+ size: {
1061
+ width,
1062
+ height
1063
+ },
1064
+ viewport: registry.get(atoms.viewport),
1065
+ headers,
1066
+ rows,
1067
+ cells: registry.get(atoms.cells),
1068
+ renderCell,
1069
+ style: staticStyle
1070
+ });
1071
+ });
1072
+ };
1073
+ schedule();
1074
+ const unsubCells = registry.subscribe(atoms.cells, schedule);
1075
+ const unsubViewport = registry.subscribe(atoms.viewport, schedule);
1076
+ return () => {
1077
+ if (raf !== null) {
1078
+ cancelAnimationFrame(raf);
1079
+ }
1080
+ unsubCells();
1081
+ unsubViewport();
1082
+ };
1083
+ }, [
1084
+ staticCtx,
1085
+ width,
1086
+ height,
1087
+ registry,
1088
+ atoms.cells,
1089
+ atoms.viewport,
1090
+ headers,
1091
+ rows,
1092
+ renderCell,
1093
+ staticStyle
1094
+ ]);
1095
+ useEffect4(() => {
1096
+ if (!overlayCtx || !width || !height) {
1097
+ return;
1098
+ }
1099
+ let raf = null;
1100
+ let stopped = false;
1101
+ const paint = () => {
1102
+ drawOverlay({
1103
+ ctx: overlayCtx,
1104
+ size: {
1105
+ width,
1106
+ height
1107
+ },
1108
+ viewport: registry.get(atoms.viewport),
1109
+ headers,
1110
+ selection: registry.get(atoms.selection),
1111
+ playhead: registry.get(atoms.playhead),
1112
+ style: overlayStyle
1113
+ });
1114
+ };
1115
+ const isAnimating = () => registry.get(atoms.playhead) !== null;
1116
+ const loop = () => {
1117
+ if (stopped) {
1118
+ return;
1119
+ }
1120
+ paint();
1121
+ if (isAnimating()) {
1122
+ raf = requestAnimationFrame(loop);
1123
+ } else {
1124
+ raf = null;
1125
+ }
1126
+ };
1127
+ const kick = () => {
1128
+ paint();
1129
+ if (raf === null && isAnimating()) {
1130
+ raf = requestAnimationFrame(loop);
1131
+ }
1132
+ };
1133
+ kick();
1134
+ const unsubSelection = registry.subscribe(atoms.selection, () => paint());
1135
+ const unsubPlayhead = registry.subscribe(atoms.playhead, kick);
1136
+ const unsubViewport = registry.subscribe(atoms.viewport, () => paint());
1137
+ return () => {
1138
+ stopped = true;
1139
+ if (raf !== null) {
1140
+ cancelAnimationFrame(raf);
1141
+ }
1142
+ unsubSelection();
1143
+ unsubPlayhead();
1144
+ unsubViewport();
1145
+ };
1146
+ }, [
1147
+ overlayCtx,
1148
+ width,
1149
+ height,
1150
+ registry,
1151
+ atoms.selection,
1152
+ atoms.playhead,
1153
+ atoms.viewport,
1154
+ headers,
1155
+ overlayStyle
1156
+ ]);
1157
+ const callbacksRef = useRef2({
1158
+ onCellToggle,
1159
+ onSelectionCommit,
1160
+ onDrawUpdate,
1161
+ onDrawCommit
1162
+ });
1163
+ callbacksRef.current = {
1164
+ onCellToggle,
1165
+ onSelectionCommit,
1166
+ onDrawUpdate,
1167
+ onDrawCommit
1168
+ };
1169
+ useEffect4(() => {
1170
+ const element = overlayInputRef.current;
1171
+ if (!element) {
1172
+ return;
1173
+ }
1174
+ const detachPointer = attachPointerHandlers(element, {
1175
+ registry,
1176
+ atoms,
1177
+ headers,
1178
+ handlers: {
1179
+ onCellToggle: (coord, mode) => callbacksRef.current.onCellToggle?.(coord, mode),
1180
+ onSelectionCommit: (range) => callbacksRef.current.onSelectionCommit?.(range),
1181
+ onDrawUpdate: (start, end) => callbacksRef.current.onDrawUpdate?.(start, end),
1182
+ onDrawCommit: (start, end) => callbacksRef.current.onDrawCommit?.(start, end)
1183
+ }
1184
+ });
1185
+ const detachWheel = attachWheelHandlers(element, {
1186
+ registry,
1187
+ atoms,
1188
+ headers
1189
+ });
1190
+ return () => {
1191
+ detachPointer();
1192
+ detachWheel();
1193
+ };
1194
+ }, [
1195
+ registry,
1196
+ atoms,
1197
+ headers
1198
+ ]);
1199
+ return /* @__PURE__ */ React5.createElement("div", {
1200
+ ref: containerRef,
1201
+ className: mx5("relative w-full h-full overflow-hidden bg-baseSurface", classNames)
1202
+ }, /* @__PURE__ */ React5.createElement("canvas", {
1203
+ ref: staticCanvasRef,
1204
+ className: "absolute inset-0 pointer-events-none",
1205
+ style: {
1206
+ top: -1,
1207
+ left: -1
1208
+ }
1209
+ }), /* @__PURE__ */ React5.createElement("canvas", {
1210
+ ref: overlayCanvasRef,
1211
+ className: "absolute inset-0 pointer-events-none",
1212
+ style: {
1213
+ top: -1,
1214
+ left: -1
1215
+ }
1216
+ }), /* @__PURE__ */ React5.createElement("div", {
1217
+ ref: overlayInputRef,
1218
+ className: "absolute inset-0 touch-none",
1219
+ style: {
1220
+ paddingLeft: headers.left,
1221
+ paddingTop: headers.top
1222
+ }
1223
+ }), headers.top > 0 && /* @__PURE__ */ React5.createElement(Ruler, {
1224
+ viewport: viewportState,
1225
+ headers,
1226
+ width
1227
+ }), headers.left > 0 && /* @__PURE__ */ React5.createElement(TrackHeader, {
1228
+ viewport: viewportState,
1229
+ headers,
1230
+ rows,
1231
+ height
1232
+ }), headers.top > 0 && headers.left > 0 && /* @__PURE__ */ React5.createElement("div", {
1233
+ className: "absolute top-0 left-0 border-b border-r border-neutral-200 dark:border-neutral-700 bg-baseSurface",
1234
+ style: {
1235
+ width: headers.left,
1236
+ height: headers.top
1237
+ }
1238
+ }));
1239
+ };
1240
+
1241
+ // src/components/CellGrid/state/atoms.ts
1242
+ import { Atom } from "@effect-atom/atom-react";
1243
+ var defaultViewport = (options = {}) => ({
1244
+ scrollX: 0,
1245
+ scrollY: 0,
1246
+ baseCellWidth: options.cellWidth ?? 24,
1247
+ cellHeight: options.cellHeight ?? 24,
1248
+ zoomX: 1
1249
+ });
1250
+ var createCellGridAtoms = (options = {}) => ({
1251
+ cells: Atom.keepAlive(Atom.make(/* @__PURE__ */ new Map())),
1252
+ viewport: Atom.keepAlive(Atom.make(defaultViewport(options))),
1253
+ selection: Atom.keepAlive(Atom.make({
1254
+ range: null
1255
+ })),
1256
+ playhead: Atom.keepAlive(Atom.make(null)),
1257
+ tool: Atom.keepAlive(Atom.make("toggle"))
1258
+ });
1259
+
1260
+ // src/components/FPS.tsx
1261
+ import React6, { useEffect as useEffect5, useReducer, useRef as useRef3 } from "react";
1262
+ import { mx as mx6 } from "@dxos/ui-theme";
484
1263
  var SEC = 1e3;
485
1264
  var FPS = ({ classNames, width = 60, height = 30, bar = "bg-cyan-500" }) => {
486
1265
  const [{ fps, max, len }, dispatch] = useReducer((state) => {
@@ -513,12 +1292,12 @@ var FPS = ({ classNames, width = 60, height = 30, bar = "bg-cyan-500" }) => {
513
1292
  frames: 0,
514
1293
  prevTime: Date.now()
515
1294
  });
516
- const requestRef = useRef2(null);
1295
+ const requestRef = useRef3(null);
517
1296
  const tick = () => {
518
1297
  dispatch();
519
1298
  requestRef.current = requestAnimationFrame(tick);
520
1299
  };
521
- useEffect4(() => {
1300
+ useEffect5(() => {
522
1301
  requestRef.current = requestAnimationFrame(tick);
523
1302
  return () => {
524
1303
  if (requestRef.current) {
@@ -526,17 +1305,17 @@ var FPS = ({ classNames, width = 60, height = 30, bar = "bg-cyan-500" }) => {
526
1305
  }
527
1306
  };
528
1307
  }, []);
529
- return /* @__PURE__ */ React3.createElement("div", {
1308
+ return /* @__PURE__ */ React6.createElement("div", {
530
1309
  style: {
531
1310
  width: width + 6
532
1311
  },
533
- className: mx3("relative flex flex-col p-0.5", "bg-base-surface 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", {
1312
+ className: mx6("relative flex flex-col p-0.5", "bg-base-surface text-xs text-subdued font-thin pointer-events-none border border-separator", classNames)
1313
+ }, /* @__PURE__ */ React6.createElement("div", null, fps[len - 1], " FPS"), /* @__PURE__ */ React6.createElement("div", {
535
1314
  className: "w-full relative",
536
1315
  style: {
537
1316
  height
538
1317
  }
539
- }, fps.map((frame, i) => /* @__PURE__ */ React3.createElement("div", {
1318
+ }, fps.map((frame, i) => /* @__PURE__ */ React6.createElement("div", {
540
1319
  key: `fps-${i}`,
541
1320
  className: bar,
542
1321
  style: {
@@ -550,9 +1329,9 @@ var FPS = ({ classNames, width = 60, height = 30, bar = "bg-cyan-500" }) => {
550
1329
  };
551
1330
 
552
1331
  // src/components/Grid/Grid.tsx
553
- import React4, { forwardRef as forwardRef2, useId, useMemo as useMemo2 } from "react";
1332
+ import React7, { forwardRef as forwardRef2, useId, useMemo as useMemo4 } from "react";
554
1333
  import { useForwardedRef } from "@dxos/react-ui";
555
- import { mx as mx4 } from "@dxos/ui-theme";
1334
+ import { mx as mx7 } from "@dxos/ui-theme";
556
1335
  var gridRatios = [
557
1336
  1 / 4,
558
1337
  1,
@@ -567,7 +1346,7 @@ var defaultOffset = {
567
1346
  var createId = (parent, grid) => `dx-canvas-grid-${parent}-${grid}`;
568
1347
  var Grid = (props) => {
569
1348
  const { scale, offset } = useCanvasContext();
570
- return /* @__PURE__ */ React4.createElement(GridComponent, {
1349
+ return /* @__PURE__ */ React7.createElement(GridComponent, {
571
1350
  ...props,
572
1351
  scale,
573
1352
  offset
@@ -577,35 +1356,35 @@ var GridComponent = /* @__PURE__ */ forwardRef2(({ size: gridSize = defaultGridS
577
1356
  const svgRef = useForwardedRef(forwardedRef);
578
1357
  const { width = 0, height = 0 } = svgRef.current?.getBoundingClientRect() ?? {};
579
1358
  const instanceId = useId();
580
- const grids = useMemo2(() => gridRatios.map((ratio) => ({
1359
+ const grids = useMemo4(() => gridRatios.map((ratio) => ({
581
1360
  id: ratio,
582
1361
  size: ratio * gridSize * scale
583
1362
  })).filter(({ size }) => size >= gridSize && size <= 128), [
584
1363
  gridSize,
585
1364
  scale
586
1365
  ]);
587
- return /* @__PURE__ */ React4.createElement("svg", {
1366
+ return /* @__PURE__ */ React7.createElement("svg", {
588
1367
  ...testId("dx-canvas-grid"),
589
1368
  ref: svgRef,
590
- className: mx4("dx-fullscreen pointer-events-none touch-none select-none", "stroke-neutral-500", classNames)
591
- }, /* @__PURE__ */ React4.createElement("defs", null, grids.map(({ id, size }) => /* @__PURE__ */ React4.createElement(GridPattern, {
1369
+ className: mx7("dx-fullscreen pointer-events-none touch-none select-none", "stroke-neutral-500", classNames)
1370
+ }, /* @__PURE__ */ React7.createElement("defs", null, grids.map(({ id, size }) => /* @__PURE__ */ React7.createElement(GridPattern, {
592
1371
  key: id,
593
1372
  id: createId(instanceId, id),
594
1373
  offset,
595
1374
  size
596
- }))), showAxes && /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement("line", {
1375
+ }))), showAxes && /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("line", {
597
1376
  x1: 0,
598
1377
  y1: offset.y,
599
1378
  x2: width,
600
1379
  y2: offset.y,
601
1380
  className: "stroke-neutral-500 opacity-40"
602
- }), /* @__PURE__ */ React4.createElement("line", {
1381
+ }), /* @__PURE__ */ React7.createElement("line", {
603
1382
  x1: offset.x,
604
1383
  y1: 0,
605
1384
  x2: offset.x,
606
1385
  y2: height,
607
1386
  className: "stroke-neutral-500 opacity-40"
608
- })), /* @__PURE__ */ React4.createElement("g", null, grids.map(({ id }, i) => /* @__PURE__ */ React4.createElement("rect", {
1387
+ })), /* @__PURE__ */ React7.createElement("g", null, grids.map(({ id }, i) => /* @__PURE__ */ React7.createElement("rect", {
609
1388
  key: id,
610
1389
  opacity: 0.1 + i * 0.05,
611
1390
  fill: `url(#${createId(instanceId, id)})`,
@@ -629,6 +1408,7 @@ export {
629
1408
  Arrow,
630
1409
  Canvas,
631
1410
  CanvasContext,
1411
+ CellGrid,
632
1412
  DATA_TEST_ID,
633
1413
  Dimension,
634
1414
  FPS,
@@ -640,15 +1420,31 @@ export {
640
1420
  Point,
641
1421
  ProjectionMapper,
642
1422
  Rect,
1423
+ Ruler,
1424
+ TrackHeader,
1425
+ attachPointerHandlers,
1426
+ attachWheelHandlers,
1427
+ cellKey,
1428
+ cellWidth,
1429
+ createCellGridAtoms,
643
1430
  createPath,
644
1431
  defaultOrigin,
1432
+ defaultViewport,
1433
+ drawCells,
1434
+ drawOverlay,
645
1435
  getRelativePoint,
646
1436
  getZoomTransform,
1437
+ hitTestCell,
647
1438
  inspectElement,
1439
+ screenToWorld,
648
1440
  testId,
1441
+ toggleCell,
649
1442
  useCanvasContext,
650
1443
  useDrag,
651
1444
  useWheel,
1445
+ visibleCellRange,
1446
+ visibleCells,
1447
+ worldToScreen,
652
1448
  zoomInPlace,
653
1449
  zoomTo
654
1450
  };