@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.
Files changed (37) hide show
  1. package/dist/{BlockNoteEditor-LRJUTHTW.mjs → BlockNoteEditor-IBV3KBQM.mjs} +2 -2
  2. package/dist/{BlockNoteEditor-OOZGXU6E.js → BlockNoteEditor-LYJUF5N4.js} +9 -9
  3. package/dist/{BlockNoteEditor-OOZGXU6E.js.map → BlockNoteEditor-LYJUF5N4.js.map} +1 -1
  4. package/dist/billing/index.js +299 -299
  5. package/dist/billing/index.mjs +1 -1
  6. package/dist/{chunk-XYGK26YG.mjs → chunk-CDNVUON3.mjs} +374 -148
  7. package/dist/chunk-CDNVUON3.mjs.map +1 -0
  8. package/dist/{chunk-HCOX3PKM.js → chunk-TRTKIQUB.js} +608 -382
  9. package/dist/chunk-TRTKIQUB.js.map +1 -0
  10. package/dist/client/index.d.mts +32 -3
  11. package/dist/client/index.d.ts +32 -3
  12. package/dist/client/index.js +4 -2
  13. package/dist/client/index.js.map +1 -1
  14. package/dist/client/index.mjs +3 -1
  15. package/dist/components/index.d.mts +5 -2
  16. package/dist/components/index.d.ts +5 -2
  17. package/dist/components/index.js +2 -2
  18. package/dist/components/index.mjs +1 -1
  19. package/dist/{content.fields-hzZvhlBd.d.mts → content.fields-xH3TGvVk.d.mts} +2 -0
  20. package/dist/{content.fields-hzZvhlBd.d.ts → content.fields-xH3TGvVk.d.ts} +2 -0
  21. package/dist/contexts/index.js +2 -2
  22. package/dist/contexts/index.mjs +1 -1
  23. package/dist/core/index.d.mts +1 -1
  24. package/dist/core/index.d.ts +1 -1
  25. package/dist/index.d.mts +1 -1
  26. package/dist/index.d.ts +1 -1
  27. package/package.json +3 -1
  28. package/src/components/forms/FormDate.tsx +27 -3
  29. package/src/components/forms/FormDateTime.tsx +40 -8
  30. package/src/hooks/__tests__/computeLayeredLayout.spec.ts +152 -0
  31. package/src/hooks/computeLayeredLayout.ts +96 -0
  32. package/src/hooks/index.ts +6 -0
  33. package/src/hooks/useCustomD3Graph.tsx +310 -148
  34. package/src/interfaces/d3.node.interface.ts +2 -0
  35. package/dist/chunk-HCOX3PKM.js.map +0 -1
  36. package/dist/chunk-XYGK26YG.mjs.map +0 -1
  37. /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
- <Button
205
- className="mt-2"
206
- onClick={() => {
207
- setOpen(false);
208
- }}
209
- >
210
- {t(`ui.buttons.select_date`)}
211
- </Button>
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
+ }
@@ -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";