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