@dxos/react-ui-canvas 0.8.3 → 0.8.4-main.1068cf700f
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.
- package/dist/lib/browser/index.mjs +388 -390
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +388 -390
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/Canvas/Canvas.d.ts +1 -1
- package/dist/types/src/components/Canvas/Canvas.d.ts.map +1 -1
- package/dist/types/src/components/Canvas/Canvas.stories.d.ts +12 -4
- package/dist/types/src/components/Canvas/Canvas.stories.d.ts.map +1 -1
- package/dist/types/src/components/Grid/Grid.d.ts +2 -2
- package/dist/types/src/components/Grid/Grid.d.ts.map +1 -1
- package/dist/types/src/components/Grid/Grid.stories.d.ts +19 -4
- package/dist/types/src/components/Grid/Grid.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +1 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/projection.d.ts +1 -1
- package/dist/types/src/hooks/projection.d.ts.map +1 -1
- package/dist/types/src/hooks/useDrag.d.ts +6 -0
- package/dist/types/src/hooks/useDrag.d.ts.map +1 -0
- package/dist/types/src/hooks/useWheel.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +1 -1
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/src/util/svg.stories.d.ts +12 -4
- package/dist/types/src/util/svg.stories.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +25 -22
- package/src/components/Canvas/Canvas.stories.tsx +17 -14
- package/src/components/Canvas/Canvas.tsx +11 -15
- package/src/components/FPS.tsx +3 -3
- package/src/components/Grid/Grid.stories.tsx +12 -10
- package/src/components/Grid/Grid.tsx +15 -13
- package/src/hooks/index.ts +1 -0
- package/src/hooks/projection.tsx +2 -2
- package/src/hooks/useDrag.tsx +96 -0
- package/src/hooks/useWheel.tsx +2 -28
- package/src/types.ts +1 -1
- package/src/util/svg.stories.tsx +9 -9
- package/src/util/svg.tsx +1 -1
- package/dist/lib/node/index.cjs +0 -691
- package/dist/lib/node/index.cjs.map +0 -7
- package/dist/lib/node/meta.json +0 -1
package/package.json
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-canvas",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4-main.1068cf700f",
|
|
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",
|
|
10
14
|
"exports": {
|
|
11
15
|
".": {
|
|
16
|
+
"source": "./src/index.ts",
|
|
12
17
|
"types": "./dist/types/src/index.d.ts",
|
|
13
18
|
"browser": "./dist/lib/browser/index.mjs",
|
|
14
19
|
"node": "./dist/lib/node-esm/index.mjs"
|
|
@@ -23,37 +28,35 @@
|
|
|
23
28
|
"src"
|
|
24
29
|
],
|
|
25
30
|
"dependencies": {
|
|
26
|
-
"@preact-signals/safe-react": "^0.9.0",
|
|
27
|
-
"@preact/signals-core": "^1.9.0",
|
|
28
31
|
"@radix-ui/react-context": "1.1.1",
|
|
29
32
|
"bind-event-listener": "^3.0.0",
|
|
30
33
|
"d3": "^7.9.0",
|
|
31
34
|
"react-resize-detector": "^11.0.1",
|
|
32
35
|
"transformation-matrix": "^2.16.1",
|
|
33
|
-
"@dxos/
|
|
34
|
-
"@dxos/
|
|
35
|
-
"@dxos/
|
|
36
|
-
"@dxos/util": "0.8.
|
|
36
|
+
"@dxos/debug": "0.8.4-main.1068cf700f",
|
|
37
|
+
"@dxos/invariant": "0.8.4-main.1068cf700f",
|
|
38
|
+
"@dxos/log": "0.8.4-main.1068cf700f",
|
|
39
|
+
"@dxos/util": "0.8.4-main.1068cf700f"
|
|
37
40
|
},
|
|
38
41
|
"devDependencies": {
|
|
39
42
|
"@types/d3": "^7.4.3",
|
|
40
|
-
"@types/react": "~
|
|
41
|
-
"@types/react-dom": "~
|
|
42
|
-
"effect": "3.
|
|
43
|
-
"react": "~
|
|
44
|
-
"react-dom": "~
|
|
45
|
-
"vite": "
|
|
46
|
-
"@dxos/random": "0.8.
|
|
47
|
-
"@dxos/react-ui
|
|
48
|
-
"@dxos/storybook-utils": "0.8.
|
|
49
|
-
"@dxos/
|
|
43
|
+
"@types/react": "~19.2.7",
|
|
44
|
+
"@types/react-dom": "~19.2.3",
|
|
45
|
+
"effect": "3.19.16",
|
|
46
|
+
"react": "~19.2.3",
|
|
47
|
+
"react-dom": "~19.2.3",
|
|
48
|
+
"vite": "7.1.9",
|
|
49
|
+
"@dxos/random": "0.8.4-main.1068cf700f",
|
|
50
|
+
"@dxos/react-ui": "0.8.4-main.1068cf700f",
|
|
51
|
+
"@dxos/storybook-utils": "0.8.4-main.1068cf700f",
|
|
52
|
+
"@dxos/ui-theme": "0.8.4-main.1068cf700f"
|
|
50
53
|
},
|
|
51
54
|
"peerDependencies": {
|
|
52
|
-
"effect": "3.
|
|
53
|
-
"react": "~
|
|
54
|
-
"react-dom": "~
|
|
55
|
-
"@dxos/react-ui
|
|
56
|
-
"@dxos/
|
|
55
|
+
"effect": "3.19.16",
|
|
56
|
+
"react": "~19.2.3",
|
|
57
|
+
"react-dom": "~19.2.3",
|
|
58
|
+
"@dxos/react-ui": "0.8.4-main.1068cf700f",
|
|
59
|
+
"@dxos/ui-theme": "0.8.4-main.1068cf700f"
|
|
57
60
|
},
|
|
58
61
|
"publishConfig": {
|
|
59
62
|
"access": "public"
|
|
@@ -2,19 +2,18 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@
|
|
6
|
-
|
|
7
|
-
import type { Meta, StoryObj } from '@storybook/react';
|
|
5
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
8
6
|
import React from 'react';
|
|
9
7
|
|
|
10
|
-
import {
|
|
8
|
+
import { withTheme } from '@dxos/react-ui/testing';
|
|
11
9
|
|
|
12
|
-
import {
|
|
13
|
-
import { useCanvasContext, useWheel } from '../../hooks';
|
|
10
|
+
import { useCanvasContext, useDrag, useWheel } from '../../hooks';
|
|
14
11
|
import { type Point } from '../../types';
|
|
15
12
|
import { testId } from '../../util';
|
|
16
13
|
import { Grid, type GridProps } from '../Grid';
|
|
17
14
|
|
|
15
|
+
import { Canvas } from './Canvas';
|
|
16
|
+
|
|
18
17
|
const size = 128;
|
|
19
18
|
|
|
20
19
|
const points: Point[] = [0, (2 * Math.PI) / 3, (2 * Math.PI * 2) / 3].map((a, i) => ({
|
|
@@ -33,14 +32,14 @@ const DefaultStory = (props: GridProps) => {
|
|
|
33
32
|
|
|
34
33
|
const TwoCanvases = (props: GridProps) => {
|
|
35
34
|
return (
|
|
36
|
-
<div className='grid grid-cols-2 gap-2
|
|
37
|
-
<div className='
|
|
35
|
+
<div className='grid grid-cols-2 gap-2 is-full bs-full'>
|
|
36
|
+
<div className='bs-full relative'>
|
|
38
37
|
<Canvas>
|
|
39
38
|
<Grid {...props} />
|
|
40
39
|
<Content />
|
|
41
40
|
</Canvas>
|
|
42
41
|
</div>
|
|
43
|
-
<div className='
|
|
42
|
+
<div className='bs-full relative'>
|
|
44
43
|
<Canvas>
|
|
45
44
|
<Grid {...props} />
|
|
46
45
|
<Content />
|
|
@@ -52,6 +51,7 @@ const TwoCanvases = (props: GridProps) => {
|
|
|
52
51
|
|
|
53
52
|
const Content = () => {
|
|
54
53
|
useWheel();
|
|
54
|
+
useDrag();
|
|
55
55
|
return (
|
|
56
56
|
<div>
|
|
57
57
|
{points.map(({ x, y }, i) => (
|
|
@@ -88,22 +88,25 @@ const Item = (p: Point) => {
|
|
|
88
88
|
);
|
|
89
89
|
};
|
|
90
90
|
|
|
91
|
-
const meta
|
|
91
|
+
const meta = {
|
|
92
92
|
title: 'ui/react-ui-canvas/Canvas',
|
|
93
93
|
component: Grid,
|
|
94
94
|
render: DefaultStory,
|
|
95
|
-
decorators: [withTheme
|
|
96
|
-
|
|
95
|
+
decorators: [withTheme()],
|
|
96
|
+
parameters: {
|
|
97
|
+
layout: 'fullscreen',
|
|
98
|
+
},
|
|
99
|
+
} satisfies Meta<typeof Grid>;
|
|
97
100
|
|
|
98
101
|
export default meta;
|
|
99
102
|
|
|
100
|
-
type Story = StoryObj<
|
|
103
|
+
type Story = StoryObj<typeof meta>;
|
|
101
104
|
|
|
102
105
|
export const Default: Story = {
|
|
103
106
|
args: { size: 16 },
|
|
104
107
|
};
|
|
105
108
|
|
|
106
109
|
export const SideBySide: Story = {
|
|
107
|
-
args: { size: 16 },
|
|
108
110
|
render: TwoCanvases,
|
|
111
|
+
args: { size: 16 },
|
|
109
112
|
};
|
|
@@ -15,9 +15,9 @@ 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/
|
|
18
|
+
import { mx } from '@dxos/ui-theme';
|
|
19
19
|
|
|
20
|
-
import {
|
|
20
|
+
import { CanvasContext, ProjectionMapper, type ProjectionState, defaultOrigin } from '../../hooks';
|
|
21
21
|
|
|
22
22
|
export interface CanvasController {
|
|
23
23
|
setProjection(projection: ProjectionState): Promise<void>;
|
|
@@ -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:
|
|
33
|
+
({ children, classNames, scale: _scale = 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:
|
|
41
|
+
const [{ scale, offset }, setProjection] = useState<ProjectionState>({ scale: _scale, offset: offsetProp });
|
|
42
42
|
useEffect(() => {
|
|
43
43
|
if (width && height && offset === defaultOrigin) {
|
|
44
44
|
setProjection({ scale, offset: { x: width / 2, y: height / 2 } });
|
|
@@ -64,17 +64,13 @@ export const Canvas = forwardRef<CanvasController, CanvasProps>(
|
|
|
64
64
|
}, [scale, offset]);
|
|
65
65
|
|
|
66
66
|
// Controller.
|
|
67
|
-
useImperativeHandle(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
};
|
|
75
|
-
},
|
|
76
|
-
[ref],
|
|
77
|
-
);
|
|
67
|
+
useImperativeHandle(forwardedRef, () => {
|
|
68
|
+
return {
|
|
69
|
+
setProjection: async (projection: ProjectionState) => {
|
|
70
|
+
setProjection(projection);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}, [ref]);
|
|
78
74
|
|
|
79
75
|
return (
|
|
80
76
|
<CanvasContext.Provider
|
package/src/components/FPS.tsx
CHANGED
|
@@ -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/
|
|
9
|
+
import { mx } from '@dxos/ui-theme';
|
|
10
10
|
|
|
11
11
|
export type FPSProps = ThemedClassName<{
|
|
12
12
|
width?: number;
|
|
@@ -53,7 +53,7 @@ export const FPS = ({ classNames, width = 60, height = 30, bar = 'bg-cyan-500' }
|
|
|
53
53
|
},
|
|
54
54
|
);
|
|
55
55
|
|
|
56
|
-
const requestRef = useRef<number>();
|
|
56
|
+
const requestRef = useRef<number | null>(null);
|
|
57
57
|
const tick = () => {
|
|
58
58
|
dispatch();
|
|
59
59
|
requestRef.current = requestAnimationFrame(tick);
|
|
@@ -78,7 +78,7 @@ export const FPS = ({ classNames, width = 60, height = 30, bar = 'bg-cyan-500' }
|
|
|
78
78
|
)}
|
|
79
79
|
>
|
|
80
80
|
<div>{fps[len - 1]} FPS</div>
|
|
81
|
-
<div className='
|
|
81
|
+
<div className='is-full relative' style={{ height }}>
|
|
82
82
|
{fps.map((frame, i) => (
|
|
83
83
|
<div
|
|
84
84
|
key={`fps-${i}`}
|
|
@@ -2,37 +2,39 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@
|
|
6
|
-
|
|
7
|
-
import type { Meta, StoryObj } from '@storybook/react';
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
8
6
|
import React, { useRef, useState } from 'react';
|
|
9
7
|
|
|
10
|
-
import {
|
|
8
|
+
import { withTheme } from '@dxos/react-ui/testing';
|
|
11
9
|
|
|
12
|
-
import { GridComponent, type GridProps } from './Grid';
|
|
13
10
|
import { type ProjectionState } from '../../hooks';
|
|
14
11
|
|
|
12
|
+
import { GridComponent, type GridProps } from './Grid';
|
|
13
|
+
|
|
15
14
|
const DefaultStory = (props: GridProps) => {
|
|
16
15
|
const ref = useRef<HTMLDivElement>(null);
|
|
17
16
|
const [{ scale, offset }] = useState<ProjectionState>({ scale: 1, offset: { x: 0, y: 0 } });
|
|
18
17
|
|
|
19
18
|
return (
|
|
20
|
-
<div ref={ref} className='grow'>
|
|
19
|
+
<div role='none' ref={ref} className='grow'>
|
|
21
20
|
<GridComponent scale={scale} offset={offset} {...props} />
|
|
22
21
|
</div>
|
|
23
22
|
);
|
|
24
23
|
};
|
|
25
24
|
|
|
26
|
-
const meta
|
|
25
|
+
const meta = {
|
|
27
26
|
title: 'ui/react-ui-canvas/Grid',
|
|
28
27
|
component: GridComponent,
|
|
29
28
|
render: DefaultStory,
|
|
30
|
-
decorators: [withTheme
|
|
31
|
-
|
|
29
|
+
decorators: [withTheme()],
|
|
30
|
+
parameters: {
|
|
31
|
+
layout: 'fullscreen',
|
|
32
|
+
},
|
|
33
|
+
} satisfies Meta<typeof GridComponent>;
|
|
32
34
|
|
|
33
35
|
export default meta;
|
|
34
36
|
|
|
35
|
-
type Story = StoryObj<
|
|
37
|
+
type Story = StoryObj<typeof meta>;
|
|
36
38
|
|
|
37
39
|
export const Default: Story = {
|
|
38
40
|
args: {
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, { forwardRef,
|
|
5
|
+
import React, { forwardRef, useId, useMemo } from 'react';
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import { mx } from '@dxos/
|
|
7
|
+
import { type ThemedClassName, useForwardedRef } from '@dxos/react-ui';
|
|
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,29 +27,35 @@ 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 <=
|
|
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
57
|
className={mx(
|
|
50
|
-
'absolute inset-0
|
|
58
|
+
'absolute inset-0 is-full bs-full pointer-events-none touch-none select-none',
|
|
51
59
|
'stroke-neutral-500',
|
|
52
60
|
classNames,
|
|
53
61
|
)}
|
|
@@ -79,9 +87,3 @@ export const GridComponent = forwardRef<SVGSVGElement, GridProps>(
|
|
|
79
87
|
);
|
|
80
88
|
},
|
|
81
89
|
);
|
|
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
|
-
};
|
package/src/hooks/index.ts
CHANGED
package/src/hooks/projection.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { interpolate, interpolateObject, transition
|
|
5
|
+
import { easeSinOut, interpolate, interpolateObject, transition } from 'd3';
|
|
6
6
|
import {
|
|
7
7
|
type Matrix,
|
|
8
8
|
applyToPoints,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
translate as translateMatrix,
|
|
14
14
|
} from 'transformation-matrix';
|
|
15
15
|
|
|
16
|
-
import { type
|
|
16
|
+
import { type Dimension, type Point } from '../types';
|
|
17
17
|
|
|
18
18
|
export const defaultOrigin: Point = { x: 0, y: 0 };
|
|
19
19
|
|
|
@@ -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
|
+
};
|
package/src/hooks/useWheel.tsx
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
import { bindAll } from 'bind-event-listener';
|
|
6
6
|
import { useEffect } from 'react';
|
|
7
7
|
|
|
8
|
+
import { getRelativePoint } from '../util';
|
|
9
|
+
|
|
8
10
|
import { getZoomTransform } from './projection';
|
|
9
11
|
import { useCanvasContext } from './useCanvasContext';
|
|
10
|
-
import { getRelativePoint } from '../util';
|
|
11
12
|
|
|
12
13
|
export type WheelOptions = {
|
|
13
14
|
zoom?: boolean;
|
|
@@ -33,9 +34,6 @@ export const useWheel = (options: WheelOptions = defaultOptions) => {
|
|
|
33
34
|
options: { capture: true, passive: false },
|
|
34
35
|
listener: (ev: WheelEvent) => {
|
|
35
36
|
const zooming = isWheelZooming(ev);
|
|
36
|
-
if (!hasFocus(root) && !zooming) {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
37
|
|
|
40
38
|
ev.preventDefault();
|
|
41
39
|
if (zooming && !options.zoom) {
|
|
@@ -81,27 +79,3 @@ const isWheelZooming = (ev: WheelEvent): boolean => {
|
|
|
81
79
|
|
|
82
80
|
return false;
|
|
83
81
|
};
|
|
84
|
-
|
|
85
|
-
const hasFocus = (element: HTMLElement): boolean => {
|
|
86
|
-
const activeElement = document.activeElement;
|
|
87
|
-
if (!activeElement) {
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Handle shadow DOM.
|
|
92
|
-
let shadowActive = activeElement;
|
|
93
|
-
while (shadowActive?.shadowRoot?.activeElement) {
|
|
94
|
-
shadowActive = shadowActive.shadowRoot.activeElement;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Check if element or any parent has focus.
|
|
98
|
-
let current: HTMLElement | null = element;
|
|
99
|
-
while (current) {
|
|
100
|
-
if (current === activeElement || current === shadowActive) {
|
|
101
|
-
return true;
|
|
102
|
-
}
|
|
103
|
-
current = current.parentElement;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return false;
|
|
107
|
-
};
|
package/src/types.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
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 });
|
package/src/util/svg.stories.tsx
CHANGED
|
@@ -2,18 +2,16 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@
|
|
6
|
-
|
|
7
|
-
import type { Meta } from '@storybook/react';
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
8
6
|
import React from 'react';
|
|
9
7
|
|
|
10
|
-
import { withTheme } from '@dxos/
|
|
8
|
+
import { withTheme } from '@dxos/react-ui/testing';
|
|
11
9
|
|
|
12
10
|
import { Arrow, createPath } from './svg';
|
|
13
11
|
import { testId } from './util';
|
|
14
12
|
|
|
15
13
|
const DefaultStory = () => (
|
|
16
|
-
<svg className='border border-
|
|
14
|
+
<svg className='border border-separator is-[30rem] bs-[400px]'>
|
|
17
15
|
<defs>
|
|
18
16
|
<Arrow id='arrow-start' classNames='fill-none stroke-red-500' dir='start' />
|
|
19
17
|
<Arrow id='arrow-end' classNames='fill-none stroke-red-500' dir='end' />
|
|
@@ -31,15 +29,17 @@ const DefaultStory = () => (
|
|
|
31
29
|
</svg>
|
|
32
30
|
);
|
|
33
31
|
|
|
34
|
-
const meta
|
|
32
|
+
const meta = {
|
|
35
33
|
title: 'ui/react-ui-canvas/svg',
|
|
36
34
|
render: DefaultStory,
|
|
37
|
-
decorators: [withTheme],
|
|
35
|
+
decorators: [withTheme()],
|
|
38
36
|
parameters: {
|
|
39
37
|
layout: 'centered',
|
|
40
38
|
},
|
|
41
|
-
};
|
|
39
|
+
} satisfies Meta;
|
|
42
40
|
|
|
43
41
|
export default meta;
|
|
44
42
|
|
|
45
|
-
|
|
43
|
+
type Story = StoryObj<typeof meta>;
|
|
44
|
+
|
|
45
|
+
export const Default: Story = {};
|
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/
|
|
8
|
+
import { mx } from '@dxos/ui-theme';
|
|
9
9
|
|
|
10
10
|
import { type Dimension, type Point } from '../types';
|
|
11
11
|
|