@carlonicora/nextjs-jsonapi 1.97.2 → 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-LRJUTHTW.mjs → BlockNoteEditor-IBV3KBQM.mjs} +2 -2
- package/dist/{BlockNoteEditor-OOZGXU6E.js → BlockNoteEditor-LYJUF5N4.js} +9 -9
- package/dist/{BlockNoteEditor-OOZGXU6E.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-XYGK26YG.mjs → chunk-CDNVUON3.mjs} +374 -148
- package/dist/chunk-CDNVUON3.mjs.map +1 -0
- package/dist/{chunk-HCOX3PKM.js → chunk-TRTKIQUB.js} +608 -382
- package/dist/chunk-TRTKIQUB.js.map +1 -0
- package/dist/client/index.d.mts +32 -3
- package/dist/client/index.d.ts +32 -3
- 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.d.mts +5 -2
- package/dist/components/index.d.ts +5 -2
- package/dist/components/index.js +2 -2
- package/dist/components/index.mjs +1 -1
- package/dist/{content.fields-hzZvhlBd.d.mts → content.fields-xH3TGvVk.d.mts} +2 -0
- package/dist/{content.fields-hzZvhlBd.d.ts → content.fields-xH3TGvVk.d.ts} +2 -0
- package/dist/contexts/index.js +2 -2
- package/dist/contexts/index.mjs +1 -1
- package/dist/core/index.d.mts +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/package.json +3 -1
- package/src/components/forms/FormDate.tsx +27 -3
- package/src/components/forms/FormDateTime.tsx +40 -8
- 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 +310 -148
- package/src/interfaces/d3.node.interface.ts +2 -0
- package/dist/chunk-HCOX3PKM.js.map +0 -1
- package/dist/chunk-XYGK26YG.mjs.map +0 -1
- /package/dist/{BlockNoteEditor-LRJUTHTW.mjs.map → BlockNoteEditor-IBV3KBQM.mjs.map} +0 -0
|
@@ -26,6 +26,7 @@ export function FormDateTime({
|
|
|
26
26
|
minDate,
|
|
27
27
|
onChange,
|
|
28
28
|
allowEmpty,
|
|
29
|
+
defaultMonth,
|
|
29
30
|
}: {
|
|
30
31
|
form: any;
|
|
31
32
|
id: string;
|
|
@@ -34,12 +35,18 @@ export function FormDateTime({
|
|
|
34
35
|
minDate?: Date;
|
|
35
36
|
onChange?: (date?: Date) => Promise<void>;
|
|
36
37
|
allowEmpty?: boolean;
|
|
38
|
+
defaultMonth?: Date;
|
|
37
39
|
}) {
|
|
38
40
|
const [open, setOpen] = useState<boolean>(false);
|
|
39
41
|
const t = useI18nTranslations();
|
|
40
42
|
const locale = useI18nLocale();
|
|
41
43
|
const dateFnsLocale = useI18nDateFnsLocale();
|
|
42
44
|
|
|
45
|
+
const [displayMonth, setDisplayMonth] = useState<Date>(() => {
|
|
46
|
+
const currentValue = form.getValues(id);
|
|
47
|
+
return currentValue || defaultMonth || new Date();
|
|
48
|
+
});
|
|
49
|
+
|
|
43
50
|
// Locale-aware date-time formatter
|
|
44
51
|
const dateTimeFormatter = useMemo(
|
|
45
52
|
() =>
|
|
@@ -123,7 +130,10 @@ export function FormDateTime({
|
|
|
123
130
|
<div className="flex flex-col space-y-4">
|
|
124
131
|
<Calendar
|
|
125
132
|
mode="single"
|
|
133
|
+
captionLayout="dropdown"
|
|
126
134
|
selected={field.value}
|
|
135
|
+
month={displayMonth}
|
|
136
|
+
onMonthChange={setDisplayMonth}
|
|
127
137
|
onSelect={(date) => {
|
|
128
138
|
if (date) {
|
|
129
139
|
// Preserve the current time when selecting a new date
|
|
@@ -135,6 +145,7 @@ export function FormDateTime({
|
|
|
135
145
|
newDate.setHours(selectedHours, selectedMinutes);
|
|
136
146
|
}
|
|
137
147
|
form.setValue(id, newDate);
|
|
148
|
+
setDisplayMonth(newDate);
|
|
138
149
|
if (onChange) onChange(newDate);
|
|
139
150
|
|
|
140
151
|
// Update time state values
|
|
@@ -144,6 +155,9 @@ export function FormDateTime({
|
|
|
144
155
|
}}
|
|
145
156
|
disabled={(date) => (minDate && date < minDate ? true : false)}
|
|
146
157
|
locale={dateFnsLocale}
|
|
158
|
+
weekStartsOn={1}
|
|
159
|
+
startMonth={new Date(1900, 0)}
|
|
160
|
+
endMonth={new Date(new Date().getFullYear() + 10, 11)}
|
|
147
161
|
/>
|
|
148
162
|
<div className="flex flex-row items-end justify-center space-x-4">
|
|
149
163
|
<div className="flex flex-col space-y-2">
|
|
@@ -201,14 +215,32 @@ export function FormDateTime({
|
|
|
201
215
|
</Select>
|
|
202
216
|
</div>
|
|
203
217
|
</div>
|
|
204
|
-
<
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
218
|
+
<div className="mt-2 flex flex-row gap-x-2">
|
|
219
|
+
{allowEmpty !== false && (
|
|
220
|
+
<Button
|
|
221
|
+
type="button"
|
|
222
|
+
variant="outline"
|
|
223
|
+
className="flex-1"
|
|
224
|
+
disabled={!field.value}
|
|
225
|
+
onClick={() => {
|
|
226
|
+
if (onChange) onChange(undefined);
|
|
227
|
+
form.setValue(id, "");
|
|
228
|
+
setOpen(false);
|
|
229
|
+
}}
|
|
230
|
+
>
|
|
231
|
+
{t(`ui.buttons.clear`)}
|
|
232
|
+
</Button>
|
|
233
|
+
)}
|
|
234
|
+
<Button
|
|
235
|
+
type="button"
|
|
236
|
+
className="flex-1"
|
|
237
|
+
onClick={() => {
|
|
238
|
+
setOpen(false);
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
{t(`ui.buttons.select_date`)}
|
|
242
|
+
</Button>
|
|
243
|
+
</div>
|
|
212
244
|
</div>
|
|
213
245
|
</PopoverContent>
|
|
214
246
|
</Popover>
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { computeLayeredLayout } from "../computeLayeredLayout";
|
|
3
|
+
import type { D3Link, D3Node } from "../../interfaces";
|
|
4
|
+
|
|
5
|
+
function makeNode(id: string, name = id, extra: Partial<D3Node> = {}): D3Node {
|
|
6
|
+
return { id, name, instanceType: "test", ...extra };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function makeLink(source: string, target: string): D3Link {
|
|
10
|
+
return { source, target } as D3Link;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const MIN_WIDTH = 80;
|
|
14
|
+
const MIN_HEIGHT = 80;
|
|
15
|
+
|
|
16
|
+
describe("computeLayeredLayout", () => {
|
|
17
|
+
it("returns an empty map for an empty graph", () => {
|
|
18
|
+
const result = computeLayeredLayout([], [], {
|
|
19
|
+
minNodeWidth: MIN_WIDTH,
|
|
20
|
+
minNodeHeight: MIN_HEIGHT,
|
|
21
|
+
});
|
|
22
|
+
expect(result).not.toBeNull();
|
|
23
|
+
expect(result!.size).toBe(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("places a single isolated node at a finite coordinate", () => {
|
|
27
|
+
const nodes = [makeNode("a")];
|
|
28
|
+
const result = computeLayeredLayout(nodes, [], {
|
|
29
|
+
minNodeWidth: MIN_WIDTH,
|
|
30
|
+
minNodeHeight: MIN_HEIGHT,
|
|
31
|
+
});
|
|
32
|
+
expect(result).not.toBeNull();
|
|
33
|
+
const pos = result!.get("a");
|
|
34
|
+
expect(pos).toBeDefined();
|
|
35
|
+
expect(Number.isFinite(pos!.x)).toBe(true);
|
|
36
|
+
expect(Number.isFinite(pos!.y)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("orders a linear chain left-to-right when rankdir=LR", () => {
|
|
40
|
+
const nodes = [makeNode("a"), makeNode("b"), makeNode("c")];
|
|
41
|
+
const links = [makeLink("a", "b"), makeLink("b", "c")];
|
|
42
|
+
const result = computeLayeredLayout(nodes, links, {
|
|
43
|
+
rankdir: "LR",
|
|
44
|
+
minNodeWidth: MIN_WIDTH,
|
|
45
|
+
minNodeHeight: MIN_HEIGHT,
|
|
46
|
+
});
|
|
47
|
+
expect(result).not.toBeNull();
|
|
48
|
+
const a = result!.get("a")!;
|
|
49
|
+
const b = result!.get("b")!;
|
|
50
|
+
const c = result!.get("c")!;
|
|
51
|
+
expect(a.x).toBeLessThan(b.x);
|
|
52
|
+
expect(b.x).toBeLessThan(c.x);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("orders a linear chain top-to-bottom when rankdir=TB", () => {
|
|
56
|
+
const nodes = [makeNode("a"), makeNode("b"), makeNode("c")];
|
|
57
|
+
const links = [makeLink("a", "b"), makeLink("b", "c")];
|
|
58
|
+
const result = computeLayeredLayout(nodes, links, {
|
|
59
|
+
rankdir: "TB",
|
|
60
|
+
minNodeWidth: MIN_WIDTH,
|
|
61
|
+
minNodeHeight: MIN_HEIGHT,
|
|
62
|
+
});
|
|
63
|
+
expect(result).not.toBeNull();
|
|
64
|
+
const a = result!.get("a")!;
|
|
65
|
+
const b = result!.get("b")!;
|
|
66
|
+
const c = result!.get("c")!;
|
|
67
|
+
expect(a.y).toBeLessThan(b.y);
|
|
68
|
+
expect(b.y).toBeLessThan(c.y);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("places siblings on the same rank but separated", () => {
|
|
72
|
+
const nodes = [makeNode("a"), makeNode("b"), makeNode("c")];
|
|
73
|
+
const links = [makeLink("a", "b"), makeLink("a", "c")];
|
|
74
|
+
const result = computeLayeredLayout(nodes, links, {
|
|
75
|
+
rankdir: "LR",
|
|
76
|
+
nodesep: 50,
|
|
77
|
+
minNodeWidth: MIN_WIDTH,
|
|
78
|
+
minNodeHeight: MIN_HEIGHT,
|
|
79
|
+
});
|
|
80
|
+
expect(result).not.toBeNull();
|
|
81
|
+
const a = result!.get("a")!;
|
|
82
|
+
const b = result!.get("b")!;
|
|
83
|
+
const c = result!.get("c")!;
|
|
84
|
+
expect(b.x).toBeGreaterThan(a.x);
|
|
85
|
+
expect(c.x).toBeGreaterThan(a.x);
|
|
86
|
+
expect(Math.abs(b.x - c.x)).toBeLessThan(5);
|
|
87
|
+
expect(Math.abs(b.y - c.y)).toBeGreaterThanOrEqual(50);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("uses label width to widen ranks for long names", () => {
|
|
91
|
+
const nodes = [makeNode("a", "x"), makeNode("b", "x".repeat(50))];
|
|
92
|
+
const links = [makeLink("a", "b")];
|
|
93
|
+
const narrow = computeLayeredLayout(nodes, links, {
|
|
94
|
+
rankdir: "LR",
|
|
95
|
+
minNodeWidth: MIN_WIDTH,
|
|
96
|
+
minNodeHeight: MIN_HEIGHT,
|
|
97
|
+
});
|
|
98
|
+
const wider = computeLayeredLayout(
|
|
99
|
+
[makeNode("a", "x".repeat(50)), makeNode("b", "x".repeat(50))],
|
|
100
|
+
[makeLink("a", "b")],
|
|
101
|
+
{
|
|
102
|
+
rankdir: "LR",
|
|
103
|
+
minNodeWidth: MIN_WIDTH,
|
|
104
|
+
minNodeHeight: MIN_HEIGHT,
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
const narrowGap = narrow!.get("b")!.x - narrow!.get("a")!.x;
|
|
108
|
+
const widerGap = wider!.get("b")!.x - wider!.get("a")!.x;
|
|
109
|
+
expect(widerGap).toBeGreaterThan(narrowGap);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("handles a scene-shaped graph (scene chain + location branches)", () => {
|
|
113
|
+
const nodes = [
|
|
114
|
+
makeNode("s1", "Scene 1", { instanceType: "scenes" }),
|
|
115
|
+
makeNode("s2", "Scene 2", { instanceType: "scenes" }),
|
|
116
|
+
makeNode("s3", "Scene 3", { instanceType: "scenes" }),
|
|
117
|
+
makeNode("loc1", "Loc 1", { instanceType: "locations" }),
|
|
118
|
+
makeNode("loc2", "Loc 2", { instanceType: "locations" }),
|
|
119
|
+
makeNode("loc3", "Loc 3", { instanceType: "locations" }),
|
|
120
|
+
];
|
|
121
|
+
const links = [
|
|
122
|
+
makeLink("s1", "s2"),
|
|
123
|
+
makeLink("s2", "s3"),
|
|
124
|
+
makeLink("s1", "loc1"),
|
|
125
|
+
makeLink("s2", "loc2"),
|
|
126
|
+
makeLink("s3", "loc3"),
|
|
127
|
+
];
|
|
128
|
+
const result = computeLayeredLayout(nodes, links, {
|
|
129
|
+
rankdir: "LR",
|
|
130
|
+
minNodeWidth: MIN_WIDTH,
|
|
131
|
+
minNodeHeight: MIN_HEIGHT,
|
|
132
|
+
});
|
|
133
|
+
expect(result).not.toBeNull();
|
|
134
|
+
expect(result!.size).toBe(6);
|
|
135
|
+
expect(result!.get("s1")!.x).toBeLessThan(result!.get("s2")!.x);
|
|
136
|
+
expect(result!.get("s2")!.x).toBeLessThan(result!.get("s3")!.x);
|
|
137
|
+
expect(result!.get("loc1")!.x).toBeGreaterThan(result!.get("s1")!.x);
|
|
138
|
+
expect(result!.get("loc2")!.x).toBeGreaterThan(result!.get("s2")!.x);
|
|
139
|
+
expect(result!.get("loc3")!.x).toBeGreaterThan(result!.get("s3")!.x);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("ignores links to/from unknown nodes", () => {
|
|
143
|
+
const nodes = [makeNode("a"), makeNode("b")];
|
|
144
|
+
const links = [makeLink("a", "b"), makeLink("a", "ghost"), makeLink("ghost", "b")];
|
|
145
|
+
const result = computeLayeredLayout(nodes, links, {
|
|
146
|
+
minNodeWidth: MIN_WIDTH,
|
|
147
|
+
minNodeHeight: MIN_HEIGHT,
|
|
148
|
+
});
|
|
149
|
+
expect(result).not.toBeNull();
|
|
150
|
+
expect(result!.size).toBe(2);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as dagre from "dagre";
|
|
2
|
+
import { D3Link, D3Node } from "../interfaces";
|
|
3
|
+
|
|
4
|
+
export type LayeredRankDir = "LR" | "RL" | "TB" | "BT";
|
|
5
|
+
|
|
6
|
+
export interface LayeredLayoutOptions {
|
|
7
|
+
rankdir?: LayeredRankDir;
|
|
8
|
+
nodesep?: number;
|
|
9
|
+
ranksep?: number;
|
|
10
|
+
minNodeWidth: number;
|
|
11
|
+
minNodeHeight: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface LayeredLayoutPosition {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DEFAULT_RANKDIR: LayeredRankDir = "LR";
|
|
20
|
+
const DEFAULT_NODESEP = 50;
|
|
21
|
+
const DEFAULT_RANKSEP = 120;
|
|
22
|
+
|
|
23
|
+
const TITLE_PX_PER_CHAR_16 = 8;
|
|
24
|
+
const NAME_PX_PER_CHAR_12 = 6.5;
|
|
25
|
+
const NAME_PX_PER_CHAR_16_BOLD = 8;
|
|
26
|
+
const SUBTITLE_PX_PER_CHAR_11 = 6;
|
|
27
|
+
const LABEL_PADDING_PX = 16;
|
|
28
|
+
|
|
29
|
+
function estimateLabelWidth(node: D3Node): number {
|
|
30
|
+
if (node.subtitle) {
|
|
31
|
+
const titleWidth = (node.name?.length ?? 0) * TITLE_PX_PER_CHAR_16;
|
|
32
|
+
const subtitleWidth = node.subtitle.length * SUBTITLE_PX_PER_CHAR_11;
|
|
33
|
+
return Math.max(titleWidth, subtitleWidth) + LABEL_PADDING_PX;
|
|
34
|
+
}
|
|
35
|
+
const perChar = node.bold ? NAME_PX_PER_CHAR_16_BOLD : NAME_PX_PER_CHAR_12;
|
|
36
|
+
return (node.name?.length ?? 0) * perChar + LABEL_PADDING_PX;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function linkEndpointId(end: D3Link["source"] | D3Link["target"]): string {
|
|
40
|
+
return typeof end === "string" ? end : end.id;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Compute a layered DAG layout using dagre. Pure function: no DOM, no React,
|
|
45
|
+
* no d3 globals. Returns a Map of node.id -> { x, y } in graph coordinates.
|
|
46
|
+
*
|
|
47
|
+
* Returns null if dagre.layout throws (e.g. an unexpected cycle). Callers
|
|
48
|
+
* should fall back to their previous layout in that case.
|
|
49
|
+
*/
|
|
50
|
+
export function computeLayeredLayout(
|
|
51
|
+
nodes: D3Node[],
|
|
52
|
+
links: D3Link[],
|
|
53
|
+
opts: LayeredLayoutOptions,
|
|
54
|
+
): Map<string, LayeredLayoutPosition> | null {
|
|
55
|
+
if (nodes.length === 0) return new Map();
|
|
56
|
+
|
|
57
|
+
const rankdir = opts.rankdir ?? DEFAULT_RANKDIR;
|
|
58
|
+
const nodesep = opts.nodesep ?? DEFAULT_NODESEP;
|
|
59
|
+
const ranksep = opts.ranksep ?? DEFAULT_RANKSEP;
|
|
60
|
+
|
|
61
|
+
const g = new dagre.graphlib.Graph({ directed: true });
|
|
62
|
+
g.setGraph({ rankdir, nodesep, ranksep, marginx: 20, marginy: 20 });
|
|
63
|
+
g.setDefaultEdgeLabel(() => ({}));
|
|
64
|
+
|
|
65
|
+
for (const node of nodes) {
|
|
66
|
+
const width = Math.max(opts.minNodeWidth, estimateLabelWidth(node));
|
|
67
|
+
const height = opts.minNodeHeight;
|
|
68
|
+
g.setNode(node.id, { width, height });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const seen = new Set<string>();
|
|
72
|
+
for (const link of links) {
|
|
73
|
+
const sourceId = linkEndpointId(link.source);
|
|
74
|
+
const targetId = linkEndpointId(link.target);
|
|
75
|
+
if (!g.hasNode(sourceId) || !g.hasNode(targetId)) continue;
|
|
76
|
+
const key = `${sourceId}->${targetId}`;
|
|
77
|
+
if (seen.has(key)) continue;
|
|
78
|
+
seen.add(key);
|
|
79
|
+
g.setEdge(sourceId, targetId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
dagre.layout(g);
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const positions = new Map<string, LayeredLayoutPosition>();
|
|
89
|
+
for (const node of nodes) {
|
|
90
|
+
const laid = g.node(node.id);
|
|
91
|
+
if (laid && Number.isFinite(laid.x) && Number.isFinite(laid.y)) {
|
|
92
|
+
positions.set(node.id, { x: laid.x, y: laid.y });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return positions;
|
|
96
|
+
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -35,3 +35,9 @@ export * from "./useNotificationSync";
|
|
|
35
35
|
export * from "./usePageTracker";
|
|
36
36
|
export * from "./usePushNotifications";
|
|
37
37
|
export * from "./useSocket";
|
|
38
|
+
export {
|
|
39
|
+
computeLayeredLayout,
|
|
40
|
+
type LayeredLayoutOptions,
|
|
41
|
+
type LayeredLayoutPosition,
|
|
42
|
+
type LayeredRankDir,
|
|
43
|
+
} from "./computeLayeredLayout";
|