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