@modernrelay/orbit-react 0.2.0
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/LICENSE +21 -0
- package/dist/GraphProvider-B8ITDXS0.d.ts +11 -0
- package/dist/chunk-7XB47YST.js +69 -0
- package/dist/chunk-7XB47YST.js.map +1 -0
- package/dist/chunk-DGFLU2SN.js +108 -0
- package/dist/chunk-DGFLU2SN.js.map +1 -0
- package/dist/chunk-JN7QCMFW.js +93 -0
- package/dist/chunk-JN7QCMFW.js.map +1 -0
- package/dist/chunk-QJ7JXQVK.js +119 -0
- package/dist/chunk-QJ7JXQVK.js.map +1 -0
- package/dist/components/ContextMenu.d.ts +73 -0
- package/dist/components/ContextMenu.js +367 -0
- package/dist/components/ContextMenu.js.map +1 -0
- package/dist/components/Histogram.d.ts +33 -0
- package/dist/components/Histogram.js +216 -0
- package/dist/components/Histogram.js.map +1 -0
- package/dist/components/Inspector.d.ts +63 -0
- package/dist/components/Inspector.js +246 -0
- package/dist/components/Inspector.js.map +1 -0
- package/dist/components/Legend.d.ts +45 -0
- package/dist/components/Legend.js +185 -0
- package/dist/components/Legend.js.map +1 -0
- package/dist/components/Minimap.d.ts +63 -0
- package/dist/components/Minimap.js +222 -0
- package/dist/components/Minimap.js.map +1 -0
- package/dist/components/Navigator.d.ts +90 -0
- package/dist/components/Navigator.js +381 -0
- package/dist/components/Navigator.js.map +1 -0
- package/dist/components/Search.d.ts +70 -0
- package/dist/components/Search.js +203 -0
- package/dist/components/Search.js.map +1 -0
- package/dist/components/SelectionActions.d.ts +36 -0
- package/dist/components/SelectionActions.js +126 -0
- package/dist/components/SelectionActions.js.map +1 -0
- package/dist/components/SimControls.d.ts +124 -0
- package/dist/components/SimControls.js +221 -0
- package/dist/components/SimControls.js.map +1 -0
- package/dist/components/Table.d.ts +92 -0
- package/dist/components/Table.js +505 -0
- package/dist/components/Table.js.map +1 -0
- package/dist/components/Timeline.d.ts +34 -0
- package/dist/components/Timeline.js +182 -0
- package/dist/components/Timeline.js.map +1 -0
- package/dist/components/Toolbar.d.ts +60 -0
- package/dist/components/Toolbar.js +220 -0
- package/dist/components/Toolbar.js.map +1 -0
- package/dist/components/Tooltip.d.ts +65 -0
- package/dist/components/Tooltip.js +178 -0
- package/dist/components/Tooltip.js.map +1 -0
- package/dist/index.d.ts +488 -0
- package/dist/index.js +827 -0
- package/dist/index.js.map +1 -0
- package/dist/shared-Bj3Gk219.d.ts +43 -0
- package/package.json +99 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { getSearchResultUnavailableCallback } from '../chunk-QJ7JXQVK.js';
|
|
2
|
+
import '../chunk-JN7QCMFW.js';
|
|
3
|
+
import { useResolvedInstance } from '../chunk-7XB47YST.js';
|
|
4
|
+
import { useId, useState, useRef, useEffect, useCallback, useSyncExternalStore } from 'react';
|
|
5
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
|
+
|
|
7
|
+
function useSearchSlice(instance) {
|
|
8
|
+
const subscribe = useCallback(
|
|
9
|
+
(onChange) => instance.store.subscribe(onChange),
|
|
10
|
+
[instance]
|
|
11
|
+
);
|
|
12
|
+
const getSnapshot = useCallback(
|
|
13
|
+
() => instance.store.getState().search,
|
|
14
|
+
[instance]
|
|
15
|
+
);
|
|
16
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
17
|
+
}
|
|
18
|
+
var DEBOUNCE_MS_DEFAULT = 200;
|
|
19
|
+
var LIMIT_DEFAULT = 20;
|
|
20
|
+
var ROOT_STYLE = {
|
|
21
|
+
pointerEvents: "auto",
|
|
22
|
+
position: "relative",
|
|
23
|
+
display: "flex",
|
|
24
|
+
flexDirection: "column",
|
|
25
|
+
gap: 4,
|
|
26
|
+
maxWidth: 320,
|
|
27
|
+
font: "13px/1.4 system-ui, sans-serif"
|
|
28
|
+
};
|
|
29
|
+
var LIST_STYLE = {
|
|
30
|
+
margin: 0,
|
|
31
|
+
padding: 4,
|
|
32
|
+
listStyle: "none",
|
|
33
|
+
overflowY: "auto",
|
|
34
|
+
maxHeight: 240,
|
|
35
|
+
borderRadius: 8,
|
|
36
|
+
background: "rgba(23, 25, 32, 0.96)",
|
|
37
|
+
border: "1px solid rgba(255, 255, 255, 0.14)",
|
|
38
|
+
color: "#e8eaf0"
|
|
39
|
+
};
|
|
40
|
+
var OPTION_STYLE = {
|
|
41
|
+
display: "flex",
|
|
42
|
+
justifyContent: "space-between",
|
|
43
|
+
gap: 8,
|
|
44
|
+
padding: "4px 8px",
|
|
45
|
+
borderRadius: 6,
|
|
46
|
+
cursor: "pointer",
|
|
47
|
+
whiteSpace: "nowrap",
|
|
48
|
+
overflow: "hidden",
|
|
49
|
+
textOverflow: "ellipsis"
|
|
50
|
+
};
|
|
51
|
+
var ACTIVE_OPTION_STYLE = {
|
|
52
|
+
...OPTION_STYLE,
|
|
53
|
+
outline: "2px solid currentColor",
|
|
54
|
+
outlineOffset: -2
|
|
55
|
+
};
|
|
56
|
+
var SCORE_STYLE = { opacity: 0.65, fontVariantNumeric: "tabular-nums" };
|
|
57
|
+
var EMPTY_STYLE = { padding: "4px 8px", fontStyle: "italic", opacity: 0.8 };
|
|
58
|
+
function GraphSearch(props) {
|
|
59
|
+
const instance = useResolvedInstance(props.instance, "<GraphSearch>");
|
|
60
|
+
const search = useSearchSlice(instance);
|
|
61
|
+
const debounceMs = props.debounceMs ?? DEBOUNCE_MS_DEFAULT;
|
|
62
|
+
const limit = props.limit ?? LIMIT_DEFAULT;
|
|
63
|
+
const baseId = useId();
|
|
64
|
+
const listboxId = `${baseId}search-listbox`;
|
|
65
|
+
const [query, setQuery] = useState("");
|
|
66
|
+
const [open, setOpen] = useState(false);
|
|
67
|
+
const [activeIndex, setActiveIndex] = useState(-1);
|
|
68
|
+
const timerRef = useRef(null);
|
|
69
|
+
const cancelPending = () => {
|
|
70
|
+
if (timerRef.current !== null) {
|
|
71
|
+
clearTimeout(timerRef.current);
|
|
72
|
+
timerRef.current = null;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
useEffect(() => cancelPending, [instance]);
|
|
76
|
+
const results = search === null ? [] : search.results;
|
|
77
|
+
const current = search !== null && search.query === query;
|
|
78
|
+
const expanded = open && search !== null;
|
|
79
|
+
const activeOptionId = expanded && activeIndex >= 0 && activeIndex < results.length ? `${baseId}search-opt-${activeIndex}` : void 0;
|
|
80
|
+
const onChange = (e) => {
|
|
81
|
+
const value = e.target.value;
|
|
82
|
+
setQuery(value);
|
|
83
|
+
setActiveIndex(-1);
|
|
84
|
+
cancelPending();
|
|
85
|
+
if (value === "") {
|
|
86
|
+
setOpen(false);
|
|
87
|
+
instance.clearSearch();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
setOpen(true);
|
|
91
|
+
timerRef.current = setTimeout(() => {
|
|
92
|
+
timerRef.current = null;
|
|
93
|
+
void instance.search(value, { limit }).catch(() => {
|
|
94
|
+
});
|
|
95
|
+
}, debounceMs);
|
|
96
|
+
};
|
|
97
|
+
const clearAll = () => {
|
|
98
|
+
cancelPending();
|
|
99
|
+
setQuery("");
|
|
100
|
+
setOpen(false);
|
|
101
|
+
setActiveIndex(-1);
|
|
102
|
+
instance.clearSearch();
|
|
103
|
+
};
|
|
104
|
+
const activate = (result) => {
|
|
105
|
+
const activation = instance.activateSearchResult(result);
|
|
106
|
+
if (activation.status === "focused") {
|
|
107
|
+
setOpen(false);
|
|
108
|
+
setActiveIndex(-1);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const cb = props.onResultUnavailable ?? getSearchResultUnavailableCallback(instance);
|
|
112
|
+
cb?.(result, activation.reason);
|
|
113
|
+
};
|
|
114
|
+
const onKeyDown = (e) => {
|
|
115
|
+
switch (e.key) {
|
|
116
|
+
case "ArrowDown":
|
|
117
|
+
if (results.length === 0) return;
|
|
118
|
+
setOpen(true);
|
|
119
|
+
setActiveIndex((i) => Math.min(i + 1, results.length - 1));
|
|
120
|
+
break;
|
|
121
|
+
case "ArrowUp":
|
|
122
|
+
if (results.length === 0) return;
|
|
123
|
+
setActiveIndex((i) => Math.max(i - 1, 0));
|
|
124
|
+
break;
|
|
125
|
+
case "Home":
|
|
126
|
+
if (!expanded || results.length === 0) return;
|
|
127
|
+
setActiveIndex(0);
|
|
128
|
+
break;
|
|
129
|
+
case "End":
|
|
130
|
+
if (!expanded || results.length === 0) return;
|
|
131
|
+
setActiveIndex(results.length - 1);
|
|
132
|
+
break;
|
|
133
|
+
case "Enter": {
|
|
134
|
+
if (!current) return;
|
|
135
|
+
const target = results[activeIndex] ?? results[0];
|
|
136
|
+
if (target === void 0) return;
|
|
137
|
+
activate(target);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case "Escape":
|
|
141
|
+
clearAll();
|
|
142
|
+
break;
|
|
143
|
+
default:
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
};
|
|
148
|
+
const boxLabel = props.label ?? "Search graph";
|
|
149
|
+
return /* @__PURE__ */ jsxs(
|
|
150
|
+
"div",
|
|
151
|
+
{
|
|
152
|
+
"data-orbit-search": "",
|
|
153
|
+
className: props.className,
|
|
154
|
+
style: { ...ROOT_STYLE, ...props.style },
|
|
155
|
+
children: [
|
|
156
|
+
/* @__PURE__ */ jsx(
|
|
157
|
+
"input",
|
|
158
|
+
{
|
|
159
|
+
"data-orbit-search-input": "",
|
|
160
|
+
type: "text",
|
|
161
|
+
role: "combobox",
|
|
162
|
+
"aria-label": boxLabel,
|
|
163
|
+
"aria-expanded": expanded,
|
|
164
|
+
"aria-controls": listboxId,
|
|
165
|
+
"aria-activedescendant": activeOptionId,
|
|
166
|
+
"aria-autocomplete": "list",
|
|
167
|
+
autoComplete: "off",
|
|
168
|
+
spellCheck: false,
|
|
169
|
+
placeholder: props.placeholder,
|
|
170
|
+
value: query,
|
|
171
|
+
onChange,
|
|
172
|
+
onKeyDown
|
|
173
|
+
}
|
|
174
|
+
),
|
|
175
|
+
expanded ? /* @__PURE__ */ jsx("ul", { id: listboxId, role: "listbox", "aria-label": `${boxLabel} results`, style: LIST_STYLE, children: results.length === 0 ? /* @__PURE__ */ jsx("li", { "data-orbit-search-empty": "", role: "presentation", style: EMPTY_STYLE, children: "No results" }) : results.map((r, index) => /* @__PURE__ */ jsx(
|
|
176
|
+
"li",
|
|
177
|
+
{
|
|
178
|
+
id: `${baseId}search-opt-${index}`,
|
|
179
|
+
role: "option",
|
|
180
|
+
"aria-selected": index === activeIndex,
|
|
181
|
+
"data-orbit-search-result": r.id,
|
|
182
|
+
style: index === activeIndex ? ACTIVE_OPTION_STYLE : OPTION_STYLE,
|
|
183
|
+
onPointerEnter: () => {
|
|
184
|
+
setActiveIndex(index);
|
|
185
|
+
},
|
|
186
|
+
onClick: () => {
|
|
187
|
+
activate(r);
|
|
188
|
+
},
|
|
189
|
+
children: props.renderResult !== void 0 ? props.renderResult(r) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
190
|
+
/* @__PURE__ */ jsx("span", { "data-orbit-search-result-label": "", children: r.label ?? r.id }),
|
|
191
|
+
r.score !== void 0 ? /* @__PURE__ */ jsx("span", { "data-orbit-search-result-score": "", style: SCORE_STYLE, children: String(r.score) }) : null
|
|
192
|
+
] })
|
|
193
|
+
},
|
|
194
|
+
`${index}:${r.id}`
|
|
195
|
+
)) }) : null
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export { GraphSearch };
|
|
202
|
+
//# sourceMappingURL=Search.js.map
|
|
203
|
+
//# sourceMappingURL=Search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/Search/index.tsx"],"names":[],"mappings":"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","file":"Search.js","sourcesContent":["/**\n * `@modernrelay/orbit-react/components/Search` — `<GraphSearch>`, the §16.5\n * search box (S11-T08).\n *\n * Owns exactly the UI half of the §16.5 split: the text input, the debounce\n * window, and the result list. Everything correctness-critical lives in the\n * instance (`instance.search` creates the RequestContext, caches by declared\n * revisions, cancels superseded work, and rejects stale results at admission\n * — §9.2); this component just issues ONE `instance.search` per settled\n * debounce window and renders `store.search`.\n *\n * Behavior:\n * - typing schedules `instance.search(query, {limit})` after `debounceMs`\n * (default 200); a keystroke inside the window replaces the pending call —\n * one service call per settled window, never one per keystroke;\n * - emptying the input cancels the pending call and clears the shared slice\n * (`instance.clearSearch()`), so the navigator section disappears too;\n * - the result list reads `store.search` (the §16.5 single source of truth —\n * bespoke UIs and the navigator observe the same slice); `score` renders\n * when the service supplied one;\n * - Enter / click activates via `instance.activateSearchResult`: 'focused'\n * closes the listbox and clears the active-option highlight; otherwise\n * `onResultUnavailable(result, reason)` fires — falling back to the\n * `<Graph onSearchResultUnavailable>` default when no local prop is given\n * (spec §16.5 result contract: the HOST reacts; search never mutates\n * scope/filters behind the user's back);\n * - I6 (query coherence): Enter only activates when `store.search.query`\n * matches the current input — after an edit, the previous query's rows may\n * keep showing through the debounce window, but Enter is a no-op until the\n * edited query's publication lands (clicks on visible rows stay live);\n * - Escape clears: input text, pending debounce, active option, and the\n * store slice (`instance.clearSearch`).\n *\n * ARIA: the combobox pattern — `role=\"combobox\"` input with\n * `aria-expanded`/`aria-controls`/`aria-activedescendant` over a\n * `role=\"listbox\"` of `role=\"option\"` rows; ArrowDown/ArrowUp move the active\n * option (no wrap), Home/End jump. Option DOM ids are index-derived (node ids\n * are untrusted text). All result-derived text (label, id, score) renders as\n * TEXT NODES only (§14) — `renderResult` is the customization escape hatch.\n */\n\nimport { useCallback, useEffect, useId, useRef, useState, useSyncExternalStore } from 'react';\nimport type {\n ChangeEvent,\n CSSProperties,\n KeyboardEvent as ReactKeyboardEvent,\n ReactElement,\n ReactNode,\n} from 'react';\nimport type { GraphStoreState, SearchResult, SearchUnavailableReason } from '@modernrelay/orbit-core';\nimport type { AnyGraphInstance } from '../../GraphProvider';\nimport { useResolvedInstance } from '../shared';\nimport { getSearchResultUnavailableCallback } from '../../hooks';\n\n/** Store slice subscription local to this component (the ambient hooks in\n * ../../hooks require the provider; here the instance may be an explicit\n * prop). `store.search` is a stable reference per publication, so the raw\n * selector needs no memo. */\nfunction useSearchSlice(\n instance: AnyGraphInstance,\n): { query: string; results: readonly SearchResult[] } | null {\n const subscribe = useCallback(\n (onChange: () => void) => instance.store.subscribe(onChange),\n [instance],\n );\n const getSnapshot = useCallback(\n (): GraphStoreState['search'] => instance.store.getState().search,\n [instance],\n );\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n\n/** §16.5 defaults (mirror the instance's SEARCH_LIMIT_DEFAULT). */\nconst DEBOUNCE_MS_DEFAULT = 200;\nconst LIMIT_DEFAULT = 20;\n\nexport interface GraphSearchProps {\n /** Explicit instance (multi-instance pages); ambient context otherwise. */\n instance?: AnyGraphInstance;\n placeholder?: string;\n /** Debounce window in ms — one `instance.search` per settled window. Default 200. */\n debounceMs?: number;\n /** Result cap forwarded to the service (§16.5). Default 20. */\n limit?: number;\n /**\n * §16.5 result contract: fired when an activated result cannot be focused\n * ('not-loaded' | 'out-of-scope' | 'filtered'). Defaults to the\n * `<Graph onSearchResultUnavailable>` prop when omitted.\n */\n onResultUnavailable?: (result: SearchResult, reason: SearchUnavailableReason) => void;\n /** Custom option content (rendered INSIDE the option row). Default: label\n * text node + score (when present). */\n renderResult?: (r: SearchResult) => ReactNode;\n /** Accessible name of the search box. Default 'Search graph'. */\n label?: string;\n className?: string;\n style?: CSSProperties;\n}\n\n// --- styling defaults (headless-styleable: className/style override) ---\nconst ROOT_STYLE: CSSProperties = {\n pointerEvents: 'auto',\n position: 'relative',\n display: 'flex',\n flexDirection: 'column',\n gap: 4,\n maxWidth: 320,\n font: '13px/1.4 system-ui, sans-serif',\n};\nconst LIST_STYLE: CSSProperties = {\n margin: 0,\n padding: 4,\n listStyle: 'none',\n overflowY: 'auto',\n maxHeight: 240,\n borderRadius: 8,\n background: 'rgba(23, 25, 32, 0.96)',\n border: '1px solid rgba(255, 255, 255, 0.14)',\n color: '#e8eaf0',\n};\nconst OPTION_STYLE: CSSProperties = {\n display: 'flex',\n justifyContent: 'space-between',\n gap: 8,\n padding: '4px 8px',\n borderRadius: 6,\n cursor: 'pointer',\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n};\nconst ACTIVE_OPTION_STYLE: CSSProperties = {\n ...OPTION_STYLE,\n outline: '2px solid currentColor',\n outlineOffset: -2,\n};\nconst SCORE_STYLE: CSSProperties = { opacity: 0.65, fontVariantNumeric: 'tabular-nums' };\nconst EMPTY_STYLE: CSSProperties = { padding: '4px 8px', fontStyle: 'italic', opacity: 0.8 };\n\nexport function GraphSearch(props: GraphSearchProps): ReactElement {\n const instance = useResolvedInstance(props.instance, '<GraphSearch>');\n const search = useSearchSlice(instance);\n\n const debounceMs = props.debounceMs ?? DEBOUNCE_MS_DEFAULT;\n const limit = props.limit ?? LIMIT_DEFAULT;\n\n const baseId = useId();\n const listboxId = `${baseId}search-listbox`;\n\n const [query, setQuery] = useState('');\n /** Listbox visibility — closed by 'focused' activation / Escape until the\n * next keystroke reopens it. */\n const [open, setOpen] = useState(false);\n /** Active option index; -1 = no active option (highlight cleared). */\n const [activeIndex, setActiveIndex] = useState(-1);\n\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const cancelPending = (): void => {\n if (timerRef.current !== null) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n };\n\n // Unmount/instance-swap hygiene: never fire a search for a dead widget.\n useEffect(() => cancelPending, [instance]);\n\n const results: readonly SearchResult[] = search === null ? [] : search.results;\n /** I6 query coherence: the published slice answers the CURRENT input only\n * when its query matches what this box sent to `instance.search` (the raw\n * input value — neither side normalizes). Stale results may keep showing\n * through the debounce window (combobox convention: don't blank the\n * listbox), but implicit activation is gated on this. */\n const current = search !== null && search.query === query;\n const expanded = open && search !== null;\n const activeOptionId =\n expanded && activeIndex >= 0 && activeIndex < results.length\n ? `${baseId}search-opt-${activeIndex}`\n : undefined;\n\n const onChange = (e: ChangeEvent<HTMLInputElement>): void => {\n const value = e.target.value;\n setQuery(value);\n setActiveIndex(-1);\n cancelPending();\n if (value === '') {\n // Input emptied: clear the shared slice immediately (§16.5) — no\n // trailing search may resurrect stale results.\n setOpen(false);\n instance.clearSearch();\n return;\n }\n setOpen(true);\n timerRef.current = setTimeout(() => {\n timerRef.current = null;\n // Supersede/staleness rejections are §16.5 by-design outcomes — the\n // diagnostics channel carries the why; the box just shows the latest\n // completed publication via store.search.\n void instance.search(value, { limit }).catch(() => {});\n }, debounceMs);\n };\n\n const clearAll = (): void => {\n cancelPending();\n setQuery('');\n setOpen(false);\n setActiveIndex(-1);\n instance.clearSearch();\n };\n\n const activate = (result: SearchResult): void => {\n const activation = instance.activateSearchResult(result);\n if (activation.status === 'focused') {\n // Focused: close the listbox and clear the highlight (§16.5).\n setOpen(false);\n setActiveIndex(-1);\n return;\n }\n const cb = props.onResultUnavailable ?? getSearchResultUnavailableCallback(instance);\n cb?.(result, activation.reason);\n };\n\n const onKeyDown = (e: ReactKeyboardEvent<HTMLInputElement>): void => {\n switch (e.key) {\n case 'ArrowDown':\n if (results.length === 0) return;\n setOpen(true);\n setActiveIndex((i) => Math.min(i + 1, results.length - 1));\n break;\n case 'ArrowUp':\n if (results.length === 0) return;\n setActiveIndex((i) => Math.max(i - 1, 0));\n break;\n case 'Home':\n if (!expanded || results.length === 0) return;\n setActiveIndex(0);\n break;\n case 'End':\n if (!expanded || results.length === 0) return;\n setActiveIndex(results.length - 1);\n break;\n case 'Enter': {\n // I6: Enter (and its first-result fallback) activates only against a\n // publication answering the CURRENT input — while the edited query is\n // still in its debounce window, Enter is a no-op (input and listbox\n // untouched). Pointer clicks on visible rows stay unguarded: the user\n // activates exactly what they saw.\n if (!current) return;\n const target = results[activeIndex] ?? results[0];\n if (target === undefined) return;\n activate(target);\n break;\n }\n case 'Escape':\n clearAll();\n break;\n default:\n return;\n }\n e.preventDefault();\n };\n\n const boxLabel = props.label ?? 'Search graph';\n\n return (\n <div\n data-orbit-search=\"\"\n className={props.className}\n style={{ ...ROOT_STYLE, ...props.style }}\n >\n <input\n data-orbit-search-input=\"\"\n type=\"text\"\n role=\"combobox\"\n aria-label={boxLabel}\n aria-expanded={expanded}\n aria-controls={listboxId}\n aria-activedescendant={activeOptionId}\n aria-autocomplete=\"list\"\n autoComplete=\"off\"\n spellCheck={false}\n placeholder={props.placeholder}\n value={query}\n onChange={onChange}\n onKeyDown={onKeyDown}\n />\n {expanded ? (\n <ul id={listboxId} role=\"listbox\" aria-label={`${boxLabel} results`} style={LIST_STYLE}>\n {results.length === 0 ? (\n <li data-orbit-search-empty=\"\" role=\"presentation\" style={EMPTY_STYLE}>\n No results\n </li>\n ) : (\n results.map((r, index) => (\n <li\n key={`${index}:${r.id}`}\n id={`${baseId}search-opt-${index}`}\n role=\"option\"\n aria-selected={index === activeIndex}\n data-orbit-search-result={r.id}\n style={index === activeIndex ? ACTIVE_OPTION_STYLE : OPTION_STYLE}\n onPointerEnter={() => {\n setActiveIndex(index);\n }}\n onClick={() => {\n activate(r);\n }}\n >\n {props.renderResult !== undefined ? (\n props.renderResult(r)\n ) : (\n <>\n {/* Result text is untrusted — TEXT NODES only (§14). */}\n <span data-orbit-search-result-label=\"\">{r.label ?? r.id}</span>\n {r.score !== undefined ? (\n <span data-orbit-search-result-score=\"\" style={SCORE_STYLE}>\n {String(r.score)}\n </span>\n ) : null}\n </>\n )}\n </li>\n ))\n )}\n </ul>\n ) : null}\n </div>\n );\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { CSSProperties, ReactElement } from 'react';
|
|
2
|
+
import { A as AnyGraphInstance } from '../GraphProvider-B8ITDXS0.js';
|
|
3
|
+
import { G as GraphCorner } from '../shared-Bj3Gk219.js';
|
|
4
|
+
import '@modernrelay/orbit-core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* <GraphSelectionActions> — §16.2 selection action panel (S7-T09).
|
|
8
|
+
*
|
|
9
|
+
* Renders ONLY while the selection is non-empty: a count plus Select
|
|
10
|
+
* neighbors / Invert / Clear / Hide selected / Pin selected / Unpin selected,
|
|
11
|
+
* all driven through the instance mutators so it stays correct under
|
|
12
|
+
* controlled selection (§6.4 — mutators degrade to intents). Headless-
|
|
13
|
+
* styleable via `className`/`buttonClassName` (providing them drops the
|
|
14
|
+
* default dark-theme inline styles).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
interface GraphSelectionActionsProps {
|
|
18
|
+
/** Explicit instance; defaults to the ambient <Graph>/<GraphProvider> one. */
|
|
19
|
+
instance?: AnyGraphInstance;
|
|
20
|
+
/** Class hook for the panel root; providing it drops the default styles. */
|
|
21
|
+
/** Corner this panel anchors to. Default 'bottom-left'. */
|
|
22
|
+
position?: GraphCorner;
|
|
23
|
+
/** Distance from the two anchored edges in CSS px. Default 12. */
|
|
24
|
+
offset?: number;
|
|
25
|
+
/** Class hook for the root; providing it drops the default styles
|
|
26
|
+
* INCLUDING placement — position it yourself in CSS. */
|
|
27
|
+
className?: string;
|
|
28
|
+
/** Style overrides merged over the defaults; an inset you set here
|
|
29
|
+
* wins over the one `position` implies (resolved per axis). */
|
|
30
|
+
style?: CSSProperties;
|
|
31
|
+
/** Class hook for each button; providing it drops the default styles. */
|
|
32
|
+
buttonClassName?: string;
|
|
33
|
+
}
|
|
34
|
+
declare function GraphSelectionActions(props: GraphSelectionActionsProps): ReactElement | null;
|
|
35
|
+
|
|
36
|
+
export { GraphSelectionActions, type GraphSelectionActionsProps };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { useResolvedInstance, mergeStyle, cornerStyle } from '../chunk-7XB47YST.js';
|
|
2
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
|
3
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
var PANEL_STYLE = {
|
|
6
|
+
display: "flex",
|
|
7
|
+
alignItems: "center",
|
|
8
|
+
gap: 2,
|
|
9
|
+
padding: 4,
|
|
10
|
+
borderRadius: 8,
|
|
11
|
+
background: "rgba(23, 25, 32, 0.92)",
|
|
12
|
+
border: "1px solid rgba(255, 255, 255, 0.14)",
|
|
13
|
+
boxShadow: "0 4px 16px rgba(0, 0, 0, 0.4)",
|
|
14
|
+
color: "#e8eaf0",
|
|
15
|
+
font: "12px/1.4 system-ui, sans-serif"
|
|
16
|
+
};
|
|
17
|
+
var COUNT_STYLE = {
|
|
18
|
+
padding: "2px 8px",
|
|
19
|
+
opacity: 0.75,
|
|
20
|
+
whiteSpace: "nowrap"
|
|
21
|
+
};
|
|
22
|
+
var BUTTON_STYLE = {
|
|
23
|
+
appearance: "none",
|
|
24
|
+
background: "transparent",
|
|
25
|
+
border: "none",
|
|
26
|
+
borderRadius: 6,
|
|
27
|
+
color: "inherit",
|
|
28
|
+
cursor: "pointer",
|
|
29
|
+
font: "inherit",
|
|
30
|
+
minHeight: 26,
|
|
31
|
+
padding: "2px 8px",
|
|
32
|
+
whiteSpace: "nowrap"
|
|
33
|
+
};
|
|
34
|
+
function GraphSelectionActions(props) {
|
|
35
|
+
const instance = useResolvedInstance(props.instance, "<GraphSelectionActions>");
|
|
36
|
+
const subscribe = useCallback(
|
|
37
|
+
(onStoreChange) => instance.store.subscribe(onStoreChange),
|
|
38
|
+
[instance]
|
|
39
|
+
);
|
|
40
|
+
const getSelection = useCallback(() => instance.store.getState().selection, [instance]);
|
|
41
|
+
const selection = useSyncExternalStore(subscribe, getSelection, getSelection);
|
|
42
|
+
const count = selection.nodeIds.length + selection.edgeIds.length + selection.groupIds.length;
|
|
43
|
+
if (count === 0) return null;
|
|
44
|
+
const nodeIds = selection.nodeIds;
|
|
45
|
+
const actions = [
|
|
46
|
+
{
|
|
47
|
+
id: "select-neighbors",
|
|
48
|
+
label: "Select neighbors",
|
|
49
|
+
run: () => {
|
|
50
|
+
instance.selectNeighbors();
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "invert",
|
|
55
|
+
label: "Invert",
|
|
56
|
+
run: () => {
|
|
57
|
+
instance.invertSelection();
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "clear",
|
|
62
|
+
label: "Clear",
|
|
63
|
+
run: () => {
|
|
64
|
+
instance.clearSelection();
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "hide",
|
|
69
|
+
label: "Hide selected",
|
|
70
|
+
run: () => {
|
|
71
|
+
instance.hideNodes(nodeIds);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "pin",
|
|
76
|
+
label: "Pin selected",
|
|
77
|
+
run: () => {
|
|
78
|
+
for (const id of nodeIds) instance.pinNode(id);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "unpin",
|
|
83
|
+
label: "Unpin selected",
|
|
84
|
+
run: () => {
|
|
85
|
+
for (const id of nodeIds) instance.unpinNode(id);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
];
|
|
89
|
+
return /* @__PURE__ */ jsxs(
|
|
90
|
+
"div",
|
|
91
|
+
{
|
|
92
|
+
"data-orbit-selection-actions": "",
|
|
93
|
+
role: "group",
|
|
94
|
+
"aria-label": "Selection actions",
|
|
95
|
+
className: props.className,
|
|
96
|
+
style: props.className !== void 0 ? props.style : mergeStyle(
|
|
97
|
+
{ ...cornerStyle(props.position ?? "bottom-left", props.offset), ...PANEL_STYLE },
|
|
98
|
+
props.style
|
|
99
|
+
),
|
|
100
|
+
children: [
|
|
101
|
+
/* @__PURE__ */ jsxs("span", { "data-orbit-selection-count": "", style: props.className !== void 0 ? void 0 : COUNT_STYLE, children: [
|
|
102
|
+
count,
|
|
103
|
+
" selected"
|
|
104
|
+
] }),
|
|
105
|
+
actions.map((action) => /* @__PURE__ */ jsx(
|
|
106
|
+
"button",
|
|
107
|
+
{
|
|
108
|
+
type: "button",
|
|
109
|
+
"data-orbit-selection-action": action.id,
|
|
110
|
+
className: props.buttonClassName,
|
|
111
|
+
style: props.buttonClassName !== void 0 ? void 0 : BUTTON_STYLE,
|
|
112
|
+
onClick: () => {
|
|
113
|
+
action.run();
|
|
114
|
+
},
|
|
115
|
+
children: action.label
|
|
116
|
+
},
|
|
117
|
+
action.id
|
|
118
|
+
))
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export { GraphSelectionActions };
|
|
125
|
+
//# sourceMappingURL=SelectionActions.js.map
|
|
126
|
+
//# sourceMappingURL=SelectionActions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/SelectionActions/index.tsx"],"names":[],"mappings":"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","file":"SelectionActions.js","sourcesContent":["/**\n * <GraphSelectionActions> — §16.2 selection action panel (S7-T09).\n *\n * Renders ONLY while the selection is non-empty: a count plus Select\n * neighbors / Invert / Clear / Hide selected / Pin selected / Unpin selected,\n * all driven through the instance mutators so it stays correct under\n * controlled selection (§6.4 — mutators degrade to intents). Headless-\n * styleable via `className`/`buttonClassName` (providing them drops the\n * default dark-theme inline styles).\n */\n\nimport { useCallback, useSyncExternalStore } from 'react';\nimport type { CSSProperties, ReactElement } from 'react';\nimport type { AnyGraphInstance } from '../../GraphProvider';\nimport { cornerStyle, mergeStyle, useResolvedInstance } from '../shared';\nimport type { GraphCorner } from '../shared';\n\nexport interface GraphSelectionActionsProps {\n /** Explicit instance; defaults to the ambient <Graph>/<GraphProvider> one. */\n instance?: AnyGraphInstance;\n /** Class hook for the panel root; providing it drops the default styles. */\n /** Corner this panel anchors to. Default 'bottom-left'. */\n position?: GraphCorner;\n /** Distance from the two anchored edges in CSS px. Default 12. */\n offset?: number;\n /** Class hook for the root; providing it drops the default styles\n * INCLUDING placement — position it yourself in CSS. */\n className?: string;\n /** Style overrides merged over the defaults; an inset you set here\n * wins over the one `position` implies (resolved per axis). */\n style?: CSSProperties;\n /** Class hook for each button; providing it drops the default styles. */\n buttonClassName?: string;\n}\n\ninterface SelectionAction {\n id: string;\n label: string;\n run(): void;\n}\n\n// --- default dark-theme styling (dropped when class hooks are provided) ---\n\nconst PANEL_STYLE: CSSProperties = {\n display: 'flex',\n alignItems: 'center',\n gap: 2,\n padding: 4,\n borderRadius: 8,\n background: 'rgba(23, 25, 32, 0.92)',\n border: '1px solid rgba(255, 255, 255, 0.14)',\n boxShadow: '0 4px 16px rgba(0, 0, 0, 0.4)',\n color: '#e8eaf0',\n font: '12px/1.4 system-ui, sans-serif',\n};\n\nconst COUNT_STYLE: CSSProperties = {\n padding: '2px 8px',\n opacity: 0.75,\n whiteSpace: 'nowrap',\n};\n\nconst BUTTON_STYLE: CSSProperties = {\n appearance: 'none',\n background: 'transparent',\n border: 'none',\n borderRadius: 6,\n color: 'inherit',\n cursor: 'pointer',\n font: 'inherit',\n minHeight: 26,\n padding: '2px 8px',\n whiteSpace: 'nowrap',\n};\n\nexport function GraphSelectionActions(props: GraphSelectionActionsProps): ReactElement | null {\n const instance = useResolvedInstance(props.instance, '<GraphSelectionActions>');\n\n // The store keeps `selection` referentially stable across unrelated\n // publishes, so the raw field is a valid useSyncExternalStore snapshot.\n const subscribe = useCallback(\n (onStoreChange: () => void) => instance.store.subscribe(onStoreChange),\n [instance],\n );\n const getSelection = useCallback(() => instance.store.getState().selection, [instance]);\n const selection = useSyncExternalStore(subscribe, getSelection, getSelection);\n\n const count = selection.nodeIds.length + selection.edgeIds.length + selection.groupIds.length;\n if (count === 0) return null;\n\n const nodeIds = selection.nodeIds;\n const actions: SelectionAction[] = [\n {\n id: 'select-neighbors',\n label: 'Select neighbors',\n run: () => {\n instance.selectNeighbors();\n },\n },\n {\n id: 'invert',\n label: 'Invert',\n run: () => {\n instance.invertSelection();\n },\n },\n {\n id: 'clear',\n label: 'Clear',\n run: () => {\n instance.clearSelection();\n },\n },\n {\n id: 'hide',\n label: 'Hide selected',\n run: () => {\n instance.hideNodes(nodeIds);\n },\n },\n {\n id: 'pin',\n label: 'Pin selected',\n run: () => {\n for (const id of nodeIds) instance.pinNode(id);\n },\n },\n {\n id: 'unpin',\n label: 'Unpin selected',\n run: () => {\n for (const id of nodeIds) instance.unpinNode(id);\n },\n },\n ];\n\n return (\n <div\n data-orbit-selection-actions=\"\"\n role=\"group\"\n aria-label=\"Selection actions\"\n className={props.className}\n style={\n props.className !== undefined\n ? props.style\n : mergeStyle(\n { ...cornerStyle(props.position ?? 'bottom-left', props.offset), ...PANEL_STYLE, },\n props.style,\n )\n }\n >\n <span data-orbit-selection-count=\"\" style={props.className !== undefined ? undefined : COUNT_STYLE}>\n {count} selected\n </span>\n {actions.map((action) => (\n <button\n key={action.id}\n type=\"button\"\n data-orbit-selection-action={action.id}\n className={props.buttonClassName}\n style={props.buttonClassName !== undefined ? undefined : BUTTON_STYLE}\n onClick={() => {\n action.run();\n }}\n >\n {action.label}\n </button>\n ))}\n </div>\n );\n}\n"]}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { ReactNode, CSSProperties, ReactElement } from 'react';
|
|
2
|
+
import { LayoutKind, SimulationConfig } from '@modernrelay/orbit-core';
|
|
3
|
+
import { A as AnyGraphInstance } from '../GraphProvider-B8ITDXS0.js';
|
|
4
|
+
import { G as GraphCorner } from '../shared-Bj3Gk219.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `@modernrelay/orbit-react/components/SimControls` — `<GraphSimControls>`,
|
|
8
|
+
* the §16.11 simulation control panel (S12-T12).
|
|
9
|
+
*
|
|
10
|
+
* Live sliders over every `SimulationConfig` field (gravity, repulsion,
|
|
11
|
+
* friction, linkDistance, linkSpring, collision, decay, repulsionTheta,
|
|
12
|
+
* center — the full §10 tunable set), a speed
|
|
13
|
+
* preset toggle, and a reheat button. Every control write is ONE atomic
|
|
14
|
+
* host update — `applyHostUpdate({ simulation: merged })`, the same lane the
|
|
15
|
+
* `<Graph>` `simulation` prop uses — which sinks as a single config-only
|
|
16
|
+
* `engine.commit`: no structure, no buffers, no restart, positions untouched
|
|
17
|
+
* (R-16.11-02, E1).
|
|
18
|
+
*
|
|
19
|
+
* ## Current-value resolution (v0.10, investigated)
|
|
20
|
+
* The store publishes NO simulation/layout lane and the instance exposes no
|
|
21
|
+
* config getter — `layout`/`simulation` are instance-local in
|
|
22
|
+
* `createGraphInstance`; `GraphStoreState` carries only `simulationRunning`.
|
|
23
|
+
* The panel therefore mirrors the caller's `simulation` prop (the same
|
|
24
|
+
* object handed to `<Graph>`) and merges control writes over that mirror; a
|
|
25
|
+
* prop identity change re-seeds the mirror (the external writer wins). Like
|
|
26
|
+
* `subgraph` (§9.2), the lane is last-writer-wins: the panel and the
|
|
27
|
+
* `<Graph>` prop write the same state. `onSimulationChange` fires with the
|
|
28
|
+
* full merged config after every write so controlled callers can loop it
|
|
29
|
+
* back through their `simulation` prop (the spec's `onLayoutChange` seam
|
|
30
|
+
* traffics in the LayoutSpec object form and is post-v0.10).
|
|
31
|
+
*
|
|
32
|
+
* ## Applicability (R-16.11-04)
|
|
33
|
+
* Rendered ONLY under layout `'force'` — the `layout` prop mirrors the
|
|
34
|
+
* caller's `<Graph>` prop for the same no-observable-lane reason, defaulting
|
|
35
|
+
* to `'force'` (the instance construction default). Under `'fixed'` the
|
|
36
|
+
* panel renders nothing. The spec's third case — `static` while its
|
|
37
|
+
* convergence run is active — is N/A in v0.10: `LayoutKind` is
|
|
38
|
+
* `'force' | 'fixed'` (core types.ts) and the store exposes no
|
|
39
|
+
* convergence-run state; wire it when the `static` variant lands.
|
|
40
|
+
*
|
|
41
|
+
* ## Reheat
|
|
42
|
+
* The spec names `reheat(alpha)`; the shipped instance surface is
|
|
43
|
+
* `resumeSimulation()` (§16.1 simulation controls — sinks to
|
|
44
|
+
* `engine.start()` and publishes `simulationRunning`). The button calls
|
|
45
|
+
* that; the alpha parameter follows when core grows the reheat API.
|
|
46
|
+
*
|
|
47
|
+
* ## Speed toggle
|
|
48
|
+
* A preset over the `decay` lane — the §10 cool-down tunable (cosmos
|
|
49
|
+
* `simulationDecay`), which core exposes as of v0.10.2. ON writes a smaller
|
|
50
|
+
* coefficient so the run cools SLOWER and stays lively; OFF restores the
|
|
51
|
+
* pre-toggle value, or the engine default when the field was unset. The
|
|
52
|
+
* toggle and the Cool-down slider are two views over one lane, so moving
|
|
53
|
+
* either is reflected by the other. Same config-only commit discipline as
|
|
54
|
+
* every slider. (Before v0.10.2 this proxied through `friction` because
|
|
55
|
+
* `decay` had no core lane — that workaround is now retired.)
|
|
56
|
+
*
|
|
57
|
+
* ## Keyboard & ARIA (§15.1)
|
|
58
|
+
* Native `<input type="range">`/checkbox controls with `<label htmlFor>`
|
|
59
|
+
* wiring. Arrow/Home/End/Page keys are handled in `onKeyDown` with
|
|
60
|
+
* `preventDefault()` so stepping is deterministic both in browsers (never
|
|
61
|
+
* double-steps against the native range keyboard model) and under jsdom
|
|
62
|
+
* (which has no native range keyboard model at all).
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
type SimFieldKey = keyof SimulationConfig;
|
|
66
|
+
interface SimFieldDescriptor {
|
|
67
|
+
key: SimFieldKey;
|
|
68
|
+
/** Visible label text (rendered as a text node). */
|
|
69
|
+
label: string;
|
|
70
|
+
min: number;
|
|
71
|
+
max: number;
|
|
72
|
+
step: number;
|
|
73
|
+
/** Displayed when the field is unset — the engine default the simulation
|
|
74
|
+
* is actually running with (cosmos 3.3.0 `defaultConfigValues`). Never
|
|
75
|
+
* WRITTEN unless the user moves the control. */
|
|
76
|
+
fallback: number;
|
|
77
|
+
}
|
|
78
|
+
/** §16.11 slider roster = the full v0.10 `SimulationConfig` field list. */
|
|
79
|
+
declare const SIM_FIELDS: readonly SimFieldDescriptor[];
|
|
80
|
+
/** Speed-preset friction values (module JSDoc: the toggle's documented
|
|
81
|
+
* simulation field is `friction` until core ships `decay`). */
|
|
82
|
+
declare const SPEED_NORMAL_DECAY = 5000;
|
|
83
|
+
/** A smaller decay coefficient cools SLOWER, so the run keeps moving —
|
|
84
|
+
* "fast" here means "stays lively", matching the §10 semantics. */
|
|
85
|
+
declare const SPEED_FAST_DECAY = 1000;
|
|
86
|
+
interface GraphSimControlsProps {
|
|
87
|
+
/** Explicit instance (multi-instance pages); ambient context otherwise. */
|
|
88
|
+
instance?: AnyGraphInstance;
|
|
89
|
+
/** Active layout kind — mirror of the caller's <Graph> `layout` prop
|
|
90
|
+
* (no observable lane exists; module JSDoc). Default 'force'. */
|
|
91
|
+
layout?: LayoutKind;
|
|
92
|
+
/** Current simulation config seed — the same object passed to <Graph>.
|
|
93
|
+
* An identity change re-seeds the panel's mirror. */
|
|
94
|
+
simulation?: SimulationConfig;
|
|
95
|
+
/** Fires with the FULL merged SimulationConfig after every write. */
|
|
96
|
+
onSimulationChange?: (next: SimulationConfig) => void;
|
|
97
|
+
/** Render-prop REPLACEMENT for a field row; the default renders
|
|
98
|
+
* label + native range input + value readout. `set` clamps/quantizes to
|
|
99
|
+
* the descriptor's bounds and step before writing. */
|
|
100
|
+
renderField?: (ctx: {
|
|
101
|
+
field: SimFieldDescriptor;
|
|
102
|
+
value: number;
|
|
103
|
+
/** Stable id for `htmlFor` wiring in custom rows. */
|
|
104
|
+
inputId: string;
|
|
105
|
+
set: (value: number) => void;
|
|
106
|
+
}) => ReactNode;
|
|
107
|
+
/** Accessible name of the panel. Default 'Simulation controls'. */
|
|
108
|
+
label?: string;
|
|
109
|
+
/** Corner this panel anchors to. Default 'bottom-left'. */
|
|
110
|
+
position?: GraphCorner;
|
|
111
|
+
/** Distance from the two anchored edges in CSS px. Default 12. */
|
|
112
|
+
offset?: number;
|
|
113
|
+
/** Class hook for the root; providing it drops the default styles
|
|
114
|
+
* INCLUDING placement — position it yourself in CSS. */
|
|
115
|
+
className?: string;
|
|
116
|
+
/** Style overrides merged over the defaults; an inset you set here
|
|
117
|
+
* wins over the one `position` implies (resolved per axis). */
|
|
118
|
+
style?: CSSProperties;
|
|
119
|
+
}
|
|
120
|
+
declare function GraphSimControls(props: GraphSimControlsProps): ReactElement | null;
|
|
121
|
+
/** S0-T02 tree-shake probe — must never reach a root-only consumer bundle. */
|
|
122
|
+
declare const __ORBIT_SIMCONTROLS_SENTINEL__ = "orbit-react/components/SimControls";
|
|
123
|
+
|
|
124
|
+
export { GraphSimControls, type GraphSimControlsProps, SIM_FIELDS, SPEED_FAST_DECAY, SPEED_NORMAL_DECAY, type SimFieldDescriptor, type SimFieldKey, __ORBIT_SIMCONTROLS_SENTINEL__ };
|