@dxos/react-ui-canvas 0.7.5-feature-compute.4d9d99a

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 (67) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +1 -0
  3. package/dist/lib/browser/index.mjs +605 -0
  4. package/dist/lib/browser/index.mjs.map +7 -0
  5. package/dist/lib/browser/meta.json +1 -0
  6. package/dist/lib/node/index.cjs +641 -0
  7. package/dist/lib/node/index.cjs.map +7 -0
  8. package/dist/lib/node/meta.json +1 -0
  9. package/dist/lib/node-esm/index.mjs +607 -0
  10. package/dist/lib/node-esm/index.mjs.map +7 -0
  11. package/dist/lib/node-esm/meta.json +1 -0
  12. package/dist/types/src/components/Canvas/Canvas.d.ts +15 -0
  13. package/dist/types/src/components/Canvas/Canvas.d.ts.map +1 -0
  14. package/dist/types/src/components/Canvas/Canvas.stories.d.ts +9 -0
  15. package/dist/types/src/components/Canvas/Canvas.stories.d.ts.map +1 -0
  16. package/dist/types/src/components/Canvas/index.d.ts +2 -0
  17. package/dist/types/src/components/Canvas/index.d.ts.map +1 -0
  18. package/dist/types/src/components/FPS.d.ts +9 -0
  19. package/dist/types/src/components/FPS.d.ts.map +1 -0
  20. package/dist/types/src/components/Grid/Grid.d.ts +19 -0
  21. package/dist/types/src/components/Grid/Grid.d.ts.map +1 -0
  22. package/dist/types/src/components/Grid/Grid.stories.d.ts +8 -0
  23. package/dist/types/src/components/Grid/Grid.stories.d.ts.map +1 -0
  24. package/dist/types/src/components/Grid/index.d.ts +2 -0
  25. package/dist/types/src/components/Grid/index.d.ts.map +1 -0
  26. package/dist/types/src/components/index.d.ts +4 -0
  27. package/dist/types/src/components/index.d.ts.map +1 -0
  28. package/dist/types/src/hooks/index.d.ts +4 -0
  29. package/dist/types/src/hooks/index.d.ts.map +1 -0
  30. package/dist/types/src/hooks/projection.d.ts +58 -0
  31. package/dist/types/src/hooks/projection.d.ts.map +1 -0
  32. package/dist/types/src/hooks/useCanvasContext.d.ts +13 -0
  33. package/dist/types/src/hooks/useCanvasContext.d.ts.map +1 -0
  34. package/dist/types/src/hooks/useWheel.d.ts +8 -0
  35. package/dist/types/src/hooks/useWheel.d.ts.map +1 -0
  36. package/dist/types/src/index.d.ts +5 -0
  37. package/dist/types/src/index.d.ts.map +1 -0
  38. package/dist/types/src/types.d.ts +20 -0
  39. package/dist/types/src/types.d.ts.map +1 -0
  40. package/dist/types/src/util/index.d.ts +3 -0
  41. package/dist/types/src/util/index.d.ts.map +1 -0
  42. package/dist/types/src/util/svg.d.ts +33 -0
  43. package/dist/types/src/util/svg.d.ts.map +1 -0
  44. package/dist/types/src/util/svg.stories.d.ts +6 -0
  45. package/dist/types/src/util/svg.stories.d.ts.map +1 -0
  46. package/dist/types/src/util/util.d.ts +17 -0
  47. package/dist/types/src/util/util.d.ts.map +1 -0
  48. package/dist/types/tsconfig.tsbuildinfo +1 -0
  49. package/package.json +61 -0
  50. package/src/components/Canvas/Canvas.stories.tsx +109 -0
  51. package/src/components/Canvas/Canvas.tsx +89 -0
  52. package/src/components/Canvas/index.ts +5 -0
  53. package/src/components/FPS.tsx +98 -0
  54. package/src/components/Grid/Grid.stories.tsx +41 -0
  55. package/src/components/Grid/Grid.tsx +87 -0
  56. package/src/components/Grid/index.ts +5 -0
  57. package/src/components/index.ts +7 -0
  58. package/src/hooks/index.ts +7 -0
  59. package/src/hooks/projection.tsx +156 -0
  60. package/src/hooks/useCanvasContext.tsx +29 -0
  61. package/src/hooks/useWheel.tsx +107 -0
  62. package/src/index.ts +8 -0
  63. package/src/types.ts +13 -0
  64. package/src/util/index.ts +6 -0
  65. package/src/util/svg.stories.tsx +45 -0
  66. package/src/util/svg.tsx +131 -0
  67. package/src/util/util.ts +50 -0
@@ -0,0 +1,107 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { bindAll } from 'bind-event-listener';
6
+ import { useEffect } from 'react';
7
+
8
+ import { getZoomTransform } from './projection';
9
+ import { useCanvasContext } from './useCanvasContext';
10
+ import { getRelativePoint } from '../util';
11
+
12
+ export type WheelOptions = {
13
+ zoom?: boolean;
14
+ };
15
+
16
+ const defaultOptions: WheelOptions = {
17
+ zoom: true,
18
+ };
19
+
20
+ /**
21
+ * Handle wheel events to update the transform state (zoom and offset).
22
+ */
23
+ export const useWheel = (options: WheelOptions = defaultOptions) => {
24
+ const { root, setProjection } = useCanvasContext();
25
+ useEffect(() => {
26
+ if (!root) {
27
+ return;
28
+ }
29
+
30
+ return bindAll(root, [
31
+ {
32
+ type: 'wheel',
33
+ options: { capture: true, passive: false },
34
+ listener: (ev: WheelEvent) => {
35
+ const zooming = isWheelZooming(ev);
36
+ if (!hasFocus(root) && !zooming) {
37
+ return;
38
+ }
39
+
40
+ ev.preventDefault();
41
+ if (zooming && !options.zoom) {
42
+ return;
43
+ }
44
+
45
+ // Zoom or pan.
46
+ if (ev.ctrlKey) {
47
+ if (!root) {
48
+ return;
49
+ }
50
+
51
+ // Keep centered while zooming.
52
+ setProjection(({ scale, offset }) => {
53
+ const pos = getRelativePoint(root, ev);
54
+ const scaleSensitivity = 0.01;
55
+ const newScale = scale * Math.exp(-ev.deltaY * scaleSensitivity);
56
+ return getZoomTransform({ scale, offset, newScale, pos });
57
+ });
58
+ } else {
59
+ setProjection(({ scale, offset: { x, y } }) => {
60
+ return {
61
+ scale,
62
+ offset: {
63
+ x: x - ev.deltaX,
64
+ y: y - ev.deltaY,
65
+ },
66
+ };
67
+ });
68
+ }
69
+ },
70
+ },
71
+ ]);
72
+ }, [root]);
73
+ };
74
+
75
+ const isWheelZooming = (ev: WheelEvent): boolean => {
76
+ // Check for ctrl/cmd key + wheel action.
77
+ if (ev.ctrlKey || ev.metaKey) {
78
+ // Some browsers use deltaY, others deltaZ for zoom.
79
+ return Math.abs(ev.deltaY) > 0 || Math.abs(ev.deltaZ) > 0;
80
+ }
81
+
82
+ return false;
83
+ };
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/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './components';
6
+ export * from './hooks';
7
+ export * from './types';
8
+ export * from './util';
package/src/types.ts ADDED
@@ -0,0 +1,13 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { S } from '@dxos/effect';
6
+
7
+ export const Point = S.Struct({ x: S.Number, y: S.Number });
8
+ export const Dimension = S.Struct({ width: S.Number, height: S.Number });
9
+ export const Rect = S.extend(Point, Dimension);
10
+
11
+ export type Point = S.Schema.Type<typeof Point>;
12
+ export type Dimension = S.Schema.Type<typeof Dimension>;
13
+ export type Rect = S.Schema.Type<typeof Rect>;
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './svg';
6
+ export * from './util';
@@ -0,0 +1,45 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import '@dxos-theme';
6
+
7
+ import type { Meta } from '@storybook/react';
8
+ import React from 'react';
9
+
10
+ import { withTheme } from '@dxos/storybook-utils';
11
+
12
+ import { Arrow, createPath } from './svg';
13
+ import { testId } from './util';
14
+
15
+ const Render = () => (
16
+ <svg className='border border-neutral-500 w-[400px] h-[400px]'>
17
+ <defs>
18
+ <Arrow id='arrow-start' classNames='fill-none stroke-red-500' dir='start' />
19
+ <Arrow id='arrow-end' classNames='fill-none stroke-red-500' dir='end' />
20
+ </defs>
21
+ <path
22
+ {...testId('dx-storybook', true)}
23
+ className={'stroke-red-500'}
24
+ d={createPath([
25
+ { x: 100, y: 300 },
26
+ { x: 300, y: 100 },
27
+ ])}
28
+ markerStart={'url(#arrow-start)'}
29
+ markerEnd={'url(#arrow-end)'}
30
+ />
31
+ </svg>
32
+ );
33
+
34
+ const meta: Meta = {
35
+ title: 'ui/react-ui-canvas/svg',
36
+ render: Render,
37
+ decorators: [withTheme],
38
+ parameters: {
39
+ layout: 'centered',
40
+ },
41
+ };
42
+
43
+ export default meta;
44
+
45
+ export const Default = {};
@@ -0,0 +1,131 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { type PropsWithChildren, type SVGProps } from 'react';
6
+
7
+ import { type ThemedClassName } from '@dxos/react-ui';
8
+ import { mx } from '@dxos/react-ui-theme';
9
+
10
+ import { type Dimension, type Point } from '../types';
11
+
12
+ // Refs
13
+ // - https://airbnb.io/visx/gallery
14
+ // - https://github.com/tldraw/tldraw/blob/main/packages/editor/src/lib/primitives/Vec.ts
15
+
16
+ export const createPath = (points: Point[], join = false) => {
17
+ return ['M', points.map(({ x, y }) => `${x},${y}`).join(' L '), join ? 'Z' : ''].join(' ');
18
+ };
19
+
20
+ /**
21
+ * https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths
22
+ * NOTE: Leave space around shape for line width.
23
+ */
24
+ export const Markers = ({ id = 'dx-marker', classNames }: ThemedClassName<{ id?: string }>) => {
25
+ return (
26
+ <>
27
+ <Arrow id={`${id}-arrow-start`} dir='start' classNames={classNames} />
28
+ <Arrow id={`${id}-arrow-end`} dir='end' classNames={classNames} />
29
+ <Arrow id={`${id}-triangle-start`} dir='start' closed classNames={classNames} />
30
+ <Arrow id={`${id}-triangle-end`} dir='end' closed classNames={classNames} />
31
+ <Marker id={`${id}-circle`} pos={{ x: 8, y: 8 }} size={{ width: 16, height: 16 }}>
32
+ <circle cx={8} cy={8} r={5} stroke={'context-stroke'} className={mx(classNames)} />
33
+ </Marker>
34
+ </>
35
+ );
36
+ };
37
+
38
+ export type MarkerProps = SVGProps<SVGMarkerElement> &
39
+ PropsWithChildren<
40
+ ThemedClassName<{
41
+ id: string;
42
+ pos: Point;
43
+ size: Dimension;
44
+ fill?: boolean;
45
+ }>
46
+ >;
47
+
48
+ /**
49
+ * https://www.w3.org/TR/SVG2/painting.html#Markers
50
+ */
51
+ export const Marker = ({
52
+ id,
53
+ className,
54
+ children,
55
+ pos: { x: refX, y: refY },
56
+ size: { width: markerWidth, height: markerHeight },
57
+ fill,
58
+ ...rest
59
+ }: MarkerProps) => (
60
+ <marker
61
+ id={id}
62
+ className={className}
63
+ {...{
64
+ refX,
65
+ refY,
66
+ markerWidth,
67
+ markerHeight,
68
+ markerUnits: 'strokeWidth',
69
+ orient: 'auto',
70
+ ...rest,
71
+ }}
72
+ >
73
+ {children}
74
+ </marker>
75
+ );
76
+
77
+ export const Arrow = ({
78
+ classNames,
79
+ id,
80
+ size = 16,
81
+ dir = 'end',
82
+ closed = false,
83
+ }: ThemedClassName<{ id: string; size?: number; dir?: 'start' | 'end'; closed?: boolean }>) => (
84
+ <Marker
85
+ id={id}
86
+ size={{ width: size, height: size }}
87
+ pos={dir === 'end' ? { x: size, y: size / 2 } : { x: 0, y: size / 2 }}
88
+ >
89
+ <path
90
+ fill={closed ? undefined : 'none'}
91
+ stroke={'context-stroke'}
92
+ className={mx(classNames)}
93
+ d={createPath(
94
+ dir === 'end'
95
+ ? [
96
+ { x: 1, y: 1 },
97
+ { x: size, y: size / 2 },
98
+ { x: 1, y: size - 1 },
99
+ ]
100
+ : [
101
+ { x: size - 1, y: 1 },
102
+ { x: 0, y: size / 2 },
103
+ { x: size - 1, y: size - 1 },
104
+ ],
105
+ closed,
106
+ )}
107
+ />
108
+ </Marker>
109
+ );
110
+
111
+ export const GridPattern = ({
112
+ classNames,
113
+ id,
114
+ size,
115
+ offset,
116
+ }: ThemedClassName<{ id: string; size: number; offset: Point }>) => (
117
+ <pattern
118
+ id={id}
119
+ x={(size / 2 + offset.x) % size}
120
+ y={(size / 2 + offset.y) % size}
121
+ width={size}
122
+ height={size}
123
+ patternUnits='userSpaceOnUse'
124
+ >
125
+ {/* TODO(burdon): vars. */}
126
+ <g className={mx(classNames)}>
127
+ <line x1={0} y1={size / 2} x2={size} y2={size / 2} />
128
+ <line x1={size / 2} y1={0} x2={size / 2} y2={size} />
129
+ </g>
130
+ </pattern>
131
+ );
@@ -0,0 +1,50 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ let logged = false;
6
+
7
+ /**
8
+ * Get the relative point of the cursor.
9
+ * NOTE: ev.offset returns the position relative to the target.
10
+ */
11
+ export const getRelativePoint = (el: HTMLElement, ev: MouseEvent) => {
12
+ const rect = el.getBoundingClientRect();
13
+ return { x: ev.clientX - rect.x, y: ev.clientY - rect.top };
14
+ };
15
+
16
+ /**
17
+ *
18
+ */
19
+ // TODO(burdon): Factor out.
20
+ export const testId = <ID = string>(id: ID, inspect = false) => {
21
+ if (inspect) {
22
+ if (!logged) {
23
+ // eslint-disable-next-line no-console
24
+ console.log('Open storybook in expanded window;\nthen run INSPECT()');
25
+ logged = true;
26
+ }
27
+
28
+ (window as any).INSPECT = () => {
29
+ const el = document.querySelector(`[data-test-id="${id}"]`);
30
+ (window as any).inspect(el);
31
+ // eslint-disable-next-line no-console
32
+ console.log(el);
33
+ };
34
+ }
35
+
36
+ return { [DATA_TEST_ID]: id };
37
+ };
38
+
39
+ export const inspectElement = (el: Element) => {
40
+ (window as any).INSPECT = () => {
41
+ (window as any).inspect(el);
42
+ (window as any).element = el;
43
+ // eslint-disable-next-line no-console
44
+ console.log('Open storybook in expanded window;\nthen run INSPECT()');
45
+ // eslint-disable-next-line no-console
46
+ console.log(el);
47
+ };
48
+ };
49
+
50
+ export const DATA_TEST_ID = 'data-test-id';