@dxos/react-ui-canvas 0.8.4-main.03d5cd7b56 → 0.8.4-main.05e74ebcff

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 (63) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/browser/index.mjs +777 -18
  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 +777 -18
  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/CellGrid/CellGrid.d.ts +21 -0
  9. package/dist/types/src/components/CellGrid/CellGrid.d.ts.map +1 -0
  10. package/dist/types/src/components/CellGrid/CellGrid.stories.d.ts +21 -0
  11. package/dist/types/src/components/CellGrid/CellGrid.stories.d.ts.map +1 -0
  12. package/dist/types/src/components/CellGrid/headers/Ruler.d.ts +15 -0
  13. package/dist/types/src/components/CellGrid/headers/Ruler.d.ts.map +1 -0
  14. package/dist/types/src/components/CellGrid/headers/TrackHeader.d.ts +19 -0
  15. package/dist/types/src/components/CellGrid/headers/TrackHeader.d.ts.map +1 -0
  16. package/dist/types/src/components/CellGrid/headers/index.d.ts +3 -0
  17. package/dist/types/src/components/CellGrid/headers/index.d.ts.map +1 -0
  18. package/dist/types/src/components/CellGrid/index.d.ts +6 -0
  19. package/dist/types/src/components/CellGrid/index.d.ts.map +1 -0
  20. package/dist/types/src/components/CellGrid/input/index.d.ts +3 -0
  21. package/dist/types/src/components/CellGrid/input/index.d.ts.map +1 -0
  22. package/dist/types/src/components/CellGrid/input/pointer.d.ts +29 -0
  23. package/dist/types/src/components/CellGrid/input/pointer.d.ts.map +1 -0
  24. package/dist/types/src/components/CellGrid/input/wheel.d.ts +14 -0
  25. package/dist/types/src/components/CellGrid/input/wheel.d.ts.map +1 -0
  26. package/dist/types/src/components/CellGrid/render/index.d.ts +3 -0
  27. package/dist/types/src/components/CellGrid/render/index.d.ts.map +1 -0
  28. package/dist/types/src/components/CellGrid/render/overlay-layer.d.ts +21 -0
  29. package/dist/types/src/components/CellGrid/render/overlay-layer.d.ts.map +1 -0
  30. package/dist/types/src/components/CellGrid/render/static-layer.d.ts +36 -0
  31. package/dist/types/src/components/CellGrid/render/static-layer.d.ts.map +1 -0
  32. package/dist/types/src/components/CellGrid/state/atoms.d.ts +23 -0
  33. package/dist/types/src/components/CellGrid/state/atoms.d.ts.map +1 -0
  34. package/dist/types/src/components/CellGrid/state/index.d.ts +4 -0
  35. package/dist/types/src/components/CellGrid/state/index.d.ts.map +1 -0
  36. package/dist/types/src/components/CellGrid/state/types.d.ts +39 -0
  37. package/dist/types/src/components/CellGrid/state/types.d.ts.map +1 -0
  38. package/dist/types/src/components/CellGrid/state/viewport.d.ts +52 -0
  39. package/dist/types/src/components/CellGrid/state/viewport.d.ts.map +1 -0
  40. package/dist/types/src/components/CellGrid/state/viewport.test.d.ts +2 -0
  41. package/dist/types/src/components/CellGrid/state/viewport.test.d.ts.map +1 -0
  42. package/dist/types/src/components/index.d.ts +1 -0
  43. package/dist/types/src/components/index.d.ts.map +1 -1
  44. package/dist/types/tsconfig.tsbuildinfo +1 -1
  45. package/package.json +14 -13
  46. package/src/components/CellGrid/CellGrid.stories.tsx +238 -0
  47. package/src/components/CellGrid/CellGrid.tsx +266 -0
  48. package/src/components/CellGrid/headers/Ruler.tsx +71 -0
  49. package/src/components/CellGrid/headers/TrackHeader.tsx +58 -0
  50. package/src/components/CellGrid/headers/index.ts +6 -0
  51. package/src/components/CellGrid/index.ts +9 -0
  52. package/src/components/CellGrid/input/index.ts +6 -0
  53. package/src/components/CellGrid/input/pointer.ts +208 -0
  54. package/src/components/CellGrid/input/wheel.ts +68 -0
  55. package/src/components/CellGrid/render/index.ts +6 -0
  56. package/src/components/CellGrid/render/overlay-layer.ts +66 -0
  57. package/src/components/CellGrid/render/static-layer.ts +112 -0
  58. package/src/components/CellGrid/state/atoms.ts +43 -0
  59. package/src/components/CellGrid/state/index.ts +7 -0
  60. package/src/components/CellGrid/state/types.ts +40 -0
  61. package/src/components/CellGrid/state/viewport.test.ts +50 -0
  62. package/src/components/CellGrid/state/viewport.ts +94 -0
  63. package/src/components/index.ts +1 -0
@@ -477,9 +477,751 @@ var Canvas = /* @__PURE__ */ forwardRef(({ children, classNames, scale: scalePro
477
477
  }, ready ? children : null));
478
478
  });
479
479
 
480
- // src/components/FPS.tsx
481
- 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";
482
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 "select": {
679
+ drag = {
680
+ kind: "select",
681
+ origin: coord
682
+ };
683
+ registry.set(atoms.selection, {
684
+ range: {
685
+ col0: coord.col,
686
+ row0: coord.row,
687
+ col1: coord.col,
688
+ row1: coord.row
689
+ }
690
+ });
691
+ break;
692
+ }
693
+ }
694
+ };
695
+ const onPointerMove = (event) => {
696
+ if (!drag) {
697
+ return;
698
+ }
699
+ if (drag.kind === "pan") {
700
+ const dx = event.clientX - drag.lastX;
701
+ const dy = event.clientY - drag.lastY;
702
+ drag.lastX = event.clientX;
703
+ drag.lastY = event.clientY;
704
+ registry.update(atoms.viewport, (current) => ({
705
+ ...current,
706
+ scrollX: Math.max(0, current.scrollX - dx),
707
+ scrollY: Math.max(0, current.scrollY - dy)
708
+ }));
709
+ return;
710
+ }
711
+ const viewport = registry.get(atoms.viewport);
712
+ const coord = hitTestCell(viewport, headers, local(event));
713
+ if (!coord) {
714
+ return;
715
+ }
716
+ if (drag.kind === "toggle") {
717
+ const key = cellKey(coord.col, coord.row);
718
+ if (!drag.touched.has(key)) {
719
+ drag.touched.add(key);
720
+ handlers.onCellToggle?.(coord, drag.mode);
721
+ }
722
+ } else if (drag.kind === "select") {
723
+ registry.set(atoms.selection, {
724
+ range: {
725
+ col0: drag.origin.col,
726
+ row0: drag.origin.row,
727
+ col1: coord.col,
728
+ row1: coord.row
729
+ }
730
+ });
731
+ }
732
+ };
733
+ const releaseCapture = (event) => {
734
+ if (element.hasPointerCapture(event.pointerId)) {
735
+ element.releasePointerCapture(event.pointerId);
736
+ }
737
+ };
738
+ const onPointerUp = (event) => {
739
+ if (!drag) {
740
+ return;
741
+ }
742
+ if (drag.kind === "select") {
743
+ const range = registry.get(atoms.selection).range;
744
+ if (range) {
745
+ handlers.onSelectionCommit?.(range);
746
+ }
747
+ }
748
+ drag = null;
749
+ releaseCapture(event);
750
+ };
751
+ const onPointerCancel = (event) => {
752
+ drag = null;
753
+ releaseCapture(event);
754
+ };
755
+ element.addEventListener("pointerdown", onPointerDown);
756
+ element.addEventListener("pointermove", onPointerMove);
757
+ element.addEventListener("pointerup", onPointerUp);
758
+ element.addEventListener("pointercancel", onPointerCancel);
759
+ return () => {
760
+ element.removeEventListener("pointerdown", onPointerDown);
761
+ element.removeEventListener("pointermove", onPointerMove);
762
+ element.removeEventListener("pointerup", onPointerUp);
763
+ element.removeEventListener("pointercancel", onPointerCancel);
764
+ };
765
+ };
766
+ var toggleCell = (registry, atoms, coord, factory, mode = "toggle") => {
767
+ registry.update(atoms.cells, (current) => {
768
+ const next = new Map(current);
769
+ const key = cellKey(coord.col, coord.row);
770
+ const exists = next.has(key);
771
+ if (mode === "set" || mode === "toggle" && !exists) {
772
+ next.set(key, factory(coord));
773
+ } else if (mode === "unset" || mode === "toggle" && exists) {
774
+ next.delete(key);
775
+ }
776
+ return next;
777
+ });
778
+ };
779
+
780
+ // src/components/CellGrid/input/wheel.ts
781
+ var MIN_ZOOM = 0.25;
782
+ var MAX_ZOOM = 8;
783
+ var attachWheelHandlers = (element, { registry, atoms, headers }) => {
784
+ const onWheel = (event) => {
785
+ if (event.ctrlKey || event.metaKey) {
786
+ event.preventDefault();
787
+ const rect = element.getBoundingClientRect();
788
+ const x = event.clientX - rect.left;
789
+ const factor = Math.exp(-event.deltaY / 200);
790
+ registry.update(atoms.viewport, (current2) => {
791
+ const nextZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, current2.zoomX * factor));
792
+ if (nextZoom === current2.zoomX) {
793
+ return current2;
794
+ }
795
+ const w = cellWidth(current2);
796
+ const worldX = (x - headers.left + current2.scrollX) / w;
797
+ const nextW = current2.baseCellWidth * nextZoom;
798
+ const nextScrollX2 = Math.max(0, worldX * nextW - (x - headers.left));
799
+ return {
800
+ ...current2,
801
+ zoomX: nextZoom,
802
+ scrollX: nextScrollX2
803
+ };
804
+ });
805
+ return;
806
+ }
807
+ const dx = event.shiftKey ? event.deltaY : event.deltaX;
808
+ const dy = event.shiftKey ? 0 : event.deltaY;
809
+ const current = registry.get(atoms.viewport);
810
+ const nextScrollX = Math.max(0, current.scrollX + dx);
811
+ const nextScrollY = Math.max(0, current.scrollY + dy);
812
+ if (nextScrollX === current.scrollX && nextScrollY === current.scrollY) {
813
+ return;
814
+ }
815
+ event.preventDefault();
816
+ registry.set(atoms.viewport, {
817
+ ...current,
818
+ scrollX: nextScrollX,
819
+ scrollY: nextScrollY
820
+ });
821
+ };
822
+ element.addEventListener("wheel", onWheel, {
823
+ passive: false
824
+ });
825
+ return () => element.removeEventListener("wheel", onWheel);
826
+ };
827
+
828
+ // src/components/CellGrid/render/overlay-layer.ts
829
+ var drawOverlay = ({ ctx, size, viewport, headers, selection, playhead, style }) => {
830
+ ctx.clearRect(0, 0, size.width, size.height);
831
+ ctx.save();
832
+ ctx.beginPath();
833
+ ctx.rect(headers.left, headers.top, size.width - headers.left, size.height - headers.top);
834
+ ctx.clip();
835
+ if (selection.range) {
836
+ const { col0, row0, col1, row1 } = selection.range;
837
+ const minCol = Math.min(col0, col1);
838
+ const maxCol = Math.max(col0, col1);
839
+ const minRow = Math.min(row0, row1);
840
+ const maxRow = Math.max(row0, row1);
841
+ const tl = worldToScreen(viewport, headers, {
842
+ col: minCol,
843
+ row: minRow
844
+ });
845
+ const br = worldToScreen(viewport, headers, {
846
+ col: maxCol + 1,
847
+ row: maxRow + 1
848
+ });
849
+ ctx.fillStyle = style.selectionFill;
850
+ ctx.fillRect(tl.x, tl.y, br.x - tl.x, br.y - tl.y);
851
+ ctx.strokeStyle = style.selectionStroke;
852
+ ctx.setLineDash([
853
+ 4,
854
+ 3
855
+ ]);
856
+ ctx.lineWidth = 1;
857
+ ctx.strokeRect(tl.x + 0.5, tl.y + 0.5, br.x - tl.x - 1, br.y - tl.y - 1);
858
+ ctx.setLineDash([]);
859
+ }
860
+ if (playhead !== null) {
861
+ const w = cellWidth(viewport);
862
+ const x = headers.left + playhead * w - viewport.scrollX;
863
+ if (x >= headers.left && x <= size.width) {
864
+ ctx.strokeStyle = style.playhead;
865
+ ctx.lineWidth = 2;
866
+ ctx.beginPath();
867
+ ctx.moveTo(x, headers.top);
868
+ ctx.lineTo(x, size.height);
869
+ ctx.stroke();
870
+ }
871
+ }
872
+ ctx.restore();
873
+ };
874
+
875
+ // src/components/CellGrid/render/static-layer.ts
876
+ var drawCells = ({ ctx, size, viewport, headers, rows, cells, renderCell, style }) => {
877
+ ctx.clearRect(0, 0, size.width, size.height);
878
+ if (style.background) {
879
+ ctx.fillStyle = style.background;
880
+ ctx.fillRect(0, 0, size.width, size.height);
881
+ }
882
+ const range = visibleCellRange(viewport, headers, size);
883
+ const w = cellWidth(viewport);
884
+ const h = viewport.cellHeight;
885
+ if (style.rowBand) {
886
+ ctx.fillStyle = style.rowBand;
887
+ for (let row = range.minRow; row <= Math.min(range.maxRow, rows.length - 1); row++) {
888
+ if (row % 2 === 0) {
889
+ continue;
890
+ }
891
+ const y = headers.top + row * h - viewport.scrollY;
892
+ ctx.fillRect(headers.left, y, size.width - headers.left, h);
893
+ }
894
+ }
895
+ ctx.strokeStyle = style.gridLine;
896
+ ctx.lineWidth = 1;
897
+ ctx.beginPath();
898
+ for (let col = range.minCol; col <= range.maxCol + 1; col++) {
899
+ const x = Math.floor(headers.left + col * w - viewport.scrollX) + 0.5;
900
+ if (x < headers.left) {
901
+ continue;
902
+ }
903
+ ctx.moveTo(x, headers.top);
904
+ ctx.lineTo(x, size.height);
905
+ }
906
+ for (let row = range.minRow; row <= Math.min(range.maxRow + 1, rows.length); row++) {
907
+ const y = Math.floor(headers.top + row * h - viewport.scrollY) + 0.5;
908
+ if (y < headers.top) {
909
+ continue;
910
+ }
911
+ ctx.moveTo(headers.left, y);
912
+ ctx.lineTo(size.width, y);
913
+ }
914
+ ctx.stroke();
915
+ ctx.save();
916
+ ctx.beginPath();
917
+ ctx.rect(headers.left, headers.top, size.width - headers.left, size.height - headers.top);
918
+ ctx.clip();
919
+ for (const cell of visibleCells(cells, range)) {
920
+ if (cell.row >= rows.length) {
921
+ continue;
922
+ }
923
+ const rect = worldToScreen(viewport, headers, cell);
924
+ renderCell({
925
+ ctx,
926
+ ...rect,
927
+ cell
928
+ });
929
+ }
930
+ ctx.restore();
931
+ };
932
+
933
+ // src/components/CellGrid/CellGrid.tsx
934
+ var defaultHeaders = {
935
+ left: 80,
936
+ top: 24
937
+ };
938
+ var defaultStaticStyle = {
939
+ gridLine: "rgba(128,128,128,0.25)",
940
+ rowBand: "rgba(128,128,128,0.06)"
941
+ };
942
+ var defaultOverlayStyle = {
943
+ playhead: "rgb(220, 38, 38)",
944
+ selectionFill: "rgba(59, 130, 246, 0.15)",
945
+ selectionStroke: "rgb(59, 130, 246)"
946
+ };
947
+ var setupCanvas = (canvas, width, height) => {
948
+ const dpr = window.devicePixelRatio || 1;
949
+ canvas.width = Math.max(1, Math.floor(width * dpr));
950
+ canvas.height = Math.max(1, Math.floor(height * dpr));
951
+ canvas.style.width = `${width}px`;
952
+ canvas.style.height = `${height}px`;
953
+ const ctx = canvas.getContext("2d");
954
+ if (!ctx) {
955
+ return null;
956
+ }
957
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
958
+ return ctx;
959
+ };
960
+ var CellGrid = ({ atoms, rows, renderCell, headers: headersProp, staticStyle: staticStyleProp, overlayStyle: overlayStyleProp, classNames, onCellToggle, onSelectionCommit }) => {
961
+ const registry = useContext2(RegistryContext);
962
+ const headers = useMemo3(() => {
963
+ if (headersProp === false) {
964
+ return {
965
+ left: 0,
966
+ top: 0
967
+ };
968
+ }
969
+ return {
970
+ ...defaultHeaders,
971
+ ...headersProp ?? {}
972
+ };
973
+ }, [
974
+ headersProp
975
+ ]);
976
+ const staticStyle = useMemo3(() => ({
977
+ ...defaultStaticStyle,
978
+ ...staticStyleProp ?? {}
979
+ }), [
980
+ staticStyleProp
981
+ ]);
982
+ const overlayStyle = useMemo3(() => ({
983
+ ...defaultOverlayStyle,
984
+ ...overlayStyleProp ?? {}
985
+ }), [
986
+ overlayStyleProp
987
+ ]);
988
+ const { ref: containerRef, width = 0, height = 0 } = useResizeDetector2();
989
+ const staticCanvasRef = useRef2(null);
990
+ const overlayCanvasRef = useRef2(null);
991
+ const overlayInputRef = useRef2(null);
992
+ const [staticCtx, setStaticCtx] = useState2(null);
993
+ const [overlayCtx, setOverlayCtx] = useState2(null);
994
+ const [viewportState, setViewportState] = useState2(() => registry.get(atoms.viewport));
995
+ useEffect4(() => {
996
+ if (!width || !height) {
997
+ return;
998
+ }
999
+ if (staticCanvasRef.current) {
1000
+ const ctx = setupCanvas(staticCanvasRef.current, width, height);
1001
+ setStaticCtx(ctx);
1002
+ }
1003
+ if (overlayCanvasRef.current) {
1004
+ const ctx = setupCanvas(overlayCanvasRef.current, width, height);
1005
+ setOverlayCtx(ctx);
1006
+ }
1007
+ }, [
1008
+ width,
1009
+ height
1010
+ ]);
1011
+ useEffect4(() => registry.subscribe(atoms.viewport, (next) => setViewportState(next)), [
1012
+ registry,
1013
+ atoms.viewport
1014
+ ]);
1015
+ useEffect4(() => {
1016
+ if (!staticCtx || !width || !height) {
1017
+ return;
1018
+ }
1019
+ let raf = null;
1020
+ const schedule = () => {
1021
+ if (raf !== null) {
1022
+ return;
1023
+ }
1024
+ raf = requestAnimationFrame(() => {
1025
+ raf = null;
1026
+ drawCells({
1027
+ ctx: staticCtx,
1028
+ size: {
1029
+ width,
1030
+ height
1031
+ },
1032
+ viewport: registry.get(atoms.viewport),
1033
+ headers,
1034
+ rows,
1035
+ cells: registry.get(atoms.cells),
1036
+ renderCell,
1037
+ style: staticStyle
1038
+ });
1039
+ });
1040
+ };
1041
+ schedule();
1042
+ const unsubCells = registry.subscribe(atoms.cells, schedule);
1043
+ const unsubViewport = registry.subscribe(atoms.viewport, schedule);
1044
+ return () => {
1045
+ if (raf !== null) {
1046
+ cancelAnimationFrame(raf);
1047
+ }
1048
+ unsubCells();
1049
+ unsubViewport();
1050
+ };
1051
+ }, [
1052
+ staticCtx,
1053
+ width,
1054
+ height,
1055
+ registry,
1056
+ atoms.cells,
1057
+ atoms.viewport,
1058
+ headers,
1059
+ rows,
1060
+ renderCell,
1061
+ staticStyle
1062
+ ]);
1063
+ useEffect4(() => {
1064
+ if (!overlayCtx || !width || !height) {
1065
+ return;
1066
+ }
1067
+ let raf = null;
1068
+ let stopped = false;
1069
+ const paint = () => {
1070
+ drawOverlay({
1071
+ ctx: overlayCtx,
1072
+ size: {
1073
+ width,
1074
+ height
1075
+ },
1076
+ viewport: registry.get(atoms.viewport),
1077
+ headers,
1078
+ selection: registry.get(atoms.selection),
1079
+ playhead: registry.get(atoms.playhead),
1080
+ style: overlayStyle
1081
+ });
1082
+ };
1083
+ const isAnimating = () => registry.get(atoms.playhead) !== null;
1084
+ const loop = () => {
1085
+ if (stopped) {
1086
+ return;
1087
+ }
1088
+ paint();
1089
+ if (isAnimating()) {
1090
+ raf = requestAnimationFrame(loop);
1091
+ } else {
1092
+ raf = null;
1093
+ }
1094
+ };
1095
+ const kick = () => {
1096
+ paint();
1097
+ if (raf === null && isAnimating()) {
1098
+ raf = requestAnimationFrame(loop);
1099
+ }
1100
+ };
1101
+ kick();
1102
+ const unsubSelection = registry.subscribe(atoms.selection, () => paint());
1103
+ const unsubPlayhead = registry.subscribe(atoms.playhead, kick);
1104
+ const unsubViewport = registry.subscribe(atoms.viewport, () => paint());
1105
+ return () => {
1106
+ stopped = true;
1107
+ if (raf !== null) {
1108
+ cancelAnimationFrame(raf);
1109
+ }
1110
+ unsubSelection();
1111
+ unsubPlayhead();
1112
+ unsubViewport();
1113
+ };
1114
+ }, [
1115
+ overlayCtx,
1116
+ width,
1117
+ height,
1118
+ registry,
1119
+ atoms.selection,
1120
+ atoms.playhead,
1121
+ atoms.viewport,
1122
+ headers,
1123
+ overlayStyle
1124
+ ]);
1125
+ const callbacksRef = useRef2({
1126
+ onCellToggle,
1127
+ onSelectionCommit
1128
+ });
1129
+ callbacksRef.current = {
1130
+ onCellToggle,
1131
+ onSelectionCommit
1132
+ };
1133
+ useEffect4(() => {
1134
+ const element = overlayInputRef.current;
1135
+ if (!element) {
1136
+ return;
1137
+ }
1138
+ const detachPointer = attachPointerHandlers(element, {
1139
+ registry,
1140
+ atoms,
1141
+ headers,
1142
+ handlers: {
1143
+ onCellToggle: (coord, mode) => callbacksRef.current.onCellToggle?.(coord, mode),
1144
+ onSelectionCommit: (range) => callbacksRef.current.onSelectionCommit?.(range)
1145
+ }
1146
+ });
1147
+ const detachWheel = attachWheelHandlers(element, {
1148
+ registry,
1149
+ atoms,
1150
+ headers
1151
+ });
1152
+ return () => {
1153
+ detachPointer();
1154
+ detachWheel();
1155
+ };
1156
+ }, [
1157
+ registry,
1158
+ atoms,
1159
+ headers
1160
+ ]);
1161
+ return /* @__PURE__ */ React5.createElement("div", {
1162
+ ref: containerRef,
1163
+ className: mx5("relative w-full h-full overflow-hidden bg-baseSurface", classNames)
1164
+ }, /* @__PURE__ */ React5.createElement("canvas", {
1165
+ ref: staticCanvasRef,
1166
+ className: "absolute inset-0 pointer-events-none",
1167
+ style: {
1168
+ top: -1,
1169
+ left: -1
1170
+ }
1171
+ }), /* @__PURE__ */ React5.createElement("canvas", {
1172
+ ref: overlayCanvasRef,
1173
+ className: "absolute inset-0 pointer-events-none",
1174
+ style: {
1175
+ top: -1,
1176
+ left: -1
1177
+ }
1178
+ }), /* @__PURE__ */ React5.createElement("div", {
1179
+ ref: overlayInputRef,
1180
+ className: "absolute inset-0 touch-none",
1181
+ style: {
1182
+ paddingLeft: headers.left,
1183
+ paddingTop: headers.top
1184
+ }
1185
+ }), headers.top > 0 && /* @__PURE__ */ React5.createElement(Ruler, {
1186
+ viewport: viewportState,
1187
+ headers,
1188
+ width
1189
+ }), headers.left > 0 && /* @__PURE__ */ React5.createElement(TrackHeader, {
1190
+ viewport: viewportState,
1191
+ headers,
1192
+ rows,
1193
+ height
1194
+ }), headers.top > 0 && headers.left > 0 && /* @__PURE__ */ React5.createElement("div", {
1195
+ className: "absolute top-0 left-0 border-b border-r border-neutral-200 dark:border-neutral-700 bg-baseSurface",
1196
+ style: {
1197
+ width: headers.left,
1198
+ height: headers.top
1199
+ }
1200
+ }));
1201
+ };
1202
+
1203
+ // src/components/CellGrid/state/atoms.ts
1204
+ import { Atom } from "@effect-atom/atom-react";
1205
+ var defaultViewport = (options = {}) => ({
1206
+ scrollX: 0,
1207
+ scrollY: 0,
1208
+ baseCellWidth: options.cellWidth ?? 24,
1209
+ cellHeight: options.cellHeight ?? 24,
1210
+ zoomX: 1
1211
+ });
1212
+ var createCellGridAtoms = (options = {}) => ({
1213
+ cells: Atom.keepAlive(Atom.make(/* @__PURE__ */ new Map())),
1214
+ viewport: Atom.keepAlive(Atom.make(defaultViewport(options))),
1215
+ selection: Atom.keepAlive(Atom.make({
1216
+ range: null
1217
+ })),
1218
+ playhead: Atom.keepAlive(Atom.make(null)),
1219
+ tool: Atom.keepAlive(Atom.make("toggle"))
1220
+ });
1221
+
1222
+ // src/components/FPS.tsx
1223
+ import React6, { useEffect as useEffect5, useReducer, useRef as useRef3 } from "react";
1224
+ import { mx as mx6 } from "@dxos/ui-theme";
483
1225
  var SEC = 1e3;
484
1226
  var FPS = ({ classNames, width = 60, height = 30, bar = "bg-cyan-500" }) => {
485
1227
  const [{ fps, max, len }, dispatch] = useReducer((state) => {
@@ -512,12 +1254,12 @@ var FPS = ({ classNames, width = 60, height = 30, bar = "bg-cyan-500" }) => {
512
1254
  frames: 0,
513
1255
  prevTime: Date.now()
514
1256
  });
515
- const requestRef = useRef2(null);
1257
+ const requestRef = useRef3(null);
516
1258
  const tick = () => {
517
1259
  dispatch();
518
1260
  requestRef.current = requestAnimationFrame(tick);
519
1261
  };
520
- useEffect4(() => {
1262
+ useEffect5(() => {
521
1263
  requestRef.current = requestAnimationFrame(tick);
522
1264
  return () => {
523
1265
  if (requestRef.current) {
@@ -525,17 +1267,17 @@ var FPS = ({ classNames, width = 60, height = 30, bar = "bg-cyan-500" }) => {
525
1267
  }
526
1268
  };
527
1269
  }, []);
528
- return /* @__PURE__ */ React3.createElement("div", {
1270
+ return /* @__PURE__ */ React6.createElement("div", {
529
1271
  style: {
530
1272
  width: width + 6
531
1273
  },
532
- 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)
533
- }, /* @__PURE__ */ React3.createElement("div", null, fps[len - 1], " FPS"), /* @__PURE__ */ React3.createElement("div", {
1274
+ className: mx6("relative flex flex-col p-0.5", "bg-base-surface text-xs text-subdued font-thin pointer-events-none border border-separator", classNames)
1275
+ }, /* @__PURE__ */ React6.createElement("div", null, fps[len - 1], " FPS"), /* @__PURE__ */ React6.createElement("div", {
534
1276
  className: "w-full relative",
535
1277
  style: {
536
1278
  height
537
1279
  }
538
- }, fps.map((frame, i) => /* @__PURE__ */ React3.createElement("div", {
1280
+ }, fps.map((frame, i) => /* @__PURE__ */ React6.createElement("div", {
539
1281
  key: `fps-${i}`,
540
1282
  className: bar,
541
1283
  style: {
@@ -549,9 +1291,9 @@ var FPS = ({ classNames, width = 60, height = 30, bar = "bg-cyan-500" }) => {
549
1291
  };
550
1292
 
551
1293
  // src/components/Grid/Grid.tsx
552
- import React4, { forwardRef as forwardRef2, useId, useMemo as useMemo2 } from "react";
1294
+ import React7, { forwardRef as forwardRef2, useId, useMemo as useMemo4 } from "react";
553
1295
  import { useForwardedRef } from "@dxos/react-ui";
554
- import { mx as mx4 } from "@dxos/ui-theme";
1296
+ import { mx as mx7 } from "@dxos/ui-theme";
555
1297
  var gridRatios = [
556
1298
  1 / 4,
557
1299
  1,
@@ -566,7 +1308,7 @@ var defaultOffset = {
566
1308
  var createId = (parent, grid) => `dx-canvas-grid-${parent}-${grid}`;
567
1309
  var Grid = (props) => {
568
1310
  const { scale, offset } = useCanvasContext();
569
- return /* @__PURE__ */ React4.createElement(GridComponent, {
1311
+ return /* @__PURE__ */ React7.createElement(GridComponent, {
570
1312
  ...props,
571
1313
  scale,
572
1314
  offset
@@ -576,35 +1318,35 @@ var GridComponent = /* @__PURE__ */ forwardRef2(({ size: gridSize = defaultGridS
576
1318
  const svgRef = useForwardedRef(forwardedRef);
577
1319
  const { width = 0, height = 0 } = svgRef.current?.getBoundingClientRect() ?? {};
578
1320
  const instanceId = useId();
579
- const grids = useMemo2(() => gridRatios.map((ratio) => ({
1321
+ const grids = useMemo4(() => gridRatios.map((ratio) => ({
580
1322
  id: ratio,
581
1323
  size: ratio * gridSize * scale
582
1324
  })).filter(({ size }) => size >= gridSize && size <= 128), [
583
1325
  gridSize,
584
1326
  scale
585
1327
  ]);
586
- return /* @__PURE__ */ React4.createElement("svg", {
1328
+ return /* @__PURE__ */ React7.createElement("svg", {
587
1329
  ...testId("dx-canvas-grid"),
588
1330
  ref: svgRef,
589
- className: mx4("dx-fullscreen pointer-events-none touch-none select-none", "stroke-neutral-500", classNames)
590
- }, /* @__PURE__ */ React4.createElement("defs", null, grids.map(({ id, size }) => /* @__PURE__ */ React4.createElement(GridPattern, {
1331
+ className: mx7("dx-fullscreen pointer-events-none touch-none select-none", "stroke-neutral-500", classNames)
1332
+ }, /* @__PURE__ */ React7.createElement("defs", null, grids.map(({ id, size }) => /* @__PURE__ */ React7.createElement(GridPattern, {
591
1333
  key: id,
592
1334
  id: createId(instanceId, id),
593
1335
  offset,
594
1336
  size
595
- }))), showAxes && /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement("line", {
1337
+ }))), showAxes && /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("line", {
596
1338
  x1: 0,
597
1339
  y1: offset.y,
598
1340
  x2: width,
599
1341
  y2: offset.y,
600
1342
  className: "stroke-neutral-500 opacity-40"
601
- }), /* @__PURE__ */ React4.createElement("line", {
1343
+ }), /* @__PURE__ */ React7.createElement("line", {
602
1344
  x1: offset.x,
603
1345
  y1: 0,
604
1346
  x2: offset.x,
605
1347
  y2: height,
606
1348
  className: "stroke-neutral-500 opacity-40"
607
- })), /* @__PURE__ */ React4.createElement("g", null, grids.map(({ id }, i) => /* @__PURE__ */ React4.createElement("rect", {
1349
+ })), /* @__PURE__ */ React7.createElement("g", null, grids.map(({ id }, i) => /* @__PURE__ */ React7.createElement("rect", {
608
1350
  key: id,
609
1351
  opacity: 0.1 + i * 0.05,
610
1352
  fill: `url(#${createId(instanceId, id)})`,
@@ -628,6 +1370,7 @@ export {
628
1370
  Arrow,
629
1371
  Canvas,
630
1372
  CanvasContext,
1373
+ CellGrid,
631
1374
  DATA_TEST_ID,
632
1375
  Dimension,
633
1376
  FPS,
@@ -639,15 +1382,31 @@ export {
639
1382
  Point,
640
1383
  ProjectionMapper,
641
1384
  Rect,
1385
+ Ruler,
1386
+ TrackHeader,
1387
+ attachPointerHandlers,
1388
+ attachWheelHandlers,
1389
+ cellKey,
1390
+ cellWidth,
1391
+ createCellGridAtoms,
642
1392
  createPath,
643
1393
  defaultOrigin,
1394
+ defaultViewport,
1395
+ drawCells,
1396
+ drawOverlay,
644
1397
  getRelativePoint,
645
1398
  getZoomTransform,
1399
+ hitTestCell,
646
1400
  inspectElement,
1401
+ screenToWorld,
647
1402
  testId,
1403
+ toggleCell,
648
1404
  useCanvasContext,
649
1405
  useDrag,
650
1406
  useWheel,
1407
+ visibleCellRange,
1408
+ visibleCells,
1409
+ worldToScreen,
651
1410
  zoomInPlace,
652
1411
  zoomTo
653
1412
  };