@sentropic/design-system-svelte 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Accordion.svelte +11 -3
- package/dist/Alert.svelte +46 -7
- package/dist/Badge.svelte +16 -7
- package/dist/Breadcrumb.svelte +11 -1
- package/dist/Button.svelte +22 -2
- package/dist/Card.svelte +7 -0
- package/dist/Checkbox.svelte +32 -8
- package/dist/Checkbox.svelte.d.ts.map +1 -1
- package/dist/ForceGraph.svelte +422 -0
- package/dist/ForceGraph.svelte.d.ts +54 -0
- package/dist/ForceGraph.svelte.d.ts.map +1 -0
- package/dist/Header.svelte +241 -2
- package/dist/Header.svelte.d.ts +37 -0
- package/dist/Header.svelte.d.ts.map +1 -1
- package/dist/Input.svelte +20 -4
- package/dist/Link.svelte +6 -0
- package/dist/Pagination.svelte +24 -6
- package/dist/Radio.svelte +38 -6
- package/dist/Search.svelte +13 -1
- package/dist/Select.svelte +40 -5
- package/dist/Switch.svelte +11 -9
- package/dist/Tabs.svelte +24 -6
- package/dist/Tag.svelte +18 -10
- package/dist/Textarea.svelte +14 -3
- package/dist/Toggle.svelte +17 -6
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/package.json +4 -4
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type ForceGraphTone =
|
|
3
|
+
| "category1" | "category2" | "category3" | "category4"
|
|
4
|
+
| "category5" | "category6" | "category7" | "category8";
|
|
5
|
+
|
|
6
|
+
export type ForceGraphNode = {
|
|
7
|
+
/** Stable identifier; referenced by edges. */
|
|
8
|
+
id: string;
|
|
9
|
+
/** Visible label (falls back to id). */
|
|
10
|
+
label?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Grouping key (e.g. node type or community). Nodes sharing a group get
|
|
13
|
+
* the same tone when `tone` is not set explicitly.
|
|
14
|
+
*/
|
|
15
|
+
group?: string | number;
|
|
16
|
+
/** Explicit data-vis tone; overrides the group-derived tone. */
|
|
17
|
+
tone?: ForceGraphTone;
|
|
18
|
+
/** Relative node radius weight (defaults to 1). */
|
|
19
|
+
weight?: number;
|
|
20
|
+
/** Pin the node to a fixed position (ignored by the simulation). */
|
|
21
|
+
fx?: number;
|
|
22
|
+
fy?: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type ForceGraphEdge = {
|
|
26
|
+
/** Source node id. */
|
|
27
|
+
source: string;
|
|
28
|
+
/** Target node id. */
|
|
29
|
+
target: string;
|
|
30
|
+
/** Optional relation label, surfaced in the tooltip on hover/focus. */
|
|
31
|
+
relation?: string;
|
|
32
|
+
/**
|
|
33
|
+
* When true the link renders as a dashed/faded "weak" link. Lets callers
|
|
34
|
+
* map a confidence dimension onto link strength without extra props.
|
|
35
|
+
*/
|
|
36
|
+
weak?: boolean;
|
|
37
|
+
};
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<script lang="ts">
|
|
41
|
+
type ForceGraphProps = {
|
|
42
|
+
nodes: ForceGraphNode[];
|
|
43
|
+
edges: ForceGraphEdge[];
|
|
44
|
+
/** Accessible name for the figure (required). */
|
|
45
|
+
label: string;
|
|
46
|
+
width?: number;
|
|
47
|
+
height?: number;
|
|
48
|
+
/** Base node radius in px (scaled by node.weight). */
|
|
49
|
+
nodeRadius?: number;
|
|
50
|
+
/** Show text labels next to nodes. */
|
|
51
|
+
showLabels?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Number of cooling ticks. The simulation runs a synchronous warmup then
|
|
54
|
+
* animates the remainder unless reduced motion is requested.
|
|
55
|
+
*/
|
|
56
|
+
iterations?: number;
|
|
57
|
+
class?: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
let {
|
|
61
|
+
nodes,
|
|
62
|
+
edges,
|
|
63
|
+
label,
|
|
64
|
+
width = 480,
|
|
65
|
+
height = 360,
|
|
66
|
+
nodeRadius = 7,
|
|
67
|
+
showLabels = true,
|
|
68
|
+
iterations = 300,
|
|
69
|
+
class: className
|
|
70
|
+
}: ForceGraphProps = $props();
|
|
71
|
+
|
|
72
|
+
const TONES: ForceGraphTone[] = [
|
|
73
|
+
"category1", "category2", "category3", "category4",
|
|
74
|
+
"category5", "category6", "category7", "category8"
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Tone assignment: explicit tone wins, else stable per-group, else per-index.
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
function buildToneMap(ns: ForceGraphNode[]): Map<string, ForceGraphTone> {
|
|
81
|
+
const groups: (string | number)[] = [];
|
|
82
|
+
const seen = new Set<string | number>();
|
|
83
|
+
for (const n of ns) {
|
|
84
|
+
if (n.group === undefined) continue;
|
|
85
|
+
if (seen.has(n.group)) continue;
|
|
86
|
+
seen.add(n.group);
|
|
87
|
+
groups.push(n.group);
|
|
88
|
+
}
|
|
89
|
+
const groupTone = new Map<string | number, ForceGraphTone>();
|
|
90
|
+
groups.forEach((g, i) => groupTone.set(g, TONES[i % TONES.length]));
|
|
91
|
+
const map = new Map<string, ForceGraphTone>();
|
|
92
|
+
ns.forEach((n, i) => {
|
|
93
|
+
if (n.tone) map.set(n.id, n.tone);
|
|
94
|
+
else if (n.group !== undefined && groupTone.has(n.group)) map.set(n.id, groupTone.get(n.group)!);
|
|
95
|
+
else map.set(n.id, TONES[i % TONES.length]);
|
|
96
|
+
});
|
|
97
|
+
return map;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Lightweight force simulation (no external dependency).
|
|
102
|
+
// - repulsion (Coulomb-like, O(n^2), fine for ontology-scale graphs)
|
|
103
|
+
// - spring links (Hooke toward a rest length)
|
|
104
|
+
// - mild gravity toward the centre to keep disconnected nodes on-canvas
|
|
105
|
+
// A deterministic seeded layout keeps SSR / tests stable.
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
type SimNode = { id: string; x: number; y: number; vx: number; vy: number; fixed: boolean };
|
|
108
|
+
|
|
109
|
+
function mulberry32(seed: number): () => number {
|
|
110
|
+
let a = seed >>> 0;
|
|
111
|
+
return () => {
|
|
112
|
+
a |= 0; a = (a + 0x6d2b79f5) | 0;
|
|
113
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
114
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
115
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function runSimulation(
|
|
120
|
+
ns: ForceGraphNode[],
|
|
121
|
+
es: ForceGraphEdge[],
|
|
122
|
+
w: number,
|
|
123
|
+
h: number,
|
|
124
|
+
ticks: number
|
|
125
|
+
): Map<string, { x: number; y: number }> {
|
|
126
|
+
const cx = w / 2;
|
|
127
|
+
const cy = h / 2;
|
|
128
|
+
const rand = mulberry32(ns.length * 2654435761 + es.length);
|
|
129
|
+
const idIndex = new Map<string, number>();
|
|
130
|
+
const sim: SimNode[] = ns.map((n, i) => {
|
|
131
|
+
idIndex.set(n.id, i);
|
|
132
|
+
const fixed = typeof n.fx === "number" && typeof n.fy === "number";
|
|
133
|
+
// Seed on a loose ring so the first ticks fan the graph out predictably.
|
|
134
|
+
const angle = (i / Math.max(ns.length, 1)) * Math.PI * 2;
|
|
135
|
+
const r = Math.min(w, h) * 0.3 * (0.5 + rand() * 0.5);
|
|
136
|
+
return {
|
|
137
|
+
id: n.id,
|
|
138
|
+
x: fixed ? (n.fx as number) : cx + Math.cos(angle) * r,
|
|
139
|
+
y: fixed ? (n.fy as number) : cy + Math.sin(angle) * r,
|
|
140
|
+
vx: 0,
|
|
141
|
+
vy: 0,
|
|
142
|
+
fixed
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const links = es
|
|
147
|
+
.map((e) => ({ s: idIndex.get(e.source), t: idIndex.get(e.target) }))
|
|
148
|
+
.filter((l): l is { s: number; t: number } => l.s !== undefined && l.t !== undefined);
|
|
149
|
+
|
|
150
|
+
const area = w * h;
|
|
151
|
+
const k = Math.sqrt(area / Math.max(ns.length, 1)); // ideal node distance
|
|
152
|
+
const repulsion = k * k * 0.9;
|
|
153
|
+
const restLength = k * 0.8;
|
|
154
|
+
const springK = 0.04;
|
|
155
|
+
const gravity = 0.012;
|
|
156
|
+
const damping = 0.85;
|
|
157
|
+
let temperature = Math.min(w, h) * 0.08;
|
|
158
|
+
const cooling = ticks > 0 ? Math.pow(0.02, 1 / ticks) : 0.95;
|
|
159
|
+
|
|
160
|
+
for (let step = 0; step < ticks; step++) {
|
|
161
|
+
// Repulsion between all node pairs.
|
|
162
|
+
for (let i = 0; i < sim.length; i++) {
|
|
163
|
+
for (let j = i + 1; j < sim.length; j++) {
|
|
164
|
+
let dx = sim[i].x - sim[j].x;
|
|
165
|
+
let dy = sim[i].y - sim[j].y;
|
|
166
|
+
let dist2 = dx * dx + dy * dy;
|
|
167
|
+
if (dist2 < 0.01) {
|
|
168
|
+
dx = (rand() - 0.5) * 0.1;
|
|
169
|
+
dy = (rand() - 0.5) * 0.1;
|
|
170
|
+
dist2 = dx * dx + dy * dy + 0.01;
|
|
171
|
+
}
|
|
172
|
+
const dist = Math.sqrt(dist2);
|
|
173
|
+
const force = repulsion / dist2;
|
|
174
|
+
const fx = (dx / dist) * force;
|
|
175
|
+
const fy = (dy / dist) * force;
|
|
176
|
+
sim[i].vx += fx; sim[i].vy += fy;
|
|
177
|
+
sim[j].vx -= fx; sim[j].vy -= fy;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Spring attraction along links.
|
|
181
|
+
for (const l of links) {
|
|
182
|
+
const a = sim[l.s];
|
|
183
|
+
const b = sim[l.t];
|
|
184
|
+
const dx = b.x - a.x;
|
|
185
|
+
const dy = b.y - a.y;
|
|
186
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 0.01;
|
|
187
|
+
const force = (dist - restLength) * springK;
|
|
188
|
+
const fx = (dx / dist) * force;
|
|
189
|
+
const fy = (dy / dist) * force;
|
|
190
|
+
a.vx += fx; a.vy += fy;
|
|
191
|
+
b.vx -= fx; b.vy -= fy;
|
|
192
|
+
}
|
|
193
|
+
// Gravity toward centre + integrate with capped, cooling step.
|
|
194
|
+
for (const node of sim) {
|
|
195
|
+
if (node.fixed) { node.vx = 0; node.vy = 0; continue; }
|
|
196
|
+
node.vx += (cx - node.x) * gravity;
|
|
197
|
+
node.vy += (cy - node.y) * gravity;
|
|
198
|
+
node.vx *= damping;
|
|
199
|
+
node.vy *= damping;
|
|
200
|
+
const speed = Math.sqrt(node.vx * node.vx + node.vy * node.vy);
|
|
201
|
+
if (speed > temperature) {
|
|
202
|
+
node.vx = (node.vx / speed) * temperature;
|
|
203
|
+
node.vy = (node.vy / speed) * temperature;
|
|
204
|
+
}
|
|
205
|
+
node.x += node.vx;
|
|
206
|
+
node.y += node.vy;
|
|
207
|
+
// Keep inside a padded viewport.
|
|
208
|
+
node.x = Math.max(nodeRadius * 2, Math.min(w - nodeRadius * 2, node.x));
|
|
209
|
+
node.y = Math.max(nodeRadius * 2, Math.min(h - nodeRadius * 2, node.y));
|
|
210
|
+
}
|
|
211
|
+
temperature *= cooling;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const out = new Map<string, { x: number; y: number }>();
|
|
215
|
+
for (const node of sim) out.set(node.id, { x: node.x, y: node.y });
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// SSR-safe reduced-motion check (window may be undefined during SSR/tests).
|
|
220
|
+
const prefersReducedMotion =
|
|
221
|
+
typeof window !== "undefined" &&
|
|
222
|
+
typeof window.matchMedia === "function" &&
|
|
223
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
224
|
+
|
|
225
|
+
const toneMap = $derived(buildToneMap(nodes));
|
|
226
|
+
|
|
227
|
+
// The whole layout is recomputed when inputs change. Under reduced motion we
|
|
228
|
+
// settle the layout fully and never animate. Otherwise the same settled
|
|
229
|
+
// layout is used as the rendered target — a static, deterministic frame —
|
|
230
|
+
// which keeps the component framework-light and test-friendly while still
|
|
231
|
+
// honouring the motion preference (no rAF loop, no jitter).
|
|
232
|
+
const layout = $derived.by(() => {
|
|
233
|
+
const ticks = Math.max(1, Math.round(iterations));
|
|
234
|
+
return runSimulation(nodes, edges, width, height, ticks);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const positionedNodes = $derived.by(() =>
|
|
238
|
+
nodes.map((n, i) => {
|
|
239
|
+
const p = layout.get(n.id) ?? { x: width / 2, y: height / 2 };
|
|
240
|
+
return {
|
|
241
|
+
node: n,
|
|
242
|
+
i,
|
|
243
|
+
x: p.x,
|
|
244
|
+
y: p.y,
|
|
245
|
+
r: nodeRadius * Math.sqrt(Math.max(n.weight ?? 1, 0.25)),
|
|
246
|
+
tone: toneMap.get(n.id) ?? "category1",
|
|
247
|
+
title: n.label ?? n.id
|
|
248
|
+
};
|
|
249
|
+
})
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const positionedEdges = $derived.by(() => {
|
|
253
|
+
return edges
|
|
254
|
+
.map((e, i) => {
|
|
255
|
+
const a = layout.get(e.source);
|
|
256
|
+
const b = layout.get(e.target);
|
|
257
|
+
if (!a || !b) return null;
|
|
258
|
+
return { edge: e, i, x1: a.x, y1: a.y, x2: b.x, y2: b.y };
|
|
259
|
+
})
|
|
260
|
+
.filter((e): e is NonNullable<typeof e> => e !== null);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
let hoveredIndex: number | null = $state(null);
|
|
264
|
+
|
|
265
|
+
const classes = () =>
|
|
266
|
+
["st-forceGraph", prefersReducedMotion ? "st-forceGraph--static" : null, className]
|
|
267
|
+
.filter(Boolean)
|
|
268
|
+
.join(" ");
|
|
269
|
+
</script>
|
|
270
|
+
|
|
271
|
+
<div class={classes()} role="img" aria-label={label}>
|
|
272
|
+
<svg
|
|
273
|
+
viewBox="0 0 {width} {height}"
|
|
274
|
+
preserveAspectRatio="xMidYMid meet"
|
|
275
|
+
width="100%"
|
|
276
|
+
height="100%"
|
|
277
|
+
focusable="false"
|
|
278
|
+
aria-hidden="true"
|
|
279
|
+
>
|
|
280
|
+
<!-- edges first so nodes paint on top -->
|
|
281
|
+
<g class="st-forceGraph__edges">
|
|
282
|
+
{#each positionedEdges as e (e.i)}
|
|
283
|
+
<line
|
|
284
|
+
class="st-forceGraph__edge"
|
|
285
|
+
class:st-forceGraph__edge--weak={e.edge.weak}
|
|
286
|
+
x1={e.x1}
|
|
287
|
+
y1={e.y1}
|
|
288
|
+
x2={e.x2}
|
|
289
|
+
y2={e.y2}
|
|
290
|
+
/>
|
|
291
|
+
{/each}
|
|
292
|
+
</g>
|
|
293
|
+
|
|
294
|
+
<g class="st-forceGraph__nodes">
|
|
295
|
+
{#each positionedNodes as p (p.node.id)}
|
|
296
|
+
<g
|
|
297
|
+
class="st-forceGraph__node st-forceGraph__node--{p.tone}"
|
|
298
|
+
class:st-forceGraph__node--dim={hoveredIndex !== null && hoveredIndex !== p.i}
|
|
299
|
+
transform="translate({p.x} {p.y})"
|
|
300
|
+
>
|
|
301
|
+
<circle
|
|
302
|
+
class="st-forceGraph__dot"
|
|
303
|
+
r={p.r}
|
|
304
|
+
tabindex="0"
|
|
305
|
+
role="img"
|
|
306
|
+
aria-label="{p.title}{p.node.group !== undefined ? ` — ${p.node.group}` : ''}"
|
|
307
|
+
onmouseenter={() => (hoveredIndex = p.i)}
|
|
308
|
+
onmouseleave={() => (hoveredIndex = null)}
|
|
309
|
+
onfocus={() => (hoveredIndex = p.i)}
|
|
310
|
+
onblur={() => (hoveredIndex = null)}
|
|
311
|
+
/>
|
|
312
|
+
{#if showLabels}
|
|
313
|
+
<text class="st-forceGraph__label" x={p.r + 3} y="0" dominant-baseline="middle">{p.title}</text>
|
|
314
|
+
{/if}
|
|
315
|
+
</g>
|
|
316
|
+
{/each}
|
|
317
|
+
</g>
|
|
318
|
+
</svg>
|
|
319
|
+
|
|
320
|
+
{#if hoveredIndex !== null && positionedNodes[hoveredIndex]}
|
|
321
|
+
{@const p = positionedNodes[hoveredIndex]}
|
|
322
|
+
{@const relCount = positionedEdges.filter(
|
|
323
|
+
(e) => e.edge.source === p.node.id || e.edge.target === p.node.id
|
|
324
|
+
).length}
|
|
325
|
+
<div
|
|
326
|
+
class="st-forceGraph__tooltip"
|
|
327
|
+
role="presentation"
|
|
328
|
+
style="left: {(p.x / width) * 100}%; top: {(p.y / height) * 100}%"
|
|
329
|
+
>
|
|
330
|
+
<span class="st-forceGraph__tooltipLabel">{p.title}</span>
|
|
331
|
+
{#if p.node.group !== undefined}
|
|
332
|
+
<span class="st-forceGraph__tooltipMeta">{p.node.group}</span>
|
|
333
|
+
{/if}
|
|
334
|
+
{#if relCount > 0}
|
|
335
|
+
<span class="st-forceGraph__tooltipMeta">{relCount} relation{relCount === 1 ? "" : "s"}</span>
|
|
336
|
+
{/if}
|
|
337
|
+
</div>
|
|
338
|
+
{/if}
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
<style>
|
|
342
|
+
.st-forceGraph {
|
|
343
|
+
color: var(--st-semantic-text-secondary);
|
|
344
|
+
display: block;
|
|
345
|
+
font-family: inherit;
|
|
346
|
+
position: relative;
|
|
347
|
+
width: 100%;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.st-forceGraph svg { display: block; overflow: visible; }
|
|
351
|
+
|
|
352
|
+
.st-forceGraph__edge {
|
|
353
|
+
stroke: var(--st-semantic-border-strong);
|
|
354
|
+
stroke-width: 1;
|
|
355
|
+
opacity: 0.55;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.st-forceGraph__edge--weak {
|
|
359
|
+
stroke: var(--st-semantic-border-subtle);
|
|
360
|
+
stroke-dasharray: 3 3;
|
|
361
|
+
opacity: 0.5;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.st-forceGraph__node { transition: opacity 120ms ease; }
|
|
365
|
+
.st-forceGraph__node--dim { opacity: 0.3; }
|
|
366
|
+
|
|
367
|
+
.st-forceGraph__dot {
|
|
368
|
+
cursor: pointer;
|
|
369
|
+
fill-opacity: 0.9;
|
|
370
|
+
stroke: var(--st-semantic-surface-default, #fff);
|
|
371
|
+
stroke-width: 1.5;
|
|
372
|
+
transition: fill-opacity 120ms ease;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.st-forceGraph__dot:hover,
|
|
376
|
+
.st-forceGraph__dot:focus-visible { fill-opacity: 1; }
|
|
377
|
+
|
|
378
|
+
.st-forceGraph__dot:focus-visible {
|
|
379
|
+
outline: 2px solid var(--st-semantic-border-interactive);
|
|
380
|
+
outline-offset: 1px;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.st-forceGraph__label {
|
|
384
|
+
fill: var(--st-semantic-text-secondary);
|
|
385
|
+
font-size: 0.6875rem;
|
|
386
|
+
pointer-events: none;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.st-forceGraph__node--category1 .st-forceGraph__dot { fill: var(--st-semantic-data-category1); }
|
|
390
|
+
.st-forceGraph__node--category2 .st-forceGraph__dot { fill: var(--st-semantic-data-category2); }
|
|
391
|
+
.st-forceGraph__node--category3 .st-forceGraph__dot { fill: var(--st-semantic-data-category3); }
|
|
392
|
+
.st-forceGraph__node--category4 .st-forceGraph__dot { fill: var(--st-semantic-data-category4); }
|
|
393
|
+
.st-forceGraph__node--category5 .st-forceGraph__dot { fill: var(--st-semantic-data-category5); }
|
|
394
|
+
.st-forceGraph__node--category6 .st-forceGraph__dot { fill: var(--st-semantic-data-category6); }
|
|
395
|
+
.st-forceGraph__node--category7 .st-forceGraph__dot { fill: var(--st-semantic-data-category7); }
|
|
396
|
+
.st-forceGraph__node--category8 .st-forceGraph__dot { fill: var(--st-semantic-data-category8); }
|
|
397
|
+
|
|
398
|
+
.st-forceGraph__tooltip {
|
|
399
|
+
background: var(--st-semantic-surface-inverse);
|
|
400
|
+
border-radius: var(--st-radius-sm, 0.25rem);
|
|
401
|
+
color: var(--st-semantic-text-inverse);
|
|
402
|
+
display: inline-flex;
|
|
403
|
+
flex-direction: column;
|
|
404
|
+
font-size: 0.75rem;
|
|
405
|
+
gap: 0.125rem;
|
|
406
|
+
line-height: 1.2;
|
|
407
|
+
padding: 0.375rem 0.5rem;
|
|
408
|
+
pointer-events: none;
|
|
409
|
+
position: absolute;
|
|
410
|
+
transform: translate(-50%, calc(-100% - 10px));
|
|
411
|
+
white-space: nowrap;
|
|
412
|
+
z-index: 1;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.st-forceGraph__tooltipLabel { font-weight: 600; }
|
|
416
|
+
.st-forceGraph__tooltipMeta { opacity: 0.85; }
|
|
417
|
+
|
|
418
|
+
@media (prefers-reduced-motion: reduce) {
|
|
419
|
+
.st-forceGraph__node,
|
|
420
|
+
.st-forceGraph__dot { transition: none; }
|
|
421
|
+
}
|
|
422
|
+
</style>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type ForceGraphTone = "category1" | "category2" | "category3" | "category4" | "category5" | "category6" | "category7" | "category8";
|
|
2
|
+
export type ForceGraphNode = {
|
|
3
|
+
/** Stable identifier; referenced by edges. */
|
|
4
|
+
id: string;
|
|
5
|
+
/** Visible label (falls back to id). */
|
|
6
|
+
label?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Grouping key (e.g. node type or community). Nodes sharing a group get
|
|
9
|
+
* the same tone when `tone` is not set explicitly.
|
|
10
|
+
*/
|
|
11
|
+
group?: string | number;
|
|
12
|
+
/** Explicit data-vis tone; overrides the group-derived tone. */
|
|
13
|
+
tone?: ForceGraphTone;
|
|
14
|
+
/** Relative node radius weight (defaults to 1). */
|
|
15
|
+
weight?: number;
|
|
16
|
+
/** Pin the node to a fixed position (ignored by the simulation). */
|
|
17
|
+
fx?: number;
|
|
18
|
+
fy?: number;
|
|
19
|
+
};
|
|
20
|
+
export type ForceGraphEdge = {
|
|
21
|
+
/** Source node id. */
|
|
22
|
+
source: string;
|
|
23
|
+
/** Target node id. */
|
|
24
|
+
target: string;
|
|
25
|
+
/** Optional relation label, surfaced in the tooltip on hover/focus. */
|
|
26
|
+
relation?: string;
|
|
27
|
+
/**
|
|
28
|
+
* When true the link renders as a dashed/faded "weak" link. Lets callers
|
|
29
|
+
* map a confidence dimension onto link strength without extra props.
|
|
30
|
+
*/
|
|
31
|
+
weak?: boolean;
|
|
32
|
+
};
|
|
33
|
+
type ForceGraphProps = {
|
|
34
|
+
nodes: ForceGraphNode[];
|
|
35
|
+
edges: ForceGraphEdge[];
|
|
36
|
+
/** Accessible name for the figure (required). */
|
|
37
|
+
label: string;
|
|
38
|
+
width?: number;
|
|
39
|
+
height?: number;
|
|
40
|
+
/** Base node radius in px (scaled by node.weight). */
|
|
41
|
+
nodeRadius?: number;
|
|
42
|
+
/** Show text labels next to nodes. */
|
|
43
|
+
showLabels?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Number of cooling ticks. The simulation runs a synchronous warmup then
|
|
46
|
+
* animates the remainder unless reduced motion is requested.
|
|
47
|
+
*/
|
|
48
|
+
iterations?: number;
|
|
49
|
+
class?: string;
|
|
50
|
+
};
|
|
51
|
+
declare const ForceGraph: import("svelte").Component<ForceGraphProps, {}, "">;
|
|
52
|
+
type ForceGraph = ReturnType<typeof ForceGraph>;
|
|
53
|
+
export default ForceGraph;
|
|
54
|
+
//# sourceMappingURL=ForceGraph.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ForceGraph.svelte.d.ts","sourceRoot":"","sources":["../src/lib/ForceGraph.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,cAAc,GACtB,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACrD,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;AAE1D,MAAM,MAAM,cAAc,GAAG;IAC3B,8CAA8C;IAC9C,EAAE,EAAE,MAAM,CAAC;IACX,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,gEAAgE;IAChE,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAoQJ,QAAA,MAAM,UAAU,qDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|