@dxos/react-ui-canvas 0.8.4-main.a4bbb77 → 0.8.4-main.abd8ff62ef

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 (38) hide show
  1. package/dist/lib/browser/index.mjs +380 -395
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +380 -395
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/Canvas/Canvas.d.ts +2 -2
  8. package/dist/types/src/components/Canvas/Canvas.stories.d.ts.map +1 -1
  9. package/dist/types/src/components/FPS.d.ts.map +1 -1
  10. package/dist/types/src/components/Grid/Grid.d.ts +2 -2
  11. package/dist/types/src/components/Grid/Grid.d.ts.map +1 -1
  12. package/dist/types/src/components/Grid/Grid.stories.d.ts +1 -1
  13. package/dist/types/src/components/Grid/Grid.stories.d.ts.map +1 -1
  14. package/dist/types/src/hooks/index.d.ts +1 -0
  15. package/dist/types/src/hooks/index.d.ts.map +1 -1
  16. package/dist/types/src/hooks/projection.d.ts.map +1 -1
  17. package/dist/types/src/hooks/useDrag.d.ts +6 -0
  18. package/dist/types/src/hooks/useDrag.d.ts.map +1 -0
  19. package/dist/types/src/hooks/useWheel.d.ts.map +1 -1
  20. package/dist/types/src/types.d.ts +1 -1
  21. package/dist/types/src/types.d.ts.map +1 -1
  22. package/dist/types/src/util/svg.d.ts +1 -1
  23. package/dist/types/src/util/svg.d.ts.map +1 -1
  24. package/dist/types/src/util/svg.stories.d.ts.map +1 -1
  25. package/dist/types/src/util/util.d.ts.map +1 -1
  26. package/dist/types/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +24 -25
  28. package/src/components/Canvas/Canvas.stories.tsx +6 -6
  29. package/src/components/Canvas/Canvas.tsx +3 -3
  30. package/src/components/FPS.tsx +2 -2
  31. package/src/components/Grid/Grid.stories.tsx +3 -4
  32. package/src/components/Grid/Grid.tsx +13 -15
  33. package/src/hooks/index.ts +1 -0
  34. package/src/hooks/useDrag.tsx +96 -0
  35. package/src/hooks/useWheel.tsx +0 -28
  36. package/src/types.ts +1 -1
  37. package/src/util/svg.stories.tsx +2 -2
  38. package/src/util/svg.tsx +1 -1
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-canvas",
3
- "version": "0.8.4-main.a4bbb77",
3
+ "version": "0.8.4-main.abd8ff62ef",
4
4
  "description": "A canvas component.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dxos/dxos"
10
+ },
7
11
  "license": "MIT",
8
12
  "author": "DXOS.org",
9
13
  "type": "module",
@@ -16,45 +20,40 @@
16
20
  }
17
21
  },
18
22
  "types": "dist/types/src/index.d.ts",
19
- "typesVersions": {
20
- "*": {}
21
- },
22
23
  "files": [
23
24
  "dist",
24
25
  "src"
25
26
  ],
26
27
  "dependencies": {
27
- "@preact-signals/safe-react": "^0.9.0",
28
- "@preact/signals-core": "^1.12.1",
29
28
  "@radix-ui/react-context": "1.1.1",
30
29
  "bind-event-listener": "^3.0.0",
31
30
  "d3": "^7.9.0",
32
31
  "react-resize-detector": "^11.0.1",
33
32
  "transformation-matrix": "^2.16.1",
34
- "@dxos/debug": "0.8.4-main.a4bbb77",
35
- "@dxos/invariant": "0.8.4-main.a4bbb77",
36
- "@dxos/log": "0.8.4-main.a4bbb77",
37
- "@dxos/util": "0.8.4-main.a4bbb77"
33
+ "@dxos/debug": "0.8.4-main.abd8ff62ef",
34
+ "@dxos/invariant": "0.8.4-main.abd8ff62ef",
35
+ "@dxos/util": "0.8.4-main.abd8ff62ef",
36
+ "@dxos/log": "0.8.4-main.abd8ff62ef"
38
37
  },
39
38
  "devDependencies": {
40
39
  "@types/d3": "^7.4.3",
41
- "@types/react": "~19.2.0",
42
- "@types/react-dom": "~19.2.0",
43
- "effect": "3.18.3",
44
- "react": "~19.2.0",
45
- "react-dom": "~19.2.0",
46
- "vite": "7.1.9",
47
- "@dxos/random": "0.8.4-main.a4bbb77",
48
- "@dxos/react-ui": "0.8.4-main.a4bbb77",
49
- "@dxos/react-ui-theme": "0.8.4-main.a4bbb77",
50
- "@dxos/storybook-utils": "0.8.4-main.a4bbb77"
40
+ "@types/react": "~19.2.7",
41
+ "@types/react-dom": "~19.2.3",
42
+ "effect": "3.20.0",
43
+ "react": "~19.2.3",
44
+ "react-dom": "~19.2.3",
45
+ "vite": "^8.0.10",
46
+ "@dxos/random": "0.8.4-main.abd8ff62ef",
47
+ "@dxos/ui-theme": "0.8.4-main.abd8ff62ef",
48
+ "@dxos/react-ui": "0.8.4-main.abd8ff62ef",
49
+ "@dxos/storybook-utils": "0.8.4-main.abd8ff62ef"
51
50
  },
52
51
  "peerDependencies": {
53
- "effect": "3.13.3",
54
- "react": "^19.0.0",
55
- "react-dom": "^19.0.0",
56
- "@dxos/react-ui": "0.8.4-main.a4bbb77",
57
- "@dxos/react-ui-theme": "0.8.4-main.a4bbb77"
52
+ "effect": "3.20.0",
53
+ "react": "~19.2.3",
54
+ "react-dom": "~19.2.3",
55
+ "@dxos/ui-theme": "0.8.4-main.abd8ff62ef",
56
+ "@dxos/react-ui": "0.8.4-main.abd8ff62ef"
58
57
  },
59
58
  "publishConfig": {
60
59
  "access": "public"
@@ -5,13 +5,12 @@
5
5
  import type { Meta, StoryObj } from '@storybook/react-vite';
6
6
  import React from 'react';
7
7
 
8
- import { withTheme } from '@dxos/react-ui/testing';
8
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
9
9
 
10
- import { useCanvasContext, useWheel } from '../../hooks';
10
+ import { useCanvasContext, useDrag, useWheel } from '../../hooks';
11
11
  import { type Point } from '../../types';
12
12
  import { testId } from '../../util';
13
13
  import { Grid, type GridProps } from '../Grid';
14
-
15
14
  import { Canvas } from './Canvas';
16
15
 
17
16
  const size = 128;
@@ -32,7 +31,7 @@ const DefaultStory = (props: GridProps) => {
32
31
 
33
32
  const TwoCanvases = (props: GridProps) => {
34
33
  return (
35
- <div className='grid grid-cols-2 gap-2 w-full h-full'>
34
+ <div className='grid grid-cols-2 gap-2 h-full w-full'>
36
35
  <div className='h-full relative'>
37
36
  <Canvas>
38
37
  <Grid {...props} />
@@ -51,6 +50,7 @@ const TwoCanvases = (props: GridProps) => {
51
50
 
52
51
  const Content = () => {
53
52
  useWheel();
53
+ useDrag();
54
54
  return (
55
55
  <div>
56
56
  {points.map(({ x, y }, i) => (
@@ -91,7 +91,7 @@ const meta = {
91
91
  title: 'ui/react-ui-canvas/Canvas',
92
92
  component: Grid,
93
93
  render: DefaultStory,
94
- decorators: [withTheme],
94
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
95
95
  parameters: {
96
96
  layout: 'fullscreen',
97
97
  },
@@ -106,6 +106,6 @@ export const Default: Story = {
106
106
  };
107
107
 
108
108
  export const SideBySide: Story = {
109
- args: { size: 16 },
110
109
  render: TwoCanvases,
110
+ args: { size: 16 },
111
111
  };
@@ -15,7 +15,7 @@ import React, {
15
15
  import { useResizeDetector } from 'react-resize-detector';
16
16
 
17
17
  import { type ThemedClassName } from '@dxos/react-ui';
18
- import { mx } from '@dxos/react-ui-theme';
18
+ import { mx } from '@dxos/ui-theme';
19
19
 
20
20
  import { CanvasContext, ProjectionMapper, type ProjectionState, defaultOrigin } from '../../hooks';
21
21
 
@@ -30,7 +30,7 @@ export type CanvasProps = ThemedClassName<PropsWithChildren<Partial<ProjectionSt
30
30
  * Manages CSS projection.
31
31
  */
32
32
  export const Canvas = forwardRef<CanvasController, CanvasProps>(
33
- ({ children, classNames, scale: _scale = 1, offset: _offset = defaultOrigin, ...props }, forwardedRef) => {
33
+ ({ children, classNames, scale: scaleProp = 1, offset: offsetProp = defaultOrigin, ...props }, forwardedRef) => {
34
34
  // Size.
35
35
  const { ref, width = 0, height = 0 } = useResizeDetector();
36
36
 
@@ -38,7 +38,7 @@ export const Canvas = forwardRef<CanvasController, CanvasProps>(
38
38
  const [ready, setReady] = useState(false);
39
39
 
40
40
  // Projection.
41
- const [{ scale, offset }, setProjection] = useState<ProjectionState>({ scale: _scale, offset: _offset });
41
+ const [{ scale, offset }, setProjection] = useState<ProjectionState>({ scale: scaleProp, offset: offsetProp });
42
42
  useEffect(() => {
43
43
  if (width && height && offset === defaultOrigin) {
44
44
  setProjection({ scale, offset: { x: width / 2, y: height / 2 } });
@@ -6,7 +6,7 @@
6
6
  import React, { useEffect, useReducer, useRef } from 'react';
7
7
 
8
8
  import { type ThemedClassName } from '@dxos/react-ui';
9
- import { mx } from '@dxos/react-ui-theme';
9
+ import { mx } from '@dxos/ui-theme';
10
10
 
11
11
  export type FPSProps = ThemedClassName<{
12
12
  width?: number;
@@ -73,7 +73,7 @@ export const FPS = ({ classNames, width = 60, height = 30, bar = 'bg-cyan-500' }
73
73
  style={{ width: width + 6 }}
74
74
  className={mx(
75
75
  'relative flex flex-col p-0.5',
76
- 'bg-baseSurface text-xs text-subdued font-thin pointer-events-none border border-separator',
76
+ 'bg-base-surface text-xs text-subdued font-thin pointer-events-none border border-separator',
77
77
  classNames,
78
78
  )}
79
79
  >
@@ -5,10 +5,9 @@
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import React, { useRef, useState } from 'react';
7
7
 
8
- import { withTheme } from '@dxos/react-ui/testing';
8
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
9
9
 
10
10
  import { type ProjectionState } from '../../hooks';
11
-
12
11
  import { GridComponent, type GridProps } from './Grid';
13
12
 
14
13
  const DefaultStory = (props: GridProps) => {
@@ -16,7 +15,7 @@ const DefaultStory = (props: GridProps) => {
16
15
  const [{ scale, offset }] = useState<ProjectionState>({ scale: 1, offset: { x: 0, y: 0 } });
17
16
 
18
17
  return (
19
- <div ref={ref} className='grow'>
18
+ <div role='none' ref={ref} className='grow'>
20
19
  <GridComponent scale={scale} offset={offset} {...props} />
21
20
  </div>
22
21
  );
@@ -26,7 +25,7 @@ const meta = {
26
25
  title: 'ui/react-ui-canvas/Grid',
27
26
  component: GridComponent,
28
27
  render: DefaultStory,
29
- decorators: [withTheme],
28
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
30
29
  parameters: {
31
30
  layout: 'fullscreen',
32
31
  },
@@ -5,7 +5,7 @@
5
5
  import React, { forwardRef, useId, useMemo } from 'react';
6
6
 
7
7
  import { type ThemedClassName, useForwardedRef } from '@dxos/react-ui';
8
- import { mx } from '@dxos/react-ui-theme';
8
+ import { mx } from '@dxos/ui-theme';
9
9
 
10
10
  import { useCanvasContext } from '../../hooks';
11
11
  import { type Point } from '../../types';
@@ -18,6 +18,8 @@ const defaultOffset: Point = { x: 0, y: 0 };
18
18
 
19
19
  const createId = (parent: string, grid: number) => `dx-canvas-grid-${parent}-${grid}`;
20
20
 
21
+ // TODO(burdon): Click to drag.
22
+
21
23
  export type GridProps = ThemedClassName<{
22
24
  size?: number;
23
25
  scale?: number;
@@ -25,32 +27,34 @@ export type GridProps = ThemedClassName<{
25
27
  showAxes?: boolean;
26
28
  }>;
27
29
 
30
+ // TODO(burdon): Use id of parent canvas.
31
+ export const Grid = (props: GridProps) => {
32
+ const { scale, offset } = useCanvasContext();
33
+ return <GridComponent {...props} scale={scale} offset={offset} />;
34
+ };
35
+
28
36
  export const GridComponent = forwardRef<SVGSVGElement, GridProps>(
29
37
  (
30
38
  { size: gridSize = defaultGridSize, scale = 1, offset = defaultOffset, showAxes = true, classNames },
31
39
  forwardedRef,
32
40
  ) => {
33
41
  const svgRef = useForwardedRef(forwardedRef);
42
+ const { width = 0, height = 0 } = svgRef.current?.getBoundingClientRect() ?? {};
43
+
34
44
  const instanceId = useId();
35
45
  const grids = useMemo(
36
46
  () =>
37
47
  gridRatios
38
48
  .map((ratio) => ({ id: ratio, size: ratio * gridSize * scale }))
39
- .filter(({ size }) => size >= gridSize && size <= 256),
49
+ .filter(({ size }) => size >= gridSize && size <= 128),
40
50
  [gridSize, scale],
41
51
  );
42
52
 
43
- const { width = 0, height = 0 } = svgRef.current?.getBoundingClientRect() ?? {};
44
-
45
53
  return (
46
54
  <svg
47
55
  {...testId('dx-canvas-grid')}
48
56
  ref={svgRef}
49
- className={mx(
50
- 'absolute inset-0 w-full h-full pointer-events-none touch-none select-none',
51
- 'stroke-neutral-500',
52
- classNames,
53
- )}
57
+ className={mx('dx-fullscreen pointer-events-none touch-none select-none', 'stroke-neutral-500', classNames)}
54
58
  >
55
59
  {/* NOTE: The pattern is offset so that the middle of the pattern aligns with the grid. */}
56
60
  <defs>
@@ -79,9 +83,3 @@ export const GridComponent = forwardRef<SVGSVGElement, GridProps>(
79
83
  );
80
84
  },
81
85
  );
82
-
83
- // TODO(burdon): Use id of parent canvas.
84
- export const Grid = (props: GridProps) => {
85
- const { scale, offset } = useCanvasContext();
86
- return <GridComponent {...props} scale={scale} offset={offset} />;
87
- };
@@ -4,4 +4,5 @@
4
4
 
5
5
  export * from './projection';
6
6
  export * from './useCanvasContext';
7
+ export * from './useDrag';
7
8
  export * from './useWheel';
@@ -0,0 +1,96 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { bind } from 'bind-event-listener';
6
+ import { useEffect, useRef } from 'react';
7
+
8
+ import { useCanvasContext } from './useCanvasContext';
9
+
10
+ export type DragOptions = {
11
+ // TODO(burdon): Add constraints?
12
+ };
13
+
14
+ /**
15
+ * Handle drag events to update the transform state (offset).
16
+ */
17
+ export const useDrag = (_options: DragOptions = {}) => {
18
+ const { root, setProjection } = useCanvasContext();
19
+
20
+ // Track drag state.
21
+ const state = useRef<{
22
+ panning: boolean;
23
+ x: number;
24
+ y: number;
25
+ }>({ panning: false, x: 0, y: 0 });
26
+
27
+ useEffect(() => {
28
+ if (!root) {
29
+ return;
30
+ }
31
+
32
+ // TODO(burdon): Use d3-drag?
33
+ return bind(root, {
34
+ type: 'pointerdown',
35
+ listener: (ev: PointerEvent) => {
36
+ // Only left click.
37
+ if (ev.button !== 0) {
38
+ return;
39
+ }
40
+
41
+ if (ev.defaultPrevented) {
42
+ return;
43
+ }
44
+
45
+ if (ev.target !== root || ev.shiftKey) {
46
+ return;
47
+ }
48
+
49
+ // Check if clicking on an interactive element?
50
+ // For now, assume if it bubbled to root, it's fair game unless prevented.
51
+
52
+ ev.preventDefault(); // Prevent text selection.
53
+ root.setPointerCapture(ev.pointerId);
54
+ state.current = { panning: true, x: ev.clientX, y: ev.clientY };
55
+
56
+ const moveUnbind = bind(root, {
57
+ type: 'pointermove',
58
+ listener: (ev: PointerEvent) => {
59
+ if (!state.current.panning) {
60
+ return;
61
+ }
62
+
63
+ // Calculate delta.
64
+ const dx = ev.clientX - state.current.x;
65
+ const dy = ev.clientY - state.current.y;
66
+
67
+ state.current.x = ev.clientX;
68
+ state.current.y = ev.clientY;
69
+
70
+ setProjection((prev) => ({
71
+ ...prev,
72
+ offset: {
73
+ x: prev.offset.x + dx,
74
+ y: prev.offset.y + dy,
75
+ },
76
+ }));
77
+ },
78
+ });
79
+
80
+ const upUnbind = bind(root, {
81
+ type: 'pointerup',
82
+ listener: (ev: PointerEvent) => {
83
+ state.current.panning = false;
84
+ root.releasePointerCapture(ev.pointerId);
85
+ moveUnbind();
86
+ upUnbind();
87
+ // Clean up lostpointercapture as well?
88
+ },
89
+ });
90
+
91
+ // Handle cancellation/lost capture just in case?
92
+ // Using setPointerCapture usually handles this well on the element.
93
+ },
94
+ });
95
+ }, [root]);
96
+ };
@@ -6,7 +6,6 @@ import { bindAll } from 'bind-event-listener';
6
6
  import { useEffect } from 'react';
7
7
 
8
8
  import { getRelativePoint } from '../util';
9
-
10
9
  import { getZoomTransform } from './projection';
11
10
  import { useCanvasContext } from './useCanvasContext';
12
11
 
@@ -34,9 +33,6 @@ export const useWheel = (options: WheelOptions = defaultOptions) => {
34
33
  options: { capture: true, passive: false },
35
34
  listener: (ev: WheelEvent) => {
36
35
  const zooming = isWheelZooming(ev);
37
- if (!hasFocus(root) && !zooming) {
38
- return;
39
- }
40
36
 
41
37
  ev.preventDefault();
42
38
  if (zooming && !options.zoom) {
@@ -82,27 +78,3 @@ const isWheelZooming = (ev: WheelEvent): boolean => {
82
78
 
83
79
  return false;
84
80
  };
85
-
86
- const hasFocus = (element: HTMLElement): boolean => {
87
- const activeElement = document.activeElement;
88
- if (!activeElement) {
89
- return false;
90
- }
91
-
92
- // Handle shadow DOM.
93
- let shadowActive = activeElement;
94
- while (shadowActive?.shadowRoot?.activeElement) {
95
- shadowActive = shadowActive.shadowRoot.activeElement;
96
- }
97
-
98
- // Check if element or any parent has focus.
99
- let current: HTMLElement | null = element;
100
- while (current) {
101
- if (current === activeElement || current === shadowActive) {
102
- return true;
103
- }
104
- current = current.parentElement;
105
- }
106
-
107
- return false;
108
- };
package/src/types.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Schema } from 'effect';
5
+ import * as Schema from 'effect/Schema';
6
6
 
7
7
  export const Point = Schema.Struct({ x: Schema.Number, y: Schema.Number });
8
8
  export const Dimension = Schema.Struct({ width: Schema.Number, height: Schema.Number });
@@ -11,7 +11,7 @@ import { Arrow, createPath } from './svg';
11
11
  import { testId } from './util';
12
12
 
13
13
  const DefaultStory = () => (
14
- <svg className='border border-neutral-500 w-[30rem] h-[400px]'>
14
+ <svg className='border border-separator w-[30rem] h-[400px]'>
15
15
  <defs>
16
16
  <Arrow id='arrow-start' classNames='fill-none stroke-red-500' dir='start' />
17
17
  <Arrow id='arrow-end' classNames='fill-none stroke-red-500' dir='end' />
@@ -32,7 +32,7 @@ const DefaultStory = () => (
32
32
  const meta = {
33
33
  title: 'ui/react-ui-canvas/svg',
34
34
  render: DefaultStory,
35
- decorators: [withTheme],
35
+ decorators: [withTheme()],
36
36
  parameters: {
37
37
  layout: 'centered',
38
38
  },
package/src/util/svg.tsx CHANGED
@@ -5,7 +5,7 @@
5
5
  import React, { type PropsWithChildren, type SVGProps } from 'react';
6
6
 
7
7
  import { type ThemedClassName } from '@dxos/react-ui';
8
- import { mx } from '@dxos/react-ui-theme';
8
+ import { mx } from '@dxos/ui-theme';
9
9
 
10
10
  import { type Dimension, type Point } from '../types';
11
11