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