@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.
- package/CHANGELOG.md +93 -0
- package/LICENSE +21 -0
- package/README.md +441 -0
- package/dist/base.d.ts +63 -0
- package/dist/base.d.ts.map +1 -0
- package/dist/base.js +20 -0
- package/dist/base.js.map +1 -0
- package/dist/camera.d.ts +61 -0
- package/dist/camera.d.ts.map +1 -0
- package/dist/camera.js +142 -0
- package/dist/camera.js.map +1 -0
- package/dist/context.d.ts +110 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +71 -0
- package/dist/context.js.map +1 -0
- package/dist/dagr-explorer.d.ts +41 -0
- package/dist/dagr-explorer.d.ts.map +1 -0
- package/dist/dagr-explorer.js +13 -0
- package/dist/dagr-explorer.js.map +1 -0
- package/dist/errors.d.ts +55 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +57 -0
- package/dist/errors.js.map +1 -0
- package/dist/explorer-details.d.ts +44 -0
- package/dist/explorer-details.d.ts.map +1 -0
- package/dist/explorer-details.js +76 -0
- package/dist/explorer-details.js.map +1 -0
- package/dist/explorer-search.d.ts +31 -0
- package/dist/explorer-search.d.ts.map +1 -0
- package/dist/explorer-search.js +86 -0
- package/dist/explorer-search.js.map +1 -0
- package/dist/explorer-toolbar.d.ts +17 -0
- package/dist/explorer-toolbar.d.ts.map +1 -0
- package/dist/explorer-toolbar.js +44 -0
- package/dist/explorer-toolbar.js.map +1 -0
- package/dist/explorer-trace-toggle.d.ts +16 -0
- package/dist/explorer-trace-toggle.d.ts.map +1 -0
- package/dist/explorer-trace-toggle.js +8 -0
- package/dist/explorer-trace-toggle.js.map +1 -0
- package/dist/explorer-viewport.d.ts +69 -0
- package/dist/explorer-viewport.d.ts.map +1 -0
- package/dist/explorer-viewport.js +69 -0
- package/dist/explorer-viewport.js.map +1 -0
- package/dist/explorer-views.d.ts +24 -0
- package/dist/explorer-views.d.ts.map +1 -0
- package/dist/explorer-views.js +16 -0
- package/dist/explorer-views.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/isomorphic-layout-effect.d.ts +7 -0
- package/dist/isomorphic-layout-effect.d.ts.map +1 -0
- package/dist/isomorphic-layout-effect.js +7 -0
- package/dist/isomorphic-layout-effect.js.map +1 -0
- package/dist/labels.d.ts +67 -0
- package/dist/labels.d.ts.map +1 -0
- package/dist/labels.js +50 -0
- package/dist/labels.js.map +1 -0
- package/dist/layout.d.ts +82 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +244 -0
- package/dist/layout.js.map +1 -0
- package/dist/navigation.d.ts +20 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +44 -0
- package/dist/navigation.js.map +1 -0
- package/dist/root.d.ts +89 -0
- package/dist/root.d.ts.map +1 -0
- package/dist/root.js +401 -0
- package/dist/root.js.map +1 -0
- package/dist/search.d.ts +19 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +31 -0
- package/dist/search.js.map +1 -0
- package/dist/size.d.ts +12 -0
- package/dist/size.d.ts.map +1 -0
- package/dist/size.js +20 -0
- package/dist/size.js.map +1 -0
- package/dist/svg-base.d.ts +33 -0
- package/dist/svg-base.d.ts.map +1 -0
- package/dist/svg-base.js +89 -0
- package/dist/svg-base.js.map +1 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/use-explorer-camera.d.ts +56 -0
- package/dist/use-explorer-camera.d.ts.map +1 -0
- package/dist/use-explorer-camera.js +641 -0
- package/dist/use-explorer-camera.js.map +1 -0
- package/dist/use-explorer.d.ts +23 -0
- package/dist/use-explorer.d.ts.map +1 -0
- package/dist/use-explorer.js +26 -0
- package/dist/use-explorer.js.map +1 -0
- package/dist/validate.d.ts +20 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +99 -0
- package/dist/validate.js.map +1 -0
- package/dist/viewport-surface.d.ts +90 -0
- package/dist/viewport-surface.d.ts.map +1 -0
- package/dist/viewport-surface.js +485 -0
- package/dist/viewport-surface.js.map +1 -0
- package/dist/visible-set.d.ts +65 -0
- package/dist/visible-set.d.ts.map +1 -0
- package/dist/visible-set.js +168 -0
- package/dist/visible-set.js.map +1 -0
- package/package.json +62 -0
- package/src/base.ts +67 -0
- package/src/camera.ts +206 -0
- package/src/context.ts +168 -0
- package/src/dagr-explorer.tsx +85 -0
- package/src/errors.ts +85 -0
- package/src/explorer-details.tsx +156 -0
- package/src/explorer-search.tsx +140 -0
- package/src/explorer-toolbar.tsx +77 -0
- package/src/explorer-trace-toggle.tsx +34 -0
- package/src/explorer-viewport.tsx +181 -0
- package/src/explorer-views.tsx +61 -0
- package/src/index.ts +76 -0
- package/src/isomorphic-layout-effect.ts +7 -0
- package/src/labels.ts +109 -0
- package/src/layout.ts +298 -0
- package/src/navigation.ts +51 -0
- package/src/root.tsx +544 -0
- package/src/search.ts +36 -0
- package/src/size.ts +23 -0
- package/src/svg-base.tsx +173 -0
- package/src/types.ts +69 -0
- package/src/use-explorer-camera.ts +675 -0
- package/src/use-explorer.ts +32 -0
- package/src/validate.ts +173 -0
- package/src/viewport-surface.tsx +650 -0
- package/src/visible-set.ts +225 -0
- package/styles.css +253 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ExplorerViewport`: the graph surface, wired to the root.
|
|
3
|
+
*
|
|
4
|
+
* It is the internal `ViewportSurface` with its props filled from the root:
|
|
5
|
+
* the active view and its layout, the selection, the dimming, and the
|
|
6
|
+
* activation callbacks (a click inspects, a double click flies to the node).
|
|
7
|
+
* The camera controls and the camera source go back up to the root, so
|
|
8
|
+
* `ExplorerApi`'s camera methods and the toolbar's readout reach them.
|
|
9
|
+
*
|
|
10
|
+
* **One per root, counted in an effect.** The viewport registers with its
|
|
11
|
+
* root in an effect with cleanup, and the second registration throws.
|
|
12
|
+
* Counting in render would misfire under StrictMode's double render, and
|
|
13
|
+
* when Suspense keeps an old tree alive beside a new one.
|
|
14
|
+
*
|
|
15
|
+
* **A view switch remounts the surface,** keyed by view id. That is the
|
|
16
|
+
* camera reset the spec asks of a switch: a new surface fits its layout on
|
|
17
|
+
* its first measure, with no flight from a camera framed on other content.
|
|
18
|
+
*
|
|
19
|
+
* **The surface renders only for what it shows.** It is memoized, and given
|
|
20
|
+
* only the state it draws (the view, the layout, the selection, the dimming)
|
|
21
|
+
* and the root's stable methods, so a keystroke that changes neither the
|
|
22
|
+
* dimming nor the selection does not render the graph.
|
|
23
|
+
*
|
|
24
|
+
* **A stage for overlays.** The surface sits in a positioned element with
|
|
25
|
+
* the part's children, such as `ExplorerDetails`, which position against it.
|
|
26
|
+
* The hint, the graph's description, comes after the stage, so an overlay
|
|
27
|
+
* never covers it.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { memo, useEffect, useId, useRef } from 'react';
|
|
31
|
+
import type { CSSProperties, ReactElement, ReactNode } from 'react';
|
|
32
|
+
import type { ExplorerBase, ExplorerCameraSource } from './base.js';
|
|
33
|
+
import { useExplorerApiContext, useExplorerContext } from './context.js';
|
|
34
|
+
import type { ExplorerApi, ExplorerInternals } from './context.js';
|
|
35
|
+
import type { ExplorerLayout } from './layout.js';
|
|
36
|
+
import type { ExplorerEdge, ExplorerGroup, ExplorerNode, ExplorerView } from './types.js';
|
|
37
|
+
import { ViewportSurface } from './viewport-surface.js';
|
|
38
|
+
import type { ExplorerTier, ExplorerTiers } from './visible-set.js';
|
|
39
|
+
|
|
40
|
+
/** Only the node type: nothing here takes an edge. */
|
|
41
|
+
export interface ExplorerViewportProps<N extends ExplorerNode = ExplorerNode> {
|
|
42
|
+
/**
|
|
43
|
+
* A node's content, for nodes large enough on screen to read. It sits
|
|
44
|
+
* inside the explorer's own node button, so it must not be interactive.
|
|
45
|
+
* Default: the label.
|
|
46
|
+
*/
|
|
47
|
+
readonly renderNode?:
|
|
48
|
+
| ((node: N, context: { readonly tier: ExplorerTier; readonly selected: boolean; readonly dimmed: boolean }) => ReactNode)
|
|
49
|
+
| undefined;
|
|
50
|
+
/** A node's accessible name. Default: its label, then `labels.inGroup(group)` for each group. */
|
|
51
|
+
readonly nodeAriaLabel?:
|
|
52
|
+
| ((node: N, context: { readonly groups: readonly ExplorerGroup[] }) => string)
|
|
53
|
+
| undefined;
|
|
54
|
+
/** On-screen widths, in CSS pixels, at which a node becomes a summary and then rich. Default 56 and 200. */
|
|
55
|
+
readonly tiers?: ExplorerTiers | undefined;
|
|
56
|
+
/** The most node elements mounted at once, before pins. Default 200. */
|
|
57
|
+
readonly maxOverlayNodes?: number | undefined;
|
|
58
|
+
/** What draws the nodes that have no element. Default: the SVG base. Experimental. */
|
|
59
|
+
readonly base?: ExplorerBase | undefined;
|
|
60
|
+
readonly className?: string | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Sizing and decoration pass through: `{ height: 600 }` replaces
|
|
63
|
+
* `--dagr-explorer-height`. `position` and `overflow` stay the viewport's
|
|
64
|
+
* own, because the graph needs them.
|
|
65
|
+
*/
|
|
66
|
+
readonly style?: CSSProperties | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Overlays positioned against the graph, such as `ExplorerDetails`. They
|
|
69
|
+
* share a positioned stage with the graph, and the hint comes after it.
|
|
70
|
+
*/
|
|
71
|
+
readonly children?: ReactNode;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface PaneProps<N extends ExplorerNode, E extends ExplorerEdge>
|
|
75
|
+
extends Omit<ExplorerViewportProps<N>, 'children'> {
|
|
76
|
+
readonly label: string;
|
|
77
|
+
readonly inGroup: (groupLabel: string) => string;
|
|
78
|
+
readonly view: ExplorerView<N, E>;
|
|
79
|
+
readonly layout: ExplorerLayout;
|
|
80
|
+
readonly selectedId: string | null;
|
|
81
|
+
readonly dimmed: ReadonlySet<string>;
|
|
82
|
+
readonly api: ExplorerApi;
|
|
83
|
+
readonly internals: ExplorerInternals;
|
|
84
|
+
readonly describedBy: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* One surface for one view, attached to the root's camera hub while it lives.
|
|
89
|
+
* Memoized: every prop is stable unless what it draws changes.
|
|
90
|
+
*/
|
|
91
|
+
const ViewportPane = memo(function ViewportPane<N extends ExplorerNode, E extends ExplorerEdge>(
|
|
92
|
+
props: PaneProps<N, E>,
|
|
93
|
+
): ReactElement {
|
|
94
|
+
const { api, internals, describedBy, ...rest } = props;
|
|
95
|
+
const sourceRef = useRef<ExplorerCameraSource | null>(null);
|
|
96
|
+
|
|
97
|
+
// The surface's own effects have run by now, so its source is in the ref.
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
const source = sourceRef.current;
|
|
100
|
+
return source === null ? undefined : internals.camera.attach(source);
|
|
101
|
+
}, [internals]);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<ViewportSurface<N, E>
|
|
105
|
+
{...rest}
|
|
106
|
+
onNodeActivate={(id, trigger) => api.inspect(id, trigger)}
|
|
107
|
+
onNodeZoom={(id) => api.focusNode(id)}
|
|
108
|
+
controlsRef={internals.controlsRef}
|
|
109
|
+
cameraSourceRef={sourceRef}
|
|
110
|
+
describedBy={describedBy}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
}) as <N extends ExplorerNode, E extends ExplorerEdge>(props: PaneProps<N, E>) => ReactElement;
|
|
114
|
+
|
|
115
|
+
const STAGE: CSSProperties = { position: 'relative' };
|
|
116
|
+
|
|
117
|
+
export function ExplorerViewport<N extends ExplorerNode = ExplorerNode, E extends ExplorerEdge = ExplorerEdge>(
|
|
118
|
+
props: ExplorerViewportProps<N>,
|
|
119
|
+
): ReactElement {
|
|
120
|
+
const { children, ...rest } = props;
|
|
121
|
+
const { state, internals } = useExplorerContext('ExplorerViewport');
|
|
122
|
+
const api = useExplorerApiContext('ExplorerViewport');
|
|
123
|
+
const describedBy = useId();
|
|
124
|
+
|
|
125
|
+
useEffect(() => internals.registerViewport(), [internals]);
|
|
126
|
+
|
|
127
|
+
// The part's type parameters are a claim about the root's data. See context.ts.
|
|
128
|
+
const view = state.activeView as ExplorerView<N, E> | null;
|
|
129
|
+
const { layout, labels } = state;
|
|
130
|
+
|
|
131
|
+
const empty = view === null || layout === null || view.nodes.length === 0;
|
|
132
|
+
|
|
133
|
+
// One shape for both states, so the stage and its children survive a view
|
|
134
|
+
// that empties and fills again.
|
|
135
|
+
return (
|
|
136
|
+
<>
|
|
137
|
+
<div data-dagr-explorer="stage" style={STAGE}>
|
|
138
|
+
{empty ? (
|
|
139
|
+
<div
|
|
140
|
+
data-dagr-explorer="viewport"
|
|
141
|
+
data-empty="true"
|
|
142
|
+
role="region"
|
|
143
|
+
aria-label={state.label}
|
|
144
|
+
className={rest.className}
|
|
145
|
+
style={{
|
|
146
|
+
position: 'relative',
|
|
147
|
+
height: 'var(--dagr-explorer-height, 480px)',
|
|
148
|
+
display: 'flex',
|
|
149
|
+
alignItems: 'center',
|
|
150
|
+
justifyContent: 'center',
|
|
151
|
+
...rest.style,
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
{view === null ? labels.noViews : labels.emptyView}
|
|
155
|
+
</div>
|
|
156
|
+
) : (
|
|
157
|
+
<ViewportPane<N, E>
|
|
158
|
+
key={view.id}
|
|
159
|
+
{...rest}
|
|
160
|
+
label={state.label}
|
|
161
|
+
inGroup={labels.inGroup}
|
|
162
|
+
view={view}
|
|
163
|
+
layout={layout}
|
|
164
|
+
selectedId={state.selectedId}
|
|
165
|
+
dimmed={state.dimmed}
|
|
166
|
+
api={api}
|
|
167
|
+
internals={internals}
|
|
168
|
+
describedBy={describedBy}
|
|
169
|
+
/>
|
|
170
|
+
)}
|
|
171
|
+
{children}
|
|
172
|
+
</div>
|
|
173
|
+
{empty ? null : (
|
|
174
|
+
<p id={describedBy} data-dagr-explorer="hint">
|
|
175
|
+
<span>{labels.stats({ nodes: view.nodes.length, edges: view.edges.length })}</span>{' '}
|
|
176
|
+
<span>{labels.hint}</span>
|
|
177
|
+
</p>
|
|
178
|
+
)}
|
|
179
|
+
</>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ExplorerViews`: the view switcher. It renders nothing for a single view,
|
|
3
|
+
* slot or not, because there is nothing to switch to.
|
|
4
|
+
*
|
|
5
|
+
* The default is a group of buttons, the active one `aria-pressed`. Buttons
|
|
6
|
+
* rather than tabs: tabs promise arrow-key movement and a tab panel, and the
|
|
7
|
+
* panel here is the whole explorer.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CSSProperties, ReactElement, ReactNode } from 'react';
|
|
11
|
+
import { useExplorerContext } from './context.js';
|
|
12
|
+
import type { ExplorerEdge, ExplorerNode, ExplorerView } from './types.js';
|
|
13
|
+
|
|
14
|
+
/** What the switcher's slot receives. */
|
|
15
|
+
export interface ExplorerViewsContext<N extends ExplorerNode = ExplorerNode, E extends ExplorerEdge = ExplorerEdge> {
|
|
16
|
+
readonly views: readonly ExplorerView<N, E>[];
|
|
17
|
+
readonly activeView: ExplorerView<N, E>;
|
|
18
|
+
readonly selectView: (id: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ExplorerViewsProps<N extends ExplorerNode = ExplorerNode, E extends ExplorerEdge = ExplorerEdge> {
|
|
22
|
+
/** The switcher's content. Default: a button per view. */
|
|
23
|
+
readonly children?: ((context: ExplorerViewsContext<N, E>) => ReactNode) | undefined;
|
|
24
|
+
readonly className?: string | undefined;
|
|
25
|
+
readonly style?: CSSProperties | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function ExplorerViews<N extends ExplorerNode = ExplorerNode, E extends ExplorerEdge = ExplorerEdge>(
|
|
29
|
+
props: ExplorerViewsProps<N, E>,
|
|
30
|
+
): ReactElement | null {
|
|
31
|
+
const { children, className, style } = props;
|
|
32
|
+
const { state } = useExplorerContext('ExplorerViews');
|
|
33
|
+
// The part's type parameters are a claim about the root's data. See context.ts.
|
|
34
|
+
const views = state.views as unknown as readonly ExplorerView<N, E>[];
|
|
35
|
+
const activeView = state.activeView as ExplorerView<N, E> | null;
|
|
36
|
+
if (views.length < 2 || activeView === null) return null;
|
|
37
|
+
|
|
38
|
+
if (children !== undefined) {
|
|
39
|
+
return (
|
|
40
|
+
<div data-dagr-explorer="views" className={className} style={style}>
|
|
41
|
+
{children({ views, activeView, selectView: state.selectView })}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div data-dagr-explorer="views" role="group" aria-label={state.labels.views} className={className} style={style}>
|
|
48
|
+
{views.map((view) => (
|
|
49
|
+
<button
|
|
50
|
+
key={view.id}
|
|
51
|
+
type="button"
|
|
52
|
+
data-view-id={view.id}
|
|
53
|
+
aria-pressed={view.id === activeView.id}
|
|
54
|
+
onClick={() => state.selectView(view.id)}
|
|
55
|
+
>
|
|
56
|
+
{view.label}
|
|
57
|
+
</button>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@prnt/dagr-explorer`: an interactive graph explorer.
|
|
3
|
+
*
|
|
4
|
+
* Two ways in, from one implementation: `DagrExplorer`, preassembled, and
|
|
5
|
+
* the named parts it is built from (`ExplorerRoot`, `ExplorerViewport`,
|
|
6
|
+
* `ExplorerSearch` and the rest) with `useExplorer()`, for a host that owns
|
|
7
|
+
* the layout. Beside them, the headless core: the data model, validation,
|
|
8
|
+
* layout into world coordinates, and search, none of which touches the DOM.
|
|
9
|
+
*
|
|
10
|
+
* The optional stylesheet is its own entry, `@prnt/dagr-explorer/styles.css`.
|
|
11
|
+
* Nothing here imports it.
|
|
12
|
+
*
|
|
13
|
+
* From `@prnt/dagr-render` this package imports only the `core` entry, which
|
|
14
|
+
* never loads three.js. `test/imports.test.ts` holds that.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export { DagrExplorer } from './dagr-explorer.js';
|
|
18
|
+
export type { DagrExplorerProps } from './dagr-explorer.js';
|
|
19
|
+
export { ExplorerRoot } from './root.js';
|
|
20
|
+
export type { ExplorerRootProps } from './root.js';
|
|
21
|
+
export { ExplorerViews } from './explorer-views.js';
|
|
22
|
+
export type { ExplorerViewsContext, ExplorerViewsProps } from './explorer-views.js';
|
|
23
|
+
export { ExplorerSearch } from './explorer-search.js';
|
|
24
|
+
export type { ExplorerSearchProps } from './explorer-search.js';
|
|
25
|
+
export { ExplorerViewport } from './explorer-viewport.js';
|
|
26
|
+
export type { ExplorerViewportProps } from './explorer-viewport.js';
|
|
27
|
+
export { ExplorerDetails } from './explorer-details.js';
|
|
28
|
+
export type {
|
|
29
|
+
ExplorerConnection,
|
|
30
|
+
ExplorerDetailsContext,
|
|
31
|
+
ExplorerDetailsProps,
|
|
32
|
+
} from './explorer-details.js';
|
|
33
|
+
export { ExplorerToolbar } from './explorer-toolbar.js';
|
|
34
|
+
export type { ExplorerToolbarProps } from './explorer-toolbar.js';
|
|
35
|
+
export { ExplorerTraceToggle } from './explorer-trace-toggle.js';
|
|
36
|
+
export type { ExplorerTraceToggleProps } from './explorer-trace-toggle.js';
|
|
37
|
+
export { useExplorer, useExplorerApi } from './use-explorer.js';
|
|
38
|
+
export type { ExplorerApi, ExplorerState } from './context.js';
|
|
39
|
+
export { DEFAULT_EXPLORER_LABELS } from './labels.js';
|
|
40
|
+
export type { ExplorerLabels } from './labels.js';
|
|
41
|
+
// The base-layer seam is experimental until a native base confirms it.
|
|
42
|
+
export type {
|
|
43
|
+
ExplorerBase,
|
|
44
|
+
ExplorerBaseProps,
|
|
45
|
+
ExplorerCameraSource,
|
|
46
|
+
ExplorerEmphasis,
|
|
47
|
+
} from './base.js';
|
|
48
|
+
// The camera and the visible set as types only, because the public api
|
|
49
|
+
// already hands them out: `ExplorerCameraSource.get()` returns a camera, and
|
|
50
|
+
// a base layer is given a visible set (experimental, like the seam above). The
|
|
51
|
+
// camera arithmetic and the visible set computation stay internal.
|
|
52
|
+
export type { ExplorerCamera } from './camera.js';
|
|
53
|
+
export type { ExplorerTier, ExplorerTiers, ExplorerVisibleSet } from './visible-set.js';
|
|
54
|
+
|
|
55
|
+
export { ExplorerContextError, ExplorerDataError } from './errors.js';
|
|
56
|
+
export type { DagrExplorerErrorCode, ExplorerContextErrorCode } from './errors.js';
|
|
57
|
+
// `layoutKey` and the fixed spacing constants stay internal on purpose. A
|
|
58
|
+
// public constant cannot change value, or become an option, without a break.
|
|
59
|
+
export { DEFAULT_NODE_SEP, DEFAULT_RANK_SEP, layoutView } from './layout.js';
|
|
60
|
+
export type {
|
|
61
|
+
ExplorerBox,
|
|
62
|
+
ExplorerLayout,
|
|
63
|
+
LayoutViewOptions,
|
|
64
|
+
} from './layout.js';
|
|
65
|
+
export { defaultSearchText, searchNodes } from './search.js';
|
|
66
|
+
export { DEFAULT_NODE_SIZE, resolveNodeSize } from './size.js';
|
|
67
|
+
export { validateView, validateViews } from './validate.js';
|
|
68
|
+
export type {
|
|
69
|
+
ExplorerEdge,
|
|
70
|
+
ExplorerGroup,
|
|
71
|
+
ExplorerLayoutOptions,
|
|
72
|
+
ExplorerNode,
|
|
73
|
+
ExplorerView,
|
|
74
|
+
Size,
|
|
75
|
+
} from './types.js';
|
|
76
|
+
export type { Vec2 } from '@prnt/dagr-render/core';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A layout effect in the browser and a plain one on a server, where React 18
|
|
5
|
+
* warns that a layout effect does nothing.
|
|
6
|
+
*/
|
|
7
|
+
export const useIsomorphicLayoutEffect = typeof document === 'undefined' ? useEffect : useLayoutEffect;
|
package/src/labels.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every string the parts show or announce, in one object.
|
|
3
|
+
*
|
|
4
|
+
* No part writes copy of its own: a host that translates, or names things in
|
|
5
|
+
* its own voice, passes `labels` to `ExplorerRoot` and replaces any subset.
|
|
6
|
+
* Counts and names arrive through formatters, so a language with other plural
|
|
7
|
+
* rules, or another word order, is a function and not a template.
|
|
8
|
+
*
|
|
9
|
+
* The defaults are neutral English and name no host framework.
|
|
10
|
+
*/
|
|
11
|
+
export interface ExplorerLabels {
|
|
12
|
+
/** The search field's label. */
|
|
13
|
+
readonly search: string;
|
|
14
|
+
readonly searchPlaceholder: string;
|
|
15
|
+
/** The accessible name of the search result list. */
|
|
16
|
+
readonly searchResults: string;
|
|
17
|
+
/** The live match count, shown while the query is not blank. */
|
|
18
|
+
readonly matches: (count: number) => string;
|
|
19
|
+
/** The line under a capped result list: how many matches it does not show. */
|
|
20
|
+
readonly moreMatches: (count: number) => string;
|
|
21
|
+
/** The graph's node and edge counts, part of its accessible description. */
|
|
22
|
+
readonly stats: (counts: { readonly nodes: number; readonly edges: number }) => string;
|
|
23
|
+
/** How to use the graph, the rest of its accessible description. */
|
|
24
|
+
readonly hint: string;
|
|
25
|
+
/** The accessible name of the view switcher. */
|
|
26
|
+
readonly views: string;
|
|
27
|
+
/** One group a node is in, as part of the node's default accessible name. */
|
|
28
|
+
readonly inGroup: (groupLabel: string) => string;
|
|
29
|
+
/** The trace toggle while trace is off, so pressing it turns trace on. */
|
|
30
|
+
readonly traceOn: string;
|
|
31
|
+
/** The trace toggle while trace is on. */
|
|
32
|
+
readonly traceOff: string;
|
|
33
|
+
/** The accessible name of the zoom toolbar. */
|
|
34
|
+
readonly zoomControls: string;
|
|
35
|
+
readonly zoomIn: string;
|
|
36
|
+
readonly zoomOut: string;
|
|
37
|
+
/** The zoom readout, from the zoom as a whole percent. */
|
|
38
|
+
readonly zoomLevel: (percent: number) => string;
|
|
39
|
+
readonly fit: string;
|
|
40
|
+
/** The zoom-to-selected button, with a node selected. */
|
|
41
|
+
readonly zoomTo: (label: string) => string;
|
|
42
|
+
/** The zoom-to-selected button, disabled, with nothing selected. */
|
|
43
|
+
readonly zoomToSelected: string;
|
|
44
|
+
/** Shown in place of the graph for a view with no nodes. */
|
|
45
|
+
readonly emptyView: string;
|
|
46
|
+
/** Shown in place of the graph when there are no views. */
|
|
47
|
+
readonly noViews: string;
|
|
48
|
+
/** The drawer's title and accessible name. */
|
|
49
|
+
readonly drawerTitle: string;
|
|
50
|
+
/** The drawer's close button. */
|
|
51
|
+
readonly close: string;
|
|
52
|
+
/** The heading of the drawer's default connection list. */
|
|
53
|
+
readonly connections: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const plural = (count: number, one: string, many: string): string =>
|
|
57
|
+
`${String(count)} ${count === 1 ? one : many}`;
|
|
58
|
+
|
|
59
|
+
export const DEFAULT_EXPLORER_LABELS: ExplorerLabels = Object.freeze({
|
|
60
|
+
search: 'Search nodes',
|
|
61
|
+
searchPlaceholder: 'Name or id',
|
|
62
|
+
searchResults: 'Search results',
|
|
63
|
+
matches: (count: number) => (count === 0 ? 'No matches' : plural(count, 'match', 'matches')),
|
|
64
|
+
moreMatches: (count: number) => plural(count, 'more match', 'more matches'),
|
|
65
|
+
stats: ({ nodes, edges }: { readonly nodes: number; readonly edges: number }) =>
|
|
66
|
+
`${plural(nodes, 'node', 'nodes')}, ${plural(edges, 'edge', 'edges')}.`,
|
|
67
|
+
hint: 'Search reaches every node. In the graph, the arrow keys move between nodes and Shift with an arrow pans. Click the graph to zoom with the wheel, drag to pan, and press Escape to leave it.',
|
|
68
|
+
views: 'Views',
|
|
69
|
+
inGroup: (groupLabel: string) => `in ${groupLabel}`,
|
|
70
|
+
traceOn: 'Trace connections',
|
|
71
|
+
traceOff: 'Stop tracing',
|
|
72
|
+
zoomControls: 'Zoom',
|
|
73
|
+
zoomIn: 'Zoom in',
|
|
74
|
+
zoomOut: 'Zoom out',
|
|
75
|
+
zoomLevel: (percent: number) => `${String(percent)}%`,
|
|
76
|
+
fit: 'Fit',
|
|
77
|
+
zoomTo: (label: string) => `Zoom to ${label}`,
|
|
78
|
+
zoomToSelected: 'Zoom to selection',
|
|
79
|
+
emptyView: 'This view has no nodes.',
|
|
80
|
+
noViews: 'Nothing to show.',
|
|
81
|
+
drawerTitle: 'Details',
|
|
82
|
+
close: 'Close',
|
|
83
|
+
connections: 'Connections',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The defaults with the caller's overrides on top. A key the caller set to
|
|
88
|
+
* `undefined`, which a JavaScript caller can do, keeps its default rather
|
|
89
|
+
* than rendering nothing.
|
|
90
|
+
*/
|
|
91
|
+
export function resolveLabels(overrides: Partial<ExplorerLabels> | undefined): ExplorerLabels {
|
|
92
|
+
if (overrides === undefined) return DEFAULT_EXPLORER_LABELS;
|
|
93
|
+
const defined = Object.fromEntries(
|
|
94
|
+
Object.entries(overrides).filter(([, value]) => value !== undefined),
|
|
95
|
+
) as Partial<ExplorerLabels>;
|
|
96
|
+
return { ...DEFAULT_EXPLORER_LABELS, ...defined };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Whether two label objects hold the same value under every key, by `Object.is`. */
|
|
100
|
+
export function sameLabels(a: ExplorerLabels, b: ExplorerLabels): boolean {
|
|
101
|
+
if (a === b) return true;
|
|
102
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
103
|
+
for (const key of keys) {
|
|
104
|
+
if (!Object.is((a as unknown as Record<string, unknown>)[key], (b as unknown as Record<string, unknown>)[key])) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|