@carlonicora/nextjs-jsonapi 1.98.0 → 1.99.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/dist/{BlockNoteEditor-HWQHTLEB.mjs → BlockNoteEditor-IBV3KBQM.mjs} +2 -2
- package/dist/{BlockNoteEditor-3SXAMY6O.js → BlockNoteEditor-LYJUF5N4.js} +9 -9
- package/dist/{BlockNoteEditor-3SXAMY6O.js.map → BlockNoteEditor-LYJUF5N4.js.map} +1 -1
- package/dist/billing/index.js +299 -299
- package/dist/billing/index.mjs +1 -1
- package/dist/{chunk-TLTENUI6.mjs → chunk-CDNVUON3.mjs} +236 -108
- package/dist/chunk-CDNVUON3.mjs.map +1 -0
- package/dist/{chunk-S5LH5422.js → chunk-TRTKIQUB.js} +471 -343
- package/dist/chunk-TRTKIQUB.js.map +1 -0
- package/dist/client/index.d.mts +28 -1
- package/dist/client/index.d.ts +28 -1
- package/dist/client/index.js +4 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +3 -1
- package/dist/components/index.js +2 -2
- package/dist/components/index.mjs +1 -1
- package/dist/contexts/index.js +2 -2
- package/dist/contexts/index.mjs +1 -1
- package/package.json +3 -1
- package/src/hooks/__tests__/computeLayeredLayout.spec.ts +152 -0
- package/src/hooks/computeLayeredLayout.ts +96 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useCustomD3Graph.tsx +221 -140
- package/dist/chunk-S5LH5422.js.map +0 -1
- package/dist/chunk-TLTENUI6.mjs.map +0 -1
- /package/dist/{BlockNoteEditor-HWQHTLEB.mjs.map → BlockNoteEditor-IBV3KBQM.mjs.map} +0 -0
|
@@ -5,6 +5,7 @@ import { Loader2 } from "lucide-react";
|
|
|
5
5
|
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
6
6
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
7
7
|
import { D3Link, D3Node } from "../interfaces";
|
|
8
|
+
import { computeLayeredLayout, type LayeredRankDir } from "./computeLayeredLayout";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Custom hook for D3 graph visualization with larger circles and more interactive features
|
|
@@ -14,7 +15,15 @@ export function useCustomD3Graph(
|
|
|
14
15
|
links: D3Link[],
|
|
15
16
|
onNodeClick: (nodeId: string) => void,
|
|
16
17
|
visibleNodeIds?: Set<string>,
|
|
17
|
-
options?: {
|
|
18
|
+
options?: {
|
|
19
|
+
directed?: boolean;
|
|
20
|
+
layout?: "radial" | "layered";
|
|
21
|
+
layered?: {
|
|
22
|
+
rankdir?: LayeredRankDir;
|
|
23
|
+
nodesep?: number;
|
|
24
|
+
ranksep?: number;
|
|
25
|
+
};
|
|
26
|
+
},
|
|
18
27
|
loadingNodeIds?: Set<string>,
|
|
19
28
|
containerKey?: string | number,
|
|
20
29
|
) {
|
|
@@ -230,168 +239,228 @@ export function useCustomD3Graph(
|
|
|
230
239
|
.on("wheel.zoom", null)
|
|
231
240
|
.on("dblclick.zoom", null);
|
|
232
241
|
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
242
|
+
const layoutMode = options?.layout ?? "radial";
|
|
243
|
+
let layeredPositionsApplied = false;
|
|
244
|
+
|
|
245
|
+
if (layoutMode === "layered") {
|
|
246
|
+
const layeredOpts = options?.layered ?? {};
|
|
247
|
+
const positions = computeLayeredLayout(visibleNodes, visibleLinks, {
|
|
248
|
+
rankdir: layeredOpts.rankdir ?? "LR",
|
|
249
|
+
nodesep: layeredOpts.nodesep,
|
|
250
|
+
ranksep: layeredOpts.ranksep,
|
|
251
|
+
minNodeWidth: nodeRadius * 2,
|
|
252
|
+
minNodeHeight: nodeRadius * 2,
|
|
253
|
+
});
|
|
237
254
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
255
|
+
if (positions) {
|
|
256
|
+
visibleNodes.forEach((node) => {
|
|
257
|
+
const saved = nodePositionsRef.current.get(node.id);
|
|
258
|
+
if (saved) {
|
|
259
|
+
node.fx = saved.x;
|
|
260
|
+
node.fy = saved.y;
|
|
261
|
+
node.x = saved.x;
|
|
262
|
+
node.y = saved.y;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const pos = positions.get(node.id);
|
|
266
|
+
if (pos) {
|
|
267
|
+
node.fx = pos.x;
|
|
268
|
+
node.fy = pos.y;
|
|
269
|
+
node.x = pos.x;
|
|
270
|
+
node.y = pos.y;
|
|
271
|
+
nodePositionsRef.current.set(node.id, { x: pos.x, y: pos.y });
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
// d3.forceLink normally mutates link.source/link.target from string
|
|
275
|
+
// IDs to D3Node references when the simulation initializes. The
|
|
276
|
+
// layered branch skips the simulation, so we resolve those refs
|
|
277
|
+
// ourselves — otherwise the downstream link rendering reads
|
|
278
|
+
// `(d.source as D3Node).x` against a string and draws every line
|
|
279
|
+
// at (0,0)→(0,0).
|
|
280
|
+
const nodeById = new Map<string, D3Node>();
|
|
281
|
+
visibleNodes.forEach((n) => nodeById.set(n.id, n));
|
|
282
|
+
visibleLinks.forEach((link) => {
|
|
283
|
+
if (typeof link.source === "string") {
|
|
284
|
+
const src = nodeById.get(link.source);
|
|
285
|
+
if (src) link.source = src;
|
|
286
|
+
}
|
|
287
|
+
if (typeof link.target === "string") {
|
|
288
|
+
const tgt = nodeById.get(link.target);
|
|
289
|
+
if (tgt) link.target = tgt;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
layeredPositionsApplied = true;
|
|
293
|
+
} else {
|
|
294
|
+
console.warn("[useCustomD3Graph] Layered layout failed; falling back to radial.");
|
|
247
295
|
}
|
|
248
|
-
|
|
296
|
+
}
|
|
249
297
|
|
|
250
|
-
|
|
251
|
-
depth: 0,
|
|
252
|
-
parent: null,
|
|
253
|
-
children: [],
|
|
254
|
-
});
|
|
298
|
+
let simulation: d3.Simulation<D3Node, D3Link> | null = null;
|
|
255
299
|
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
const
|
|
300
|
+
if (!layeredPositionsApplied) {
|
|
301
|
+
const childDistanceFromRoot = Math.min(width, height) * 0.4;
|
|
302
|
+
const grandchildDistanceFromChild = nodeRadius * 10;
|
|
303
|
+
|
|
304
|
+
const centralNodeId = nodes[0].id;
|
|
259
305
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
306
|
+
const nodeHierarchy = new Map<
|
|
307
|
+
string,
|
|
308
|
+
{
|
|
309
|
+
depth: number;
|
|
310
|
+
parent: string | null;
|
|
311
|
+
children: string[];
|
|
312
|
+
angle?: number;
|
|
313
|
+
x?: number;
|
|
314
|
+
y?: number;
|
|
265
315
|
}
|
|
266
|
-
|
|
267
|
-
});
|
|
316
|
+
>();
|
|
268
317
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
318
|
+
nodeHierarchy.set(centralNodeId, {
|
|
319
|
+
depth: 0,
|
|
320
|
+
parent: null,
|
|
321
|
+
children: [],
|
|
322
|
+
});
|
|
272
323
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
sourceNode.children.push(targetId);
|
|
277
|
-
}
|
|
278
|
-
});
|
|
324
|
+
visibleLinks.forEach((link) => {
|
|
325
|
+
const sourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
326
|
+
const targetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
279
327
|
|
|
280
|
-
|
|
328
|
+
if (sourceId === centralNodeId) {
|
|
329
|
+
nodeHierarchy.set(targetId, { depth: 1, parent: centralNodeId, children: [] });
|
|
330
|
+
const rootNode = nodeHierarchy.get(centralNodeId);
|
|
331
|
+
if (rootNode) {
|
|
332
|
+
rootNode.children.push(targetId);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
});
|
|
281
336
|
|
|
282
|
-
|
|
337
|
+
visibleLinks.forEach((link) => {
|
|
338
|
+
const sourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
339
|
+
const targetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
283
340
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
childNode.y = height / 2 + childDistanceFromRoot * Math.sin(angle);
|
|
291
|
-
}
|
|
292
|
-
});
|
|
341
|
+
const sourceNode = nodeHierarchy.get(sourceId);
|
|
342
|
+
if (sourceNode && sourceNode.depth === 1 && !nodeHierarchy.has(targetId)) {
|
|
343
|
+
nodeHierarchy.set(targetId, { depth: 2, parent: sourceId, children: [] });
|
|
344
|
+
sourceNode.children.push(targetId);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
293
347
|
|
|
294
|
-
|
|
295
|
-
if (node.depth === 1 && node.angle !== undefined && node.x !== undefined && node.y !== undefined) {
|
|
296
|
-
const childAngle = node.angle;
|
|
297
|
-
const childX = node.x;
|
|
298
|
-
const childY = node.y;
|
|
299
|
-
const grandchildren = node.children;
|
|
300
|
-
|
|
301
|
-
if (grandchildren.length === 0) continue;
|
|
302
|
-
|
|
303
|
-
const dirX = childX - width / 2;
|
|
304
|
-
const dirY = childY - height / 2;
|
|
305
|
-
const dirLength = Math.sqrt(dirX * dirX + dirY * dirY);
|
|
306
|
-
|
|
307
|
-
const normDirX = dirX / dirLength;
|
|
308
|
-
const normDirY = dirY / dirLength;
|
|
309
|
-
|
|
310
|
-
if (grandchildren.length === 1) {
|
|
311
|
-
const grandchildId = grandchildren[0];
|
|
312
|
-
const grandchildNode = nodeHierarchy.get(grandchildId);
|
|
313
|
-
if (grandchildNode) {
|
|
314
|
-
grandchildNode.x = childX + normDirX * grandchildDistanceFromChild;
|
|
315
|
-
grandchildNode.y = childY + normDirY * grandchildDistanceFromChild;
|
|
316
|
-
grandchildNode.angle = childAngle;
|
|
317
|
-
}
|
|
318
|
-
} else {
|
|
319
|
-
// Multiple grandchildren - arrange in semicircular arc
|
|
320
|
-
const numChildren = grandchildren.length;
|
|
348
|
+
const rootChildren = nodeHierarchy.get(centralNodeId)?.children || [];
|
|
321
349
|
|
|
322
|
-
|
|
323
|
-
const minArc = Math.PI / 3; // 60 degrees
|
|
324
|
-
const maxArc = Math.PI; // 180 degrees
|
|
325
|
-
const arcProgress = Math.min(1, (numChildren - 2) / 5);
|
|
326
|
-
const arcSpan = minArc + arcProgress * (maxArc - minArc);
|
|
350
|
+
const childAngleStep = (2 * Math.PI) / Math.max(rootChildren.length, 1);
|
|
327
351
|
|
|
328
|
-
|
|
329
|
-
|
|
352
|
+
rootChildren.forEach((childId, index) => {
|
|
353
|
+
const childNode = nodeHierarchy.get(childId);
|
|
354
|
+
if (childNode) {
|
|
355
|
+
const angle = index * childAngleStep;
|
|
356
|
+
childNode.angle = angle;
|
|
357
|
+
childNode.x = width / 2 + childDistanceFromRoot * Math.cos(angle);
|
|
358
|
+
childNode.y = height / 2 + childDistanceFromRoot * Math.sin(angle);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
330
361
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
362
|
+
for (const [_nodeId, node] of nodeHierarchy.entries()) {
|
|
363
|
+
if (node.depth === 1 && node.angle !== undefined && node.x !== undefined && node.y !== undefined) {
|
|
364
|
+
const childAngle = node.angle;
|
|
365
|
+
const childX = node.x;
|
|
366
|
+
const childY = node.y;
|
|
367
|
+
const grandchildren = node.children;
|
|
334
368
|
|
|
335
|
-
|
|
336
|
-
const angleOffset = numChildren > 1 ? (index / (numChildren - 1)) * arcSpan : 0;
|
|
337
|
-
const angle = startAngle + angleOffset;
|
|
369
|
+
if (grandchildren.length === 0) continue;
|
|
338
370
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
grandchildNode.angle = angle;
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
371
|
+
const dirX = childX - width / 2;
|
|
372
|
+
const dirY = childY - height / 2;
|
|
373
|
+
const dirLength = Math.sqrt(dirX * dirX + dirY * dirY);
|
|
347
374
|
|
|
348
|
-
|
|
349
|
-
|
|
375
|
+
const normDirX = dirX / dirLength;
|
|
376
|
+
const normDirY = dirY / dirLength;
|
|
350
377
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
378
|
+
if (grandchildren.length === 1) {
|
|
379
|
+
const grandchildId = grandchildren[0];
|
|
380
|
+
const grandchildNode = nodeHierarchy.get(grandchildId);
|
|
381
|
+
if (grandchildNode) {
|
|
382
|
+
grandchildNode.x = childX + normDirX * grandchildDistanceFromChild;
|
|
383
|
+
grandchildNode.y = childY + normDirY * grandchildDistanceFromChild;
|
|
384
|
+
grandchildNode.angle = childAngle;
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
// Multiple grandchildren - arrange in semicircular arc
|
|
388
|
+
const numChildren = grandchildren.length;
|
|
389
|
+
|
|
390
|
+
// Dynamic arc span: scale from 60° (2 children) to 180° (7+ children)
|
|
391
|
+
const minArc = Math.PI / 3; // 60 degrees
|
|
392
|
+
const maxArc = Math.PI; // 180 degrees
|
|
393
|
+
const arcProgress = Math.min(1, (numChildren - 2) / 5);
|
|
394
|
+
const arcSpan = minArc + arcProgress * (maxArc - minArc);
|
|
395
|
+
|
|
396
|
+
// Calculate starting angle (center the arc around the radial direction)
|
|
397
|
+
const startAngle = childAngle - arcSpan / 2;
|
|
398
|
+
|
|
399
|
+
grandchildren.forEach((grandchildId, index) => {
|
|
400
|
+
const grandchildNode = nodeHierarchy.get(grandchildId);
|
|
401
|
+
if (!grandchildNode) return;
|
|
402
|
+
|
|
403
|
+
// Calculate angle for this child
|
|
404
|
+
const angleOffset = numChildren > 1 ? (index / (numChildren - 1)) * arcSpan : 0;
|
|
405
|
+
const angle = startAngle + angleOffset;
|
|
406
|
+
|
|
407
|
+
// Position at constant radius from parent
|
|
408
|
+
grandchildNode.x = childX + grandchildDistanceFromChild * Math.cos(angle);
|
|
409
|
+
grandchildNode.y = childY + grandchildDistanceFromChild * Math.sin(angle);
|
|
410
|
+
grandchildNode.angle = angle;
|
|
411
|
+
});
|
|
412
|
+
}
|
|
366
413
|
}
|
|
367
414
|
}
|
|
368
|
-
});
|
|
369
415
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
.force(
|
|
373
|
-
"link",
|
|
374
|
-
d3
|
|
375
|
-
.forceLink<D3Node, D3Link>(visibleLinks)
|
|
376
|
-
.id((d) => d.id)
|
|
377
|
-
.distance(nodeRadius * 3)
|
|
378
|
-
.strength(0.1),
|
|
379
|
-
)
|
|
380
|
-
.force("charge", d3.forceManyBody().strength(-500).distanceMax(300))
|
|
381
|
-
.force("collision", d3.forceCollide().radius(nodeRadius * 1.2))
|
|
382
|
-
.force("center", d3.forceCenter(width / 2, height / 2).strength(0.1));
|
|
416
|
+
visibleNodes.forEach((node) => {
|
|
417
|
+
const savedPosition = nodePositionsRef.current.get(node.id);
|
|
383
418
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
419
|
+
if (savedPosition) {
|
|
420
|
+
node.fx = savedPosition.x;
|
|
421
|
+
node.fy = savedPosition.y;
|
|
422
|
+
} else {
|
|
423
|
+
const hierarchyNode = nodeHierarchy.get(node.id);
|
|
424
|
+
if (hierarchyNode && hierarchyNode.x !== undefined && hierarchyNode.y !== undefined) {
|
|
425
|
+
node.fx = hierarchyNode.x;
|
|
426
|
+
node.fy = hierarchyNode.y;
|
|
427
|
+
// Save the calculated position so it persists across re-renders
|
|
428
|
+
nodePositionsRef.current.set(node.id, { x: hierarchyNode.x, y: hierarchyNode.y });
|
|
429
|
+
} else if (node.id === centralNodeId) {
|
|
430
|
+
node.fx = width / 2;
|
|
431
|
+
node.fy = height / 2;
|
|
432
|
+
// Save the center position
|
|
433
|
+
nodePositionsRef.current.set(node.id, { x: width / 2, y: height / 2 });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
});
|
|
388
437
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
438
|
+
simulation = d3
|
|
439
|
+
.forceSimulation<D3Node>(visibleNodes)
|
|
440
|
+
.force(
|
|
441
|
+
"link",
|
|
442
|
+
d3
|
|
443
|
+
.forceLink<D3Node, D3Link>(visibleLinks)
|
|
444
|
+
.id((d) => d.id)
|
|
445
|
+
.distance(nodeRadius * 3)
|
|
446
|
+
.strength(0.1),
|
|
447
|
+
)
|
|
448
|
+
.force("charge", d3.forceManyBody().strength(-500).distanceMax(300))
|
|
449
|
+
.force("collision", d3.forceCollide().radius(nodeRadius * 1.2))
|
|
450
|
+
.force("center", d3.forceCenter(width / 2, height / 2).strength(0.1));
|
|
451
|
+
|
|
452
|
+
simulation.stop();
|
|
453
|
+
for (let i = 0; i < 100; i++) {
|
|
454
|
+
simulation.tick();
|
|
393
455
|
}
|
|
394
|
-
|
|
456
|
+
|
|
457
|
+
visibleNodes.forEach((node) => {
|
|
458
|
+
if (node.fx === undefined) {
|
|
459
|
+
node.fx = node.x;
|
|
460
|
+
node.fy = node.y;
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
} // end if (!layeredPositionsApplied)
|
|
395
464
|
|
|
396
465
|
// When directed, stop the line at the target node boundary so the
|
|
397
466
|
// arrowhead tip sits just outside the circle rather than inside it.
|
|
@@ -704,9 +773,21 @@ export function useCustomD3Graph(
|
|
|
704
773
|
});
|
|
705
774
|
|
|
706
775
|
return () => {
|
|
707
|
-
simulation
|
|
776
|
+
simulation?.stop();
|
|
708
777
|
};
|
|
709
|
-
}, [
|
|
778
|
+
}, [
|
|
779
|
+
nodes,
|
|
780
|
+
links,
|
|
781
|
+
colorScale,
|
|
782
|
+
visibleNodeIds,
|
|
783
|
+
options?.directed,
|
|
784
|
+
options?.layout,
|
|
785
|
+
options?.layered?.rankdir,
|
|
786
|
+
options?.layered?.nodesep,
|
|
787
|
+
options?.layered?.ranksep,
|
|
788
|
+
loadingNodeIds,
|
|
789
|
+
onNodeClick,
|
|
790
|
+
]);
|
|
710
791
|
|
|
711
792
|
const zoomIn = useCallback(() => {
|
|
712
793
|
if (!svgRef.current || !zoomBehaviorRef.current) return;
|