@prnt/dagr-explorer 0.1.3

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 (135) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/LICENSE +21 -0
  3. package/README.md +441 -0
  4. package/dist/base.d.ts +63 -0
  5. package/dist/base.d.ts.map +1 -0
  6. package/dist/base.js +20 -0
  7. package/dist/base.js.map +1 -0
  8. package/dist/camera.d.ts +61 -0
  9. package/dist/camera.d.ts.map +1 -0
  10. package/dist/camera.js +142 -0
  11. package/dist/camera.js.map +1 -0
  12. package/dist/context.d.ts +110 -0
  13. package/dist/context.d.ts.map +1 -0
  14. package/dist/context.js +71 -0
  15. package/dist/context.js.map +1 -0
  16. package/dist/dagr-explorer.d.ts +41 -0
  17. package/dist/dagr-explorer.d.ts.map +1 -0
  18. package/dist/dagr-explorer.js +13 -0
  19. package/dist/dagr-explorer.js.map +1 -0
  20. package/dist/errors.d.ts +55 -0
  21. package/dist/errors.d.ts.map +1 -0
  22. package/dist/errors.js +57 -0
  23. package/dist/errors.js.map +1 -0
  24. package/dist/explorer-details.d.ts +44 -0
  25. package/dist/explorer-details.d.ts.map +1 -0
  26. package/dist/explorer-details.js +76 -0
  27. package/dist/explorer-details.js.map +1 -0
  28. package/dist/explorer-search.d.ts +31 -0
  29. package/dist/explorer-search.d.ts.map +1 -0
  30. package/dist/explorer-search.js +86 -0
  31. package/dist/explorer-search.js.map +1 -0
  32. package/dist/explorer-toolbar.d.ts +17 -0
  33. package/dist/explorer-toolbar.d.ts.map +1 -0
  34. package/dist/explorer-toolbar.js +44 -0
  35. package/dist/explorer-toolbar.js.map +1 -0
  36. package/dist/explorer-trace-toggle.d.ts +16 -0
  37. package/dist/explorer-trace-toggle.d.ts.map +1 -0
  38. package/dist/explorer-trace-toggle.js +8 -0
  39. package/dist/explorer-trace-toggle.js.map +1 -0
  40. package/dist/explorer-viewport.d.ts +69 -0
  41. package/dist/explorer-viewport.d.ts.map +1 -0
  42. package/dist/explorer-viewport.js +69 -0
  43. package/dist/explorer-viewport.js.map +1 -0
  44. package/dist/explorer-views.d.ts +24 -0
  45. package/dist/explorer-views.d.ts.map +1 -0
  46. package/dist/explorer-views.js +16 -0
  47. package/dist/explorer-views.js.map +1 -0
  48. package/dist/index.d.ts +48 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +33 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/isomorphic-layout-effect.d.ts +7 -0
  53. package/dist/isomorphic-layout-effect.d.ts.map +1 -0
  54. package/dist/isomorphic-layout-effect.js +7 -0
  55. package/dist/isomorphic-layout-effect.js.map +1 -0
  56. package/dist/labels.d.ts +67 -0
  57. package/dist/labels.d.ts.map +1 -0
  58. package/dist/labels.js +50 -0
  59. package/dist/labels.js.map +1 -0
  60. package/dist/layout.d.ts +82 -0
  61. package/dist/layout.d.ts.map +1 -0
  62. package/dist/layout.js +244 -0
  63. package/dist/layout.js.map +1 -0
  64. package/dist/navigation.d.ts +20 -0
  65. package/dist/navigation.d.ts.map +1 -0
  66. package/dist/navigation.js +44 -0
  67. package/dist/navigation.js.map +1 -0
  68. package/dist/root.d.ts +89 -0
  69. package/dist/root.d.ts.map +1 -0
  70. package/dist/root.js +401 -0
  71. package/dist/root.js.map +1 -0
  72. package/dist/search.d.ts +19 -0
  73. package/dist/search.d.ts.map +1 -0
  74. package/dist/search.js +31 -0
  75. package/dist/search.js.map +1 -0
  76. package/dist/size.d.ts +12 -0
  77. package/dist/size.d.ts.map +1 -0
  78. package/dist/size.js +20 -0
  79. package/dist/size.js.map +1 -0
  80. package/dist/svg-base.d.ts +33 -0
  81. package/dist/svg-base.d.ts.map +1 -0
  82. package/dist/svg-base.js +89 -0
  83. package/dist/svg-base.js.map +1 -0
  84. package/dist/types.d.ts +61 -0
  85. package/dist/types.d.ts.map +1 -0
  86. package/dist/types.js +2 -0
  87. package/dist/types.js.map +1 -0
  88. package/dist/use-explorer-camera.d.ts +56 -0
  89. package/dist/use-explorer-camera.d.ts.map +1 -0
  90. package/dist/use-explorer-camera.js +641 -0
  91. package/dist/use-explorer-camera.js.map +1 -0
  92. package/dist/use-explorer.d.ts +23 -0
  93. package/dist/use-explorer.d.ts.map +1 -0
  94. package/dist/use-explorer.js +26 -0
  95. package/dist/use-explorer.js.map +1 -0
  96. package/dist/validate.d.ts +20 -0
  97. package/dist/validate.d.ts.map +1 -0
  98. package/dist/validate.js +99 -0
  99. package/dist/validate.js.map +1 -0
  100. package/dist/viewport-surface.d.ts +90 -0
  101. package/dist/viewport-surface.d.ts.map +1 -0
  102. package/dist/viewport-surface.js +485 -0
  103. package/dist/viewport-surface.js.map +1 -0
  104. package/dist/visible-set.d.ts +65 -0
  105. package/dist/visible-set.d.ts.map +1 -0
  106. package/dist/visible-set.js +168 -0
  107. package/dist/visible-set.js.map +1 -0
  108. package/package.json +62 -0
  109. package/src/base.ts +67 -0
  110. package/src/camera.ts +206 -0
  111. package/src/context.ts +168 -0
  112. package/src/dagr-explorer.tsx +85 -0
  113. package/src/errors.ts +85 -0
  114. package/src/explorer-details.tsx +156 -0
  115. package/src/explorer-search.tsx +140 -0
  116. package/src/explorer-toolbar.tsx +77 -0
  117. package/src/explorer-trace-toggle.tsx +34 -0
  118. package/src/explorer-viewport.tsx +181 -0
  119. package/src/explorer-views.tsx +61 -0
  120. package/src/index.ts +76 -0
  121. package/src/isomorphic-layout-effect.ts +7 -0
  122. package/src/labels.ts +109 -0
  123. package/src/layout.ts +298 -0
  124. package/src/navigation.ts +51 -0
  125. package/src/root.tsx +544 -0
  126. package/src/search.ts +36 -0
  127. package/src/size.ts +23 -0
  128. package/src/svg-base.tsx +173 -0
  129. package/src/types.ts +69 -0
  130. package/src/use-explorer-camera.ts +675 -0
  131. package/src/use-explorer.ts +32 -0
  132. package/src/validate.ts +173 -0
  133. package/src/viewport-surface.tsx +650 -0
  134. package/src/visible-set.ts +225 -0
  135. package/styles.css +253 -0
@@ -0,0 +1,173 @@
1
+ /**
2
+ * The default base layer: marks, edges and group outlines as one SVG.
3
+ *
4
+ * It sits inside the transformed plane, in world coordinates, so the camera
5
+ * never touches it: a pan or a zoom is the plane's transform and nothing
6
+ * here. It renders when the visible set or the emphasis changes, which is
7
+ * when the viewport hands it a new prop, and at no other time.
8
+ *
9
+ * It is decoration for sighted users, so it is `aria-hidden`. Every node a
10
+ * user can reach is reachable through the overlay and search, and a mark
11
+ * is clicked through the viewport's delegated listener, which resolves the
12
+ * point against the layout rather than against these elements.
13
+ *
14
+ * **One arrowhead marker per edge color.** An SVG marker cannot take the
15
+ * stroke of the path that uses it without `context-stroke`, which not every
16
+ * browser this package supports draws. So the arrowhead for a red edge is a
17
+ * red marker, and the markers are as many as the colors in view.
18
+ *
19
+ * **A marker's id is its color, escaped.** Ids live in the document, not
20
+ * in a React root, and two roots (two explorers, or two copies of React)
21
+ * can count `useId` the same way. Named by color, a marker one root reaches
22
+ * in another root's tree is the same arrowhead.
23
+ *
24
+ * **Strokes keep their width at any zoom** (`vector-effect`), so an edge
25
+ * never thins to nothing zoomed out or thickens to a bar zoomed in.
26
+ * Arrowheads are not strokes: the marker is in user space, so they scale
27
+ * with the camera like the marks they point at.
28
+ *
29
+ * Internal to the package. Nothing here is exported from the entry.
30
+ */
31
+
32
+ import { useMemo } from 'react';
33
+ import type { ReactElement } from 'react';
34
+ import type { ExplorerBase, ExplorerBaseProps } from './base.js';
35
+ import type { ExplorerEdge, ExplorerNode } from './types.js';
36
+
37
+ const DEFAULT_COLOR = 'currentColor';
38
+ const DASH = '6 4';
39
+
40
+ /**
41
+ * The marker id for a color: letters and digits as they are, and every other
42
+ * character, `-` included, as `-<hex code point>-`. Escaped rather than
43
+ * stripped, so two colors never share an id (`rgb(1, 23, 4)` and
44
+ * `rgb(12, 3, 4)` strip to the same characters), and the result is a valid
45
+ * `url(#...)` fragment for any CSS color.
46
+ */
47
+ function markerIdOf(color: string): string {
48
+ const escaped = color.replace(/[^A-Za-z0-9]/gu, (char) => `-${(char.codePointAt(0) ?? 0).toString(16)}-`);
49
+ return `dagr-explorer-arrow-${escaped}`;
50
+ }
51
+
52
+ function SvgBaseLayer({
53
+ view,
54
+ layout,
55
+ visible,
56
+ emphasis,
57
+ }: ExplorerBaseProps<ExplorerNode, ExplorerEdge>): ReactElement {
58
+ const nodes = useMemo(() => new Map(view.nodes.map((node) => [node.id, node])), [view.nodes]);
59
+ const edges = useMemo(() => new Map(view.edges.map((edge) => [edge.id, edge])), [view.edges]);
60
+
61
+ const markers = new Map<string, string>();
62
+ const markerFor = (color: string): string => {
63
+ let id = markers.get(color);
64
+ if (id === undefined) {
65
+ id = markerIdOf(color);
66
+ markers.set(color, id);
67
+ }
68
+ return id;
69
+ };
70
+
71
+ const paths: ReactElement[] = [];
72
+ for (const id of visible.edges) {
73
+ const edge = edges.get(id);
74
+ const route = layout.routes.get(id);
75
+ const first = route?.[0];
76
+ if (edge === undefined || route === undefined || first === undefined) continue;
77
+ const d = route.map((p, i) => `${i === 0 ? 'M' : 'L'}${String(p.x)} ${String(p.y)}`).join(' ');
78
+ const color = edge.color ?? DEFAULT_COLOR;
79
+ const dimmed = emphasis.dimmed.has(edge.source) || emphasis.dimmed.has(edge.target);
80
+ paths.push(
81
+ <path
82
+ key={id}
83
+ data-edge-id={id}
84
+ data-dimmed={dimmed ? 'true' : undefined}
85
+ d={d}
86
+ fill="none"
87
+ stroke={color}
88
+ strokeWidth={1.5}
89
+ vectorEffect="non-scaling-stroke"
90
+ strokeDasharray={edge.dash === true ? DASH : undefined}
91
+ markerEnd={`url(#${markerFor(color)})`}
92
+ />,
93
+ );
94
+ }
95
+
96
+ const marks: ReactElement[] = [];
97
+ for (const id of visible.baseNodes) {
98
+ const node = nodes.get(id);
99
+ const box = layout.boxes.get(id);
100
+ if (node === undefined || box === undefined) continue;
101
+ marks.push(
102
+ <rect
103
+ key={id}
104
+ data-node-id={id}
105
+ data-dimmed={emphasis.dimmed.has(id) ? 'true' : undefined}
106
+ x={box.x}
107
+ y={box.y}
108
+ width={box.width}
109
+ height={box.height}
110
+ rx={4}
111
+ fill={node.color ?? DEFAULT_COLOR}
112
+ />,
113
+ );
114
+ }
115
+
116
+ const groups: ReactElement[] = [];
117
+ for (const group of view.groups ?? []) {
118
+ const box = layout.groups.get(group.id);
119
+ if (box === undefined) continue;
120
+ const color = group.color ?? DEFAULT_COLOR;
121
+ groups.push(
122
+ <g key={group.id} data-group-id={group.id}>
123
+ <rect
124
+ x={box.x}
125
+ y={box.y}
126
+ width={box.width}
127
+ height={box.height}
128
+ rx={8}
129
+ fill="none"
130
+ stroke={color}
131
+ vectorEffect="non-scaling-stroke"
132
+ strokeDasharray="4 4"
133
+ />
134
+ <text x={box.x + 12} y={box.y + 17} fill={color} fontSize={12}>
135
+ {group.label}
136
+ </text>
137
+ </g>,
138
+ );
139
+ }
140
+
141
+ return (
142
+ <svg
143
+ aria-hidden="true"
144
+ width={layout.width}
145
+ height={layout.height}
146
+ viewBox={`0 0 ${String(layout.width)} ${String(layout.height)}`}
147
+ style={{ position: 'absolute', left: 0, top: 0, overflow: 'visible' }}
148
+ >
149
+ <defs>
150
+ {[...markers].map(([color, id]) => (
151
+ <marker
152
+ key={id}
153
+ id={id}
154
+ viewBox="0 0 10 10"
155
+ refX={10}
156
+ refY={5}
157
+ markerWidth={8}
158
+ markerHeight={8}
159
+ markerUnits="userSpaceOnUse"
160
+ orient="auto"
161
+ >
162
+ <path d="M0 0L10 5L0 10z" fill={color} />
163
+ </marker>
164
+ ))}
165
+ </defs>
166
+ {groups}
167
+ {paths}
168
+ {marks}
169
+ </svg>
170
+ );
171
+ }
172
+
173
+ export const svgBase: ExplorerBase = Object.freeze({ Layer: SvgBaseLayer, space: 'plane' });
package/src/types.ts ADDED
@@ -0,0 +1,69 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { Size } from '@prnt/dagr-render/core';
3
+
4
+ export type { Size };
5
+
6
+ /**
7
+ * One node. `id` and `label` are all the explorer itself reads: the label is
8
+ * the accessible name, the search result text, and the default content.
9
+ * Everything else about a node is the caller's own fields on a type that
10
+ * extends this one, and every slot receives that type.
11
+ *
12
+ * Optional fields are spelled `T | undefined` so a caller compiling with
13
+ * `exactOptionalPropertyTypes` can pass a value it computed, including
14
+ * `undefined`, without a conditional spread.
15
+ */
16
+ export interface ExplorerNode {
17
+ readonly id: string;
18
+ readonly label: string;
19
+ /** World size in CSS pixels at zoom 1. Declared, never measured. */
20
+ readonly size?: Size | undefined;
21
+ /** Any CSS color. */
22
+ readonly color?: string | undefined;
23
+ }
24
+
25
+ export interface ExplorerEdge {
26
+ readonly id: string;
27
+ readonly source: string;
28
+ readonly target: string;
29
+ readonly label?: string | undefined;
30
+ /** Any CSS color. */
31
+ readonly color?: string | undefined;
32
+ /** Draw the edge dashed. */
33
+ readonly dash?: boolean | undefined;
34
+ }
35
+
36
+ /** A labeled outline around its members. An annotation: it moves no node. */
37
+ export interface ExplorerGroup {
38
+ readonly id: string;
39
+ readonly label: string;
40
+ readonly nodeIds: readonly string[];
41
+ /** Any CSS color. */
42
+ readonly color?: string | undefined;
43
+ }
44
+
45
+ export interface ExplorerLayoutOptions<N extends ExplorerNode = ExplorerNode> {
46
+ /** Which way edges flow. Default `'right'`. */
47
+ readonly direction?: 'right' | 'down' | undefined;
48
+ /** Size for nodes with no `size` of their own. Default 240 by 120. */
49
+ readonly nodeSize?: Size | ((node: N) => Size) | undefined;
50
+ /** Gap between neighbors across the flow. Default 40. */
51
+ readonly nodeSep?: number | undefined;
52
+ /** Gap between ranks along the flow. Default 120. */
53
+ readonly rankSep?: number | undefined;
54
+ /** Default `'smooth'`. */
55
+ readonly edgeStyle?: 'smooth' | 'orthogonal' | undefined;
56
+ }
57
+
58
+ export interface ExplorerView<
59
+ N extends ExplorerNode = ExplorerNode,
60
+ E extends ExplorerEdge = ExplorerEdge,
61
+ > {
62
+ readonly id: string;
63
+ readonly label: string;
64
+ readonly description?: ReactNode | undefined;
65
+ readonly nodes: readonly N[];
66
+ readonly edges: readonly E[];
67
+ readonly groups?: readonly ExplorerGroup[] | undefined;
68
+ readonly layout?: ExplorerLayoutOptions<N> | undefined;
69
+ }