@reddb-io/ui 0.1.1 → 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/build/_app/immutable/assets/0.DQDLkSdR.css +1 -0
- package/build/_app/immutable/assets/Workspace.CvP5De7i.css +1 -0
- package/build/_app/immutable/chunks/65XjEwaF.js +367 -0
- package/build/_app/immutable/chunks/{Ov8N-vxt.js → BHBXmd1Y.js} +1 -1
- package/build/_app/immutable/chunks/BMc_qoms.js +973 -0
- package/build/_app/immutable/chunks/{ALqaDrNQ.js → C278awGv.js} +1 -1
- package/build/_app/immutable/chunks/CDKIdLnm.js +187 -0
- package/build/_app/immutable/chunks/{DXY_Rxa7.js → CXNPJgv1.js} +7 -7
- package/build/_app/immutable/chunks/{BMHKbvs8.js → CwxFwvow.js} +1 -1
- package/build/_app/immutable/chunks/{Bgso8cDH.js → DqvWKbSc.js} +1 -1
- package/build/_app/immutable/entry/app.CGfmRH-N.js +2 -0
- package/build/_app/immutable/entry/start._LhFJimm.js +1 -0
- package/build/_app/immutable/nodes/{0.CHPiFxDo.js → 0.BYhY6O5-.js} +1 -1
- package/build/_app/immutable/nodes/{1.BC_4YZ0h.js → 1.4XYGRMul.js} +1 -1
- package/build/_app/immutable/nodes/11.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/{10.B7DMKSNc.js → 12.DXZfNPNK.js} +5 -5
- package/build/_app/immutable/nodes/7.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/{7.BZX9Iird.js → 8.C7OOsMOJ.js} +1 -1
- package/build/_app/version.json +1 -1
- package/build/index.html +12 -12
- package/dist/embed/{RedUiEmbedRoot-BGoLxJf1.js → RedUiEmbedRoot-Dia9lxfb.js} +12622 -10614
- package/dist/embed/RedUiEmbedRoot-Dia9lxfb.js.map +1 -0
- package/dist/embed/index-Bq0TT7Ao.js +577 -0
- package/dist/embed/{index-B6ZQhCsu.js.map → index-Bq0TT7Ao.js.map} +1 -1
- package/dist/embed/{index-fad77a13.esm-BqPj0saH.js → index-fad77a13.esm-BMULHdcE.js} +22 -20
- package/dist/embed/{index-fad77a13.esm-BqPj0saH.js.map → index-fad77a13.esm-BMULHdcE.js.map} +1 -1
- package/dist/embed/index.js +1 -1
- package/dist/embed/sigma-edge-curve.esm-CAT5Xrom.js +536 -0
- package/dist/embed/sigma-edge-curve.esm-CAT5Xrom.js.map +1 -0
- package/dist/embed/{sigma-node-border.esm-CGtlgqmo.js → sigma-node-border.esm-Bnk4BRbw.js} +32 -32
- package/dist/embed/{sigma-node-border.esm-CGtlgqmo.js.map → sigma-node-border.esm-Bnk4BRbw.js.map} +1 -1
- package/dist/embed/{sigma.esm-y0qgY-OC.js → sigma.esm-C2pqg5JH.js} +3 -3
- package/dist/embed/{sigma.esm-y0qgY-OC.js.map → sigma.esm-C2pqg5JH.js.map} +1 -1
- package/package.json +5 -3
- package/src/lib/Appearance.svelte +90 -0
- package/src/lib/CommandPalette.svelte +2 -0
- package/src/lib/SettingsView.svelte +322 -0
- package/src/lib/Topbar.svelte +15 -1
- package/src/lib/Workspace.svelte +14 -1
- package/src/lib/index.ts +2 -0
- package/src/lib/renderers/GraphRenderer.svelte +448 -44
- package/src/lib/renderers/graph-bundle.ts +401 -0
- package/src/lib/renderers/graph-communities.ts +193 -0
- package/src/lib/renderers/graph-render.ts +48 -5
- package/src/lib/renderers/graph-sigma.ts +274 -8
- package/src/lib/router.svelte.ts +84 -57
- package/src/lib/settings-sections.ts +249 -0
- package/src/lib/skins/skins.svelte.ts +64 -0
- package/src/lib/skins/skins.ts +178 -0
- package/build/_app/immutable/assets/0.C63jY5jX.css +0 -1
- package/build/_app/immutable/assets/Workspace.BCzG1DsH.css +0 -1
- package/build/_app/immutable/chunks/BQHbwf54.js +0 -916
- package/build/_app/immutable/chunks/Bmgx1mfv.js +0 -310
- package/build/_app/immutable/entry/app.D_IVtpdv.js +0 -2
- package/build/_app/immutable/entry/start.3oC0UQ_Q.js +0 -1
- package/dist/embed/RedUiEmbedRoot-BGoLxJf1.js.map +0 -1
- package/dist/embed/index-B6ZQhCsu.js +0 -577
- /package/build/_app/immutable/assets/{10.Dblyk0Kr.css → 12.Dblyk0Kr.css} +0 -0
- /package/build/_app/immutable/nodes/{8.CAG5_cGP.js → 10.CAG5_cGP.js} +0 -0
|
@@ -54,6 +54,24 @@ export function sigmaNodeSize(type: string, incomingScale: number): number {
|
|
|
54
54
|
return nodeVisualRadius(type, incomingScale) * SIGMA_SIZE_FACTOR;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// ─── community supernode sizing (collapsed-cluster rendering) ───────────────
|
|
58
|
+
|
|
59
|
+
/** Light stroke that marks a supernode as an expandable cluster. */
|
|
60
|
+
export const SUPERNODE_STROKE_COLOR = "#e2e8f0";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Supernode radius in the SVG fallback's [0, 100] space — grows with the
|
|
64
|
+
* member count (sub-linear so a 500-node cluster doesn't swallow the canvas).
|
|
65
|
+
*/
|
|
66
|
+
export function supernodeSvgRadius(memberCount: number): number {
|
|
67
|
+
return Math.min(7, 2.2 + Math.sqrt(Math.max(1, memberCount)) * 0.7);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Same radius mapped onto sigma's pixel sizing. */
|
|
71
|
+
export function supernodeSigmaSize(memberCount: number): number {
|
|
72
|
+
return supernodeSvgRadius(memberCount) * SIGMA_SIZE_FACTOR;
|
|
73
|
+
}
|
|
74
|
+
|
|
57
75
|
// ─── colour utilities (sigma's WebGL colour parser handles hex/rgb(a) but not
|
|
58
76
|
// hsl(), so community tints are converted up front) ─────────────────────
|
|
59
77
|
|
|
@@ -159,6 +177,10 @@ export interface SigmaNodeAttributes {
|
|
|
159
177
|
nodeType: string;
|
|
160
178
|
community: number;
|
|
161
179
|
orphan: boolean;
|
|
180
|
+
/** True for a collapsed-community supernode; always labels, never dims away. */
|
|
181
|
+
isSupernode: boolean;
|
|
182
|
+
/** Member count behind a supernode (0 for ordinary nodes). */
|
|
183
|
+
memberCount: number;
|
|
162
184
|
/** Drives `defaultNodeType` → NodeBorderProgram. */
|
|
163
185
|
type: "border";
|
|
164
186
|
}
|
|
@@ -169,7 +191,19 @@ export interface SigmaEdgeAttributes {
|
|
|
169
191
|
/** Original edge label, kept so the reducer can surface it on focus only. */
|
|
170
192
|
edgeLabel: string;
|
|
171
193
|
label: string;
|
|
172
|
-
|
|
194
|
+
/**
|
|
195
|
+
* True when the edge's endpoints sit in different communities — the
|
|
196
|
+
* group-in-a-box overlay routes these at the box borders, so the native
|
|
197
|
+
* straight edge is hidden while that mode is on.
|
|
198
|
+
*/
|
|
199
|
+
crossCommunity: boolean;
|
|
200
|
+
/**
|
|
201
|
+
* Bundling curvature consumed by @sigma/edge-curve when bundling is on. 0 for
|
|
202
|
+
* a straight chord; the reducer flips the edge to the "curved" program only
|
|
203
|
+
* when bundling is active.
|
|
204
|
+
*/
|
|
205
|
+
curvature: number;
|
|
206
|
+
type: "line" | "curved";
|
|
173
207
|
}
|
|
174
208
|
|
|
175
209
|
export interface BuildSigmaGraphParams {
|
|
@@ -180,6 +214,12 @@ export interface BuildSigmaGraphParams {
|
|
|
180
214
|
orphanIds: Set<string>;
|
|
181
215
|
dark?: boolean;
|
|
182
216
|
edgeColor?: string;
|
|
217
|
+
/**
|
|
218
|
+
* Per-edge bundling curvature (sigma scene space), keyed by edge id. When
|
|
219
|
+
* present and non-zero, the edge carries the value so the curve program can
|
|
220
|
+
* bow it onto its bundle. Absent / 0 → straight edge.
|
|
221
|
+
*/
|
|
222
|
+
bundledCurvatures?: Map<string, number>;
|
|
183
223
|
}
|
|
184
224
|
|
|
185
225
|
/**
|
|
@@ -199,6 +239,7 @@ export function buildSigmaGraph(
|
|
|
199
239
|
orphanIds,
|
|
200
240
|
dark = true,
|
|
201
241
|
edgeColor = "#3a424d",
|
|
242
|
+
bundledCurvatures,
|
|
202
243
|
} = params;
|
|
203
244
|
|
|
204
245
|
const graph = new Graph<SigmaNodeAttributes, SigmaEdgeAttributes>({
|
|
@@ -212,7 +253,12 @@ export function buildSigmaGraph(
|
|
|
212
253
|
const type = String(node.data.node_type ?? "node");
|
|
213
254
|
const incomingScale = sizeScales.get(node.id) ?? 1;
|
|
214
255
|
const pos = layout.get(node.id);
|
|
215
|
-
const
|
|
256
|
+
const isSupernode = node.data.__supernode === true;
|
|
257
|
+
const memberCount =
|
|
258
|
+
typeof node.data.__memberCount === "number" ? node.data.__memberCount : 0;
|
|
259
|
+
const size = isSupernode
|
|
260
|
+
? supernodeSigmaSize(memberCount)
|
|
261
|
+
: sigmaNodeSize(type, incomingScale);
|
|
216
262
|
const fill = pos
|
|
217
263
|
? sigmaColorForCommunity(pos.community, dark)
|
|
218
264
|
: colorForType(type);
|
|
@@ -224,11 +270,13 @@ export function buildSigmaGraph(
|
|
|
224
270
|
baseSize: size,
|
|
225
271
|
color: fill,
|
|
226
272
|
mutedColor: withAlpha(fill, 0.14),
|
|
227
|
-
borderColor: colorForType(type),
|
|
273
|
+
borderColor: isSupernode ? SUPERNODE_STROKE_COLOR : colorForType(type),
|
|
228
274
|
label: node.label,
|
|
229
275
|
nodeType: type,
|
|
230
276
|
community: pos?.community ?? -1,
|
|
231
277
|
orphan: orphanIds.has(node.id),
|
|
278
|
+
isSupernode,
|
|
279
|
+
memberCount,
|
|
232
280
|
type: "border",
|
|
233
281
|
});
|
|
234
282
|
}
|
|
@@ -236,11 +284,18 @@ export function buildSigmaGraph(
|
|
|
236
284
|
for (const edge of edges) {
|
|
237
285
|
if (!graph.hasNode(edge.source) || !graph.hasNode(edge.target)) continue;
|
|
238
286
|
if (graph.hasEdge(edge.id)) continue;
|
|
287
|
+
const sc = layout.get(edge.source)?.community ?? -1;
|
|
288
|
+
const tc = layout.get(edge.target)?.community ?? -1;
|
|
289
|
+
const aggregated = edge.data?.__aggregated === true;
|
|
290
|
+
const weight = typeof edge.data?.weight === "number" ? edge.data.weight : 1;
|
|
239
291
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, {
|
|
240
|
-
|
|
292
|
+
// Aggregated cross-cluster edges thicken with the number they fold in.
|
|
293
|
+
size: aggregated ? Math.min(6, 1 + Math.log2(weight + 1)) : 1,
|
|
241
294
|
color: withAlpha(edgeColor, 0.34),
|
|
242
295
|
edgeLabel: edge.label ?? "",
|
|
243
296
|
label: "",
|
|
297
|
+
crossCommunity: sc !== tc,
|
|
298
|
+
curvature: bundledCurvatures?.get(edge.id) ?? 0,
|
|
244
299
|
type: "line",
|
|
245
300
|
});
|
|
246
301
|
}
|
|
@@ -248,6 +303,175 @@ export function buildSigmaGraph(
|
|
|
248
303
|
return graph;
|
|
249
304
|
}
|
|
250
305
|
|
|
306
|
+
// ─── group-in-a-box geometry (pure; consumed by the SVG fallback directly and
|
|
307
|
+
// by the canvas overlay after a graph→viewport transform) ────────────────
|
|
308
|
+
|
|
309
|
+
export interface CommunityBox {
|
|
310
|
+
community: number;
|
|
311
|
+
/** Member count — drives nothing visual, surfaced for labels/tooltips. */
|
|
312
|
+
count: number;
|
|
313
|
+
/** Tint hue for this community (hsl string). */
|
|
314
|
+
tint: string;
|
|
315
|
+
/** Faint translucent fill so clusters read without drowning the nodes. */
|
|
316
|
+
fill: string;
|
|
317
|
+
/** Stronger translucent stroke for the box border. */
|
|
318
|
+
stroke: string;
|
|
319
|
+
minX: number;
|
|
320
|
+
minY: number;
|
|
321
|
+
maxX: number;
|
|
322
|
+
maxY: number;
|
|
323
|
+
/** Box centre, handy for community labels. */
|
|
324
|
+
cx: number;
|
|
325
|
+
cy: number;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export interface CommunityBoxOptions {
|
|
329
|
+
/** Padding (in layout units) added around the member bounding box. */
|
|
330
|
+
padding?: number;
|
|
331
|
+
dark?: boolean;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* One bounding box per community, sized to its members with a small padding.
|
|
336
|
+
* Positions are in the same [0, 100] layout space the SVG fallback draws in;
|
|
337
|
+
* the canvas overlay maps them through sigma's graph→viewport transform.
|
|
338
|
+
* Communities are returned in ascending id order so the output is stable.
|
|
339
|
+
*/
|
|
340
|
+
export function computeCommunityBoxes(
|
|
341
|
+
nodes: GraphNode[],
|
|
342
|
+
layout: GraphLayout,
|
|
343
|
+
options: CommunityBoxOptions = {}
|
|
344
|
+
): CommunityBox[] {
|
|
345
|
+
const { padding = 2.5, dark = true } = options;
|
|
346
|
+
const bounds = new Map<
|
|
347
|
+
number,
|
|
348
|
+
{ minX: number; minY: number; maxX: number; maxY: number; count: number }
|
|
349
|
+
>();
|
|
350
|
+
|
|
351
|
+
for (const node of nodes) {
|
|
352
|
+
const pos = layout.get(node.id);
|
|
353
|
+
if (!pos) continue;
|
|
354
|
+
const b = bounds.get(pos.community);
|
|
355
|
+
if (!b) {
|
|
356
|
+
bounds.set(pos.community, {
|
|
357
|
+
minX: pos.x,
|
|
358
|
+
minY: pos.y,
|
|
359
|
+
maxX: pos.x,
|
|
360
|
+
maxY: pos.y,
|
|
361
|
+
count: 1,
|
|
362
|
+
});
|
|
363
|
+
} else {
|
|
364
|
+
b.minX = Math.min(b.minX, pos.x);
|
|
365
|
+
b.minY = Math.min(b.minY, pos.y);
|
|
366
|
+
b.maxX = Math.max(b.maxX, pos.x);
|
|
367
|
+
b.maxY = Math.max(b.maxY, pos.y);
|
|
368
|
+
b.count += 1;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return [...bounds.entries()]
|
|
373
|
+
.sort((a, b) => a[0] - b[0])
|
|
374
|
+
.map(([community, b]) => {
|
|
375
|
+
const tint = colorForCommunity(community, dark);
|
|
376
|
+
return {
|
|
377
|
+
community,
|
|
378
|
+
count: b.count,
|
|
379
|
+
tint,
|
|
380
|
+
fill: withAlpha(tint, 0.08),
|
|
381
|
+
stroke: withAlpha(tint, 0.5),
|
|
382
|
+
minX: b.minX - padding,
|
|
383
|
+
minY: b.minY - padding,
|
|
384
|
+
maxX: b.maxX + padding,
|
|
385
|
+
maxY: b.maxY + padding,
|
|
386
|
+
cx: (b.minX + b.maxX) / 2,
|
|
387
|
+
cy: (b.minY + b.maxY) / 2,
|
|
388
|
+
};
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export interface RoutedEdge {
|
|
393
|
+
id: string;
|
|
394
|
+
source: string;
|
|
395
|
+
target: string;
|
|
396
|
+
label?: string;
|
|
397
|
+
sourceCommunity: number;
|
|
398
|
+
targetCommunity: number;
|
|
399
|
+
x1: number;
|
|
400
|
+
y1: number;
|
|
401
|
+
x2: number;
|
|
402
|
+
y2: number;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
interface Box {
|
|
406
|
+
minX: number;
|
|
407
|
+
minY: number;
|
|
408
|
+
maxX: number;
|
|
409
|
+
maxY: number;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Walk the ray from `p` toward `q` and return the point where it leaves the
|
|
414
|
+
* box that contains `p`. Used to clip an inter-community edge to its source /
|
|
415
|
+
* target box borders so the segment connects the boxes rather than cutting
|
|
416
|
+
* through their interiors.
|
|
417
|
+
*/
|
|
418
|
+
function exitPoint(
|
|
419
|
+
px: number,
|
|
420
|
+
py: number,
|
|
421
|
+
qx: number,
|
|
422
|
+
qy: number,
|
|
423
|
+
box: Box
|
|
424
|
+
): { x: number; y: number } {
|
|
425
|
+
const dx = qx - px;
|
|
426
|
+
const dy = qy - py;
|
|
427
|
+
let t = 1;
|
|
428
|
+
if (dx > 0) t = Math.min(t, (box.maxX - px) / dx);
|
|
429
|
+
else if (dx < 0) t = Math.min(t, (box.minX - px) / dx);
|
|
430
|
+
if (dy > 0) t = Math.min(t, (box.maxY - py) / dy);
|
|
431
|
+
else if (dy < 0) t = Math.min(t, (box.minY - py) / dy);
|
|
432
|
+
t = Math.max(0, Math.min(1, t));
|
|
433
|
+
return { x: px + dx * t, y: py + dy * t };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Route every inter-community edge so it runs between the two community box
|
|
438
|
+
* borders instead of node-centre to node-centre — the group-in-a-box
|
|
439
|
+
* convention that keeps cross-cluster edges out of unrelated clusters.
|
|
440
|
+
* Intra-community edges are left to the native renderer and skipped here.
|
|
441
|
+
*/
|
|
442
|
+
export function routeInterCommunityEdges(
|
|
443
|
+
edges: GraphEdge[],
|
|
444
|
+
layout: GraphLayout,
|
|
445
|
+
boxes: CommunityBox[]
|
|
446
|
+
): RoutedEdge[] {
|
|
447
|
+
const boxByCommunity = new Map(boxes.map((b) => [b.community, b]));
|
|
448
|
+
const out: RoutedEdge[] = [];
|
|
449
|
+
for (const edge of edges) {
|
|
450
|
+
const s = layout.get(edge.source);
|
|
451
|
+
const t = layout.get(edge.target);
|
|
452
|
+
if (!s || !t) continue;
|
|
453
|
+
if (s.community === t.community) continue;
|
|
454
|
+
const sourceBox = boxByCommunity.get(s.community);
|
|
455
|
+
const targetBox = boxByCommunity.get(t.community);
|
|
456
|
+
if (!sourceBox || !targetBox) continue;
|
|
457
|
+
const start = exitPoint(s.x, s.y, t.x, t.y, sourceBox);
|
|
458
|
+
const end = exitPoint(t.x, t.y, s.x, s.y, targetBox);
|
|
459
|
+
out.push({
|
|
460
|
+
id: edge.id,
|
|
461
|
+
source: edge.source,
|
|
462
|
+
target: edge.target,
|
|
463
|
+
label: edge.label,
|
|
464
|
+
sourceCommunity: s.community,
|
|
465
|
+
targetCommunity: t.community,
|
|
466
|
+
x1: start.x,
|
|
467
|
+
y1: start.y,
|
|
468
|
+
x2: end.x,
|
|
469
|
+
y2: end.y,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
return out;
|
|
473
|
+
}
|
|
474
|
+
|
|
251
475
|
// ─── focus neighbourhood (parity with the canvas focus highlight) ───────────
|
|
252
476
|
|
|
253
477
|
export interface GraphFocus {
|
|
@@ -284,6 +508,13 @@ export interface SigmaReducerState {
|
|
|
284
508
|
/** Focus-pulse progress, 0 (rest) → 1 (peak). Held at 0 for reduced motion. */
|
|
285
509
|
pulse: number;
|
|
286
510
|
colors: SigmaThemeColors;
|
|
511
|
+
/**
|
|
512
|
+
* Group-in-a-box mode: cross-community edges are drawn by the overlay at the
|
|
513
|
+
* box borders, so the native straight edge is hidden to avoid double lines.
|
|
514
|
+
*/
|
|
515
|
+
groupInBox?: boolean;
|
|
516
|
+
/** Edge bundling on → edges render through the curve program. */
|
|
517
|
+
bundle: boolean;
|
|
287
518
|
}
|
|
288
519
|
|
|
289
520
|
export interface SigmaNodeDisplay {
|
|
@@ -303,6 +534,10 @@ export interface SigmaEdgeDisplay {
|
|
|
303
534
|
forceLabel: boolean;
|
|
304
535
|
zIndex: number;
|
|
305
536
|
hidden: boolean;
|
|
537
|
+
/** Which edge program draws this edge: straight "line" or bundled "curved". */
|
|
538
|
+
type: "line" | "curved";
|
|
539
|
+
/** Bow magnitude/sign for the curve program (ignored by the line program). */
|
|
540
|
+
curvature: number;
|
|
306
541
|
}
|
|
307
542
|
|
|
308
543
|
const NODE_LABEL_MAX = 36;
|
|
@@ -337,9 +572,12 @@ export function reduceSigmaNode(
|
|
|
337
572
|
? colors.selectedNodeStroke
|
|
338
573
|
: attrs.borderColor;
|
|
339
574
|
|
|
575
|
+
// Supernodes always carry their cluster/count label so the collapsed state
|
|
576
|
+
// is self-explanatory and obviously clickable.
|
|
340
577
|
const showLabel =
|
|
341
|
-
|
|
342
|
-
(
|
|
578
|
+
attrs.isSupernode ||
|
|
579
|
+
(hasFocus &&
|
|
580
|
+
(isPrimary || (focus.nodes.size <= NEIGHBOUR_LABEL_LIMIT && isNeighbor)));
|
|
343
581
|
|
|
344
582
|
return {
|
|
345
583
|
size: attrs.baseSize * scale,
|
|
@@ -347,7 +585,7 @@ export function reduceSigmaNode(
|
|
|
347
585
|
borderColor,
|
|
348
586
|
label: showLabel ? attrs.label.slice(0, NODE_LABEL_MAX) : "",
|
|
349
587
|
forceLabel: showLabel,
|
|
350
|
-
zIndex: isPrimary ? 3 : isNeighbor ? 2 : 1,
|
|
588
|
+
zIndex: isPrimary ? 3 : attrs.isSupernode ? 2 : isNeighbor ? 2 : 1,
|
|
351
589
|
hidden: false,
|
|
352
590
|
};
|
|
353
591
|
}
|
|
@@ -358,10 +596,32 @@ export function reduceSigmaEdge(
|
|
|
358
596
|
attrs: SigmaEdgeAttributes,
|
|
359
597
|
state: SigmaReducerState
|
|
360
598
|
): SigmaEdgeDisplay {
|
|
361
|
-
const { focus, focusId, colors } = state;
|
|
599
|
+
const { focus, focusId, colors, groupInBox, bundle } = state;
|
|
362
600
|
const hasFocus = focusId !== null;
|
|
363
601
|
const focused = focus.edges.has(id);
|
|
364
602
|
|
|
603
|
+
// The sigma edge reducer's return *replaces* the edge data, so type and
|
|
604
|
+
// curvature must be carried through on every branch or the curve program
|
|
605
|
+
// can't see them. Bundle off → always the straight line program.
|
|
606
|
+
const curvature = attrs.curvature;
|
|
607
|
+
const type: "line" | "curved" =
|
|
608
|
+
bundle && Math.abs(curvature) > 1e-4 ? "curved" : "line";
|
|
609
|
+
|
|
610
|
+
// In group-in-a-box mode the overlay owns cross-community edges; hide the
|
|
611
|
+
// native straight line so it doesn't cut through unrelated clusters.
|
|
612
|
+
if (groupInBox && attrs.crossCommunity && !focused) {
|
|
613
|
+
return {
|
|
614
|
+
size: attrs.size,
|
|
615
|
+
color: attrs.color,
|
|
616
|
+
label: "",
|
|
617
|
+
forceLabel: false,
|
|
618
|
+
zIndex: 0,
|
|
619
|
+
hidden: true,
|
|
620
|
+
type,
|
|
621
|
+
curvature,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
365
625
|
if (!hasFocus) {
|
|
366
626
|
return {
|
|
367
627
|
size: attrs.size,
|
|
@@ -370,6 +630,8 @@ export function reduceSigmaEdge(
|
|
|
370
630
|
forceLabel: false,
|
|
371
631
|
zIndex: 0,
|
|
372
632
|
hidden: false,
|
|
633
|
+
type,
|
|
634
|
+
curvature,
|
|
373
635
|
};
|
|
374
636
|
}
|
|
375
637
|
|
|
@@ -381,6 +643,8 @@ export function reduceSigmaEdge(
|
|
|
381
643
|
forceLabel: Boolean(attrs.edgeLabel),
|
|
382
644
|
zIndex: 2,
|
|
383
645
|
hidden: false,
|
|
646
|
+
type,
|
|
647
|
+
curvature,
|
|
384
648
|
};
|
|
385
649
|
}
|
|
386
650
|
|
|
@@ -391,5 +655,7 @@ export function reduceSigmaEdge(
|
|
|
391
655
|
forceLabel: false,
|
|
392
656
|
zIndex: 0,
|
|
393
657
|
hidden: false,
|
|
658
|
+
type,
|
|
659
|
+
curvature,
|
|
394
660
|
};
|
|
395
661
|
}
|
package/src/lib/router.svelte.ts
CHANGED
|
@@ -14,74 +14,90 @@
|
|
|
14
14
|
// Both implementations satisfy the same `Router` contract, so every view
|
|
15
15
|
// component navigates the same way regardless of which Surface it runs in.
|
|
16
16
|
|
|
17
|
-
import { getContext, setContext } from
|
|
18
|
-
import { collectionPageHref } from
|
|
17
|
+
import { getContext, setContext } from "svelte";
|
|
18
|
+
import { collectionPageHref } from "./collection-pages";
|
|
19
19
|
|
|
20
|
-
/** The
|
|
21
|
-
export type View =
|
|
20
|
+
/** The top-level destinations of the Mountable Root. */
|
|
21
|
+
export type View =
|
|
22
|
+
| "query"
|
|
23
|
+
| "collections"
|
|
24
|
+
| "cluster"
|
|
25
|
+
| "security"
|
|
26
|
+
| "settings"
|
|
27
|
+
| "appearance";
|
|
22
28
|
|
|
23
29
|
/**
|
|
24
30
|
* A navigation intent. `collection` carries a selected collection + subpage
|
|
25
31
|
* within the collections workspace; the other targets are the bare views.
|
|
26
32
|
*/
|
|
27
33
|
export type RouteTarget =
|
|
28
|
-
| { view:
|
|
29
|
-
| { view:
|
|
30
|
-
| { view:
|
|
31
|
-
| { view:
|
|
32
|
-
| { view:
|
|
34
|
+
| { view: "query" }
|
|
35
|
+
| { view: "collections" }
|
|
36
|
+
| { view: "cluster" }
|
|
37
|
+
| { view: "security" }
|
|
38
|
+
| { view: "settings" }
|
|
39
|
+
| { view: "appearance" }
|
|
40
|
+
| { view: "collection"; collection: string; subpage: string };
|
|
33
41
|
|
|
34
42
|
/** Resolved current location, the reactive surface views read from. */
|
|
35
43
|
export interface RouteLocation {
|
|
36
|
-
view: View
|
|
44
|
+
view: View;
|
|
37
45
|
/** Selected collection when inside the collections workspace, else null. */
|
|
38
|
-
collection: string | null
|
|
46
|
+
collection: string | null;
|
|
39
47
|
/** Selected collection subpage (`table`, `graph`, …), else null. */
|
|
40
|
-
subpage: string | null
|
|
48
|
+
subpage: string | null;
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
export interface Router {
|
|
44
52
|
/** Current top-level view. */
|
|
45
|
-
readonly view: View
|
|
53
|
+
readonly view: View;
|
|
46
54
|
/** Selected collection, or null. */
|
|
47
|
-
readonly collection: string | null
|
|
55
|
+
readonly collection: string | null;
|
|
48
56
|
/** Selected collection subpage, or null. */
|
|
49
|
-
readonly subpage: string | null
|
|
57
|
+
readonly subpage: string | null;
|
|
50
58
|
/** Canonical path for the current location (mirrors the SvelteKit URL). */
|
|
51
|
-
readonly path: string
|
|
59
|
+
readonly path: string;
|
|
52
60
|
/** The canonical href a `RouteTarget` maps to (for `<a href>` display). */
|
|
53
|
-
href(target: RouteTarget): string
|
|
61
|
+
href(target: RouteTarget): string;
|
|
54
62
|
/**
|
|
55
63
|
* Navigate to a target. When called from an anchor `onclick`, pass the
|
|
56
64
|
* event so the router can `preventDefault()` (state router) or honour
|
|
57
65
|
* modifier-clicks for open-in-new-tab (Kit router). `replace` swaps the
|
|
58
66
|
* current history entry instead of pushing (Kit only; ignored by state).
|
|
59
67
|
*/
|
|
60
|
-
go(target: RouteTarget, event?: Event, replace?: boolean): void
|
|
68
|
+
go(target: RouteTarget, event?: Event, replace?: boolean): void;
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
/** Map a `RouteTarget` to its canonical path. */
|
|
64
72
|
export function targetToHref(target: RouteTarget): string {
|
|
65
73
|
switch (target.view) {
|
|
66
|
-
case
|
|
67
|
-
return
|
|
68
|
-
case
|
|
69
|
-
return
|
|
70
|
-
case
|
|
71
|
-
return
|
|
72
|
-
case
|
|
73
|
-
return
|
|
74
|
-
case
|
|
75
|
-
return
|
|
74
|
+
case "query":
|
|
75
|
+
return "/query";
|
|
76
|
+
case "collections":
|
|
77
|
+
return "/collections";
|
|
78
|
+
case "cluster":
|
|
79
|
+
return "/cluster";
|
|
80
|
+
case "security":
|
|
81
|
+
return "/security";
|
|
82
|
+
case "settings":
|
|
83
|
+
return "/settings";
|
|
84
|
+
case "appearance":
|
|
85
|
+
return "/appearance";
|
|
86
|
+
case "collection":
|
|
87
|
+
return collectionPageHref(target.collection, target.subpage);
|
|
76
88
|
}
|
|
77
89
|
}
|
|
78
90
|
|
|
79
91
|
/** Map a `RouteTarget` to a resolved location. */
|
|
80
92
|
export function targetToLocation(target: RouteTarget): RouteLocation {
|
|
81
|
-
if (target.view ===
|
|
82
|
-
return {
|
|
93
|
+
if (target.view === "collection") {
|
|
94
|
+
return {
|
|
95
|
+
view: "collections",
|
|
96
|
+
collection: target.collection,
|
|
97
|
+
subpage: target.subpage,
|
|
98
|
+
};
|
|
83
99
|
}
|
|
84
|
-
return { view: target.view, collection: null, subpage: null }
|
|
100
|
+
return { view: target.view, collection: null, subpage: null };
|
|
85
101
|
}
|
|
86
102
|
|
|
87
103
|
/**
|
|
@@ -90,18 +106,25 @@ export function targetToLocation(target: RouteTarget): RouteLocation {
|
|
|
90
106
|
* `/collections` for the bare collections workspace.
|
|
91
107
|
*/
|
|
92
108
|
export function pathToLocation(pathname: string): RouteLocation {
|
|
93
|
-
if (pathname.startsWith(
|
|
94
|
-
|
|
95
|
-
if (pathname.startsWith(
|
|
96
|
-
|
|
109
|
+
if (pathname.startsWith("/query"))
|
|
110
|
+
return { view: "query", collection: null, subpage: null };
|
|
111
|
+
if (pathname.startsWith("/cluster"))
|
|
112
|
+
return { view: "cluster", collection: null, subpage: null };
|
|
113
|
+
if (pathname.startsWith("/security"))
|
|
114
|
+
return { view: "security", collection: null, subpage: null };
|
|
115
|
+
if (pathname.startsWith("/settings"))
|
|
116
|
+
return { view: "settings", collection: null, subpage: null };
|
|
117
|
+
if (pathname.startsWith("/appearance"))
|
|
118
|
+
return { view: "appearance", collection: null, subpage: null };
|
|
119
|
+
const m = pathname.match(/^\/c\/([^/]+)(?:\/p\/([^/]+))?/);
|
|
97
120
|
if (m) {
|
|
98
121
|
return {
|
|
99
|
-
view:
|
|
122
|
+
view: "collections",
|
|
100
123
|
collection: decodeURIComponent(m[1]),
|
|
101
124
|
subpage: m[2] ? decodeURIComponent(m[2]) : null,
|
|
102
|
-
}
|
|
125
|
+
};
|
|
103
126
|
}
|
|
104
|
-
return { view:
|
|
127
|
+
return { view: "collections", collection: null, subpage: null };
|
|
105
128
|
}
|
|
106
129
|
|
|
107
130
|
/**
|
|
@@ -111,43 +134,47 @@ export function pathToLocation(pathname: string): RouteLocation {
|
|
|
111
134
|
*/
|
|
112
135
|
export function createStateRouter(initial?: RouteLocation): Router {
|
|
113
136
|
let loc = $state<RouteLocation>(
|
|
114
|
-
initial ?? { view:
|
|
115
|
-
)
|
|
137
|
+
initial ?? { view: "collections", collection: null, subpage: null }
|
|
138
|
+
);
|
|
116
139
|
return {
|
|
117
140
|
get view() {
|
|
118
|
-
return loc.view
|
|
141
|
+
return loc.view;
|
|
119
142
|
},
|
|
120
143
|
get collection() {
|
|
121
|
-
return loc.collection
|
|
144
|
+
return loc.collection;
|
|
122
145
|
},
|
|
123
146
|
get subpage() {
|
|
124
|
-
return loc.subpage
|
|
147
|
+
return loc.subpage;
|
|
125
148
|
},
|
|
126
149
|
get path() {
|
|
127
150
|
return targetToHref(
|
|
128
151
|
loc.collection
|
|
129
|
-
? {
|
|
130
|
-
|
|
131
|
-
|
|
152
|
+
? {
|
|
153
|
+
view: "collection",
|
|
154
|
+
collection: loc.collection,
|
|
155
|
+
subpage: loc.subpage ?? "table",
|
|
156
|
+
}
|
|
157
|
+
: { view: loc.view }
|
|
158
|
+
);
|
|
132
159
|
},
|
|
133
160
|
href: (target) => targetToHref(target),
|
|
134
161
|
go: (target, event) => {
|
|
135
162
|
// The href is synthetic in state mode, so always suppress the anchor's
|
|
136
163
|
// default navigation and switch the in-memory location instead.
|
|
137
|
-
event?.preventDefault()
|
|
138
|
-
loc = targetToLocation(target)
|
|
164
|
+
event?.preventDefault();
|
|
165
|
+
loc = targetToLocation(target);
|
|
139
166
|
},
|
|
140
|
-
}
|
|
167
|
+
};
|
|
141
168
|
}
|
|
142
169
|
|
|
143
|
-
const ROUTER_KEY = Symbol(
|
|
170
|
+
const ROUTER_KEY = Symbol("red-ui-router");
|
|
144
171
|
|
|
145
|
-
let fallbackRouter: Router | null = null
|
|
172
|
+
let fallbackRouter: Router | null = null;
|
|
146
173
|
|
|
147
174
|
/** Provide a router to descendant view components (called by the Mountable Root). */
|
|
148
175
|
export function setRouter(router: Router): Router {
|
|
149
|
-
setContext(ROUTER_KEY, router)
|
|
150
|
-
return router
|
|
176
|
+
setContext(ROUTER_KEY, router);
|
|
177
|
+
return router;
|
|
151
178
|
}
|
|
152
179
|
|
|
153
180
|
/**
|
|
@@ -156,8 +183,8 @@ export function setRouter(router: Router): Router {
|
|
|
156
183
|
* lack of a provider.
|
|
157
184
|
*/
|
|
158
185
|
export function useRouter(): Router {
|
|
159
|
-
const router = getContext<Router | undefined>(ROUTER_KEY)
|
|
160
|
-
if (router) return router
|
|
161
|
-
if (!fallbackRouter) fallbackRouter = createStateRouter()
|
|
162
|
-
return fallbackRouter
|
|
186
|
+
const router = getContext<Router | undefined>(ROUTER_KEY);
|
|
187
|
+
if (router) return router;
|
|
188
|
+
if (!fallbackRouter) fallbackRouter = createStateRouter();
|
|
189
|
+
return fallbackRouter;
|
|
163
190
|
}
|