@petrarca/sonnet-core 0.7.0 → 0.9.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/README.md +18 -5
- package/dist/{chunk-XAEHYRXC.js → chunk-LOWDCP73.js} +1 -1
- package/dist/chunk-ZM3U3HR2.js +98 -0
- package/dist/chunk-ZM3U3HR2.js.map +1 -0
- package/dist/config/index.d.ts +12 -10
- package/dist/config/index.js +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/entityOptions/index.d.ts +2 -2
- package/dist/hooks/index.d.ts +116 -3
- package/dist/hooks/index.js +153 -12
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +35 -11
- package/dist/index.js.map +1 -1
- package/dist/lazyTree-C0oThxCS.d.ts +139 -0
- package/dist/logger/index.d.ts +6 -0
- package/dist/logger/index.js +13 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/schema/index.d.ts +166 -378
- package/dist/schema/index.js +501 -37
- package/dist/schema/index.js.map +1 -1
- package/dist/search/index.d.ts +18 -1
- package/dist/search/index.js +26 -1
- package/dist/search/index.js.map +1 -1
- package/dist/terminology/index.d.ts +161 -0
- package/dist/terminology/index.js +178 -0
- package/dist/terminology/index.js.map +1 -0
- package/dist/{types-DJ4hb97y.d.ts → types-BiFVnfqr.d.ts} +1 -1
- package/dist/types-HhzIKd1R.d.ts +582 -0
- package/package.json +18 -10
- /package/dist/{chunk-XAEHYRXC.js.map → chunk-LOWDCP73.js.map} +0 -0
package/dist/hooks/index.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildLazyTree,
|
|
3
|
+
lazyTreeLevels,
|
|
4
|
+
lazyTreePages,
|
|
5
|
+
lazyTreeRevealStep
|
|
6
|
+
} from "../chunk-ZM3U3HR2.js";
|
|
1
7
|
import {
|
|
2
8
|
isEqual
|
|
3
9
|
} from "../chunk-XCCOEUVC.js";
|
|
@@ -23,11 +29,21 @@ function useClipboard({
|
|
|
23
29
|
},
|
|
24
30
|
[timeout]
|
|
25
31
|
);
|
|
32
|
+
const paste = useCallback(async () => {
|
|
33
|
+
try {
|
|
34
|
+
const text = await navigator.clipboard.readText();
|
|
35
|
+
setError(null);
|
|
36
|
+
return text;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}, []);
|
|
26
42
|
const reset = useCallback(() => {
|
|
27
43
|
setCopied(false);
|
|
28
44
|
setError(null);
|
|
29
45
|
}, []);
|
|
30
|
-
return { copy, copied, error, reset };
|
|
46
|
+
return { copy, paste, copied, error, reset };
|
|
31
47
|
}
|
|
32
48
|
|
|
33
49
|
// src/hooks/useTreeExpansion.ts
|
|
@@ -57,8 +73,26 @@ function useDisclosure(initialState = false) {
|
|
|
57
73
|
return [opened, { open, close, toggle }];
|
|
58
74
|
}
|
|
59
75
|
|
|
76
|
+
// src/hooks/useMediaQuery.ts
|
|
77
|
+
import { useState as useState4, useEffect } from "react";
|
|
78
|
+
function useMediaQuery(query) {
|
|
79
|
+
const [matches, setMatches] = useState4(() => {
|
|
80
|
+
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
81
|
+
return window.matchMedia(query).matches;
|
|
82
|
+
});
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (typeof window === "undefined" || !window.matchMedia) return;
|
|
85
|
+
const mql = window.matchMedia(query);
|
|
86
|
+
const onChange = () => setMatches(mql.matches);
|
|
87
|
+
onChange();
|
|
88
|
+
mql.addEventListener("change", onChange);
|
|
89
|
+
return () => mql.removeEventListener("change", onChange);
|
|
90
|
+
}, [query]);
|
|
91
|
+
return matches;
|
|
92
|
+
}
|
|
93
|
+
|
|
60
94
|
// src/hooks/useResizablePanel.ts
|
|
61
|
-
import { useState as
|
|
95
|
+
import { useState as useState5, useRef, useCallback as useCallback4, useEffect as useEffect2 } from "react";
|
|
62
96
|
function useResizablePanel(options = {}) {
|
|
63
97
|
const {
|
|
64
98
|
defaultWidth = 320,
|
|
@@ -66,7 +100,7 @@ function useResizablePanel(options = {}) {
|
|
|
66
100
|
maxWidth = 640,
|
|
67
101
|
direction = "left-edge"
|
|
68
102
|
} = options;
|
|
69
|
-
const [panelWidth, setPanelWidth] =
|
|
103
|
+
const [panelWidth, setPanelWidth] = useState5(defaultWidth);
|
|
70
104
|
const resizingRef = useRef(false);
|
|
71
105
|
const startXRef = useRef(0);
|
|
72
106
|
const startWidthRef = useRef(0);
|
|
@@ -101,7 +135,7 @@ function useResizablePanel(options = {}) {
|
|
|
101
135
|
() => setPanelWidth(defaultWidth),
|
|
102
136
|
[defaultWidth]
|
|
103
137
|
);
|
|
104
|
-
|
|
138
|
+
useEffect2(() => {
|
|
105
139
|
window.addEventListener("pointermove", handlePointerMove);
|
|
106
140
|
window.addEventListener("pointerup", finishResize);
|
|
107
141
|
window.addEventListener("pointercancel", finishResize);
|
|
@@ -134,10 +168,10 @@ function useResizablePanel(options = {}) {
|
|
|
134
168
|
}
|
|
135
169
|
|
|
136
170
|
// src/hooks/useFormState.ts
|
|
137
|
-
import { useState as
|
|
171
|
+
import { useState as useState6, useCallback as useCallback5, useMemo } from "react";
|
|
138
172
|
function useFormState(initialData) {
|
|
139
|
-
const [data, setDataState] =
|
|
140
|
-
const [original, setOriginal] =
|
|
173
|
+
const [data, setDataState] = useState6(initialData);
|
|
174
|
+
const [original, setOriginal] = useState6(initialData);
|
|
141
175
|
const isDirty = useMemo(() => {
|
|
142
176
|
return !isEqual(data, original);
|
|
143
177
|
}, [data, original]);
|
|
@@ -169,7 +203,7 @@ function useFormState(initialData) {
|
|
|
169
203
|
}
|
|
170
204
|
|
|
171
205
|
// src/hooks/useEntityOptions.ts
|
|
172
|
-
import { useState as
|
|
206
|
+
import { useState as useState7, useEffect as useEffect3, useCallback as useCallback6 } from "react";
|
|
173
207
|
import { useQuery } from "@tanstack/react-query";
|
|
174
208
|
function resolveEnabled(enabled, mode, activeSearch, minChars, committedSearch) {
|
|
175
209
|
if (!enabled) return false;
|
|
@@ -186,10 +220,10 @@ function useEntityOptions({
|
|
|
186
220
|
limit = 50,
|
|
187
221
|
enabled = true
|
|
188
222
|
}) {
|
|
189
|
-
const [search, setSearch] =
|
|
190
|
-
const [debouncedSearch, setDebouncedSearch] =
|
|
191
|
-
const [committedSearch, setCommittedSearch] =
|
|
192
|
-
|
|
223
|
+
const [search, setSearch] = useState7("");
|
|
224
|
+
const [debouncedSearch, setDebouncedSearch] = useState7("");
|
|
225
|
+
const [committedSearch, setCommittedSearch] = useState7(null);
|
|
226
|
+
useEffect3(() => {
|
|
193
227
|
if (mode === "explicit") return;
|
|
194
228
|
const timer = setTimeout(() => setDebouncedSearch(search), debounceMs);
|
|
195
229
|
return () => clearTimeout(timer);
|
|
@@ -214,12 +248,119 @@ function useEntityOptions({
|
|
|
214
248
|
const options = data ?? [];
|
|
215
249
|
return { options, isLoading, isError, search, setSearch, submit };
|
|
216
250
|
}
|
|
251
|
+
|
|
252
|
+
// src/hooks/useLazyTree.ts
|
|
253
|
+
import { useQueries } from "@tanstack/react-query";
|
|
254
|
+
function useLazyTree(config) {
|
|
255
|
+
const { rootId, expandedIds, morePages, pageParams, queryKey, fetchPage } = config;
|
|
256
|
+
const select = config.select ?? ((page) => page);
|
|
257
|
+
const requests = lazyTreePages(rootId, expandedIds, morePages, pageParams);
|
|
258
|
+
const states = useQueries({
|
|
259
|
+
queries: requests.map((r) => ({
|
|
260
|
+
queryKey: queryKey(r.params),
|
|
261
|
+
queryFn: () => fetchPage(r.params),
|
|
262
|
+
select
|
|
263
|
+
}))
|
|
264
|
+
});
|
|
265
|
+
const levelOf = lazyTreeLevels(requests, states);
|
|
266
|
+
return {
|
|
267
|
+
nodes: buildLazyTree({ ...config, levelOf }),
|
|
268
|
+
root: levelOf(rootId),
|
|
269
|
+
levelOf,
|
|
270
|
+
revealStep: (path) => lazyTreeRevealStep(
|
|
271
|
+
rootId,
|
|
272
|
+
path,
|
|
273
|
+
levelOf,
|
|
274
|
+
(item) => config.toNode(item).id
|
|
275
|
+
)
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/hooks/useLazyTreeReveal.ts
|
|
280
|
+
import { useCallback as useCallback7, useEffect as useEffect4, useState as useState8 } from "react";
|
|
281
|
+
function moreOf(step) {
|
|
282
|
+
return step?.state === "more" ? { levelId: step.levelId, shown: step.shown } : {};
|
|
283
|
+
}
|
|
284
|
+
var SETTLED = /* @__PURE__ */ new Set([
|
|
285
|
+
"found",
|
|
286
|
+
"missing",
|
|
287
|
+
"error"
|
|
288
|
+
]);
|
|
289
|
+
function useLazyTreeReveal({
|
|
290
|
+
revealStep,
|
|
291
|
+
showMore
|
|
292
|
+
}) {
|
|
293
|
+
const [path, setPath] = useState8(void 0);
|
|
294
|
+
const step = path ? revealStep(path) : void 0;
|
|
295
|
+
const { levelId: moreLevel, shown: moreShown } = moreOf(step);
|
|
296
|
+
useEffect4(() => {
|
|
297
|
+
if (moreLevel !== void 0) showMore(moreLevel);
|
|
298
|
+
}, [moreLevel, moreShown, showMore]);
|
|
299
|
+
const settled = step !== void 0 && SETTLED.has(step.state);
|
|
300
|
+
useEffect4(() => {
|
|
301
|
+
if (settled) setPath(void 0);
|
|
302
|
+
}, [settled]);
|
|
303
|
+
const reveal = useCallback7((next) => setPath(next), []);
|
|
304
|
+
return { reveal, step };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/hooks/useTheme.ts
|
|
308
|
+
import { useSyncExternalStore } from "react";
|
|
309
|
+
var DARK_CLASS = "dark";
|
|
310
|
+
var STORAGE_KEY = "sonnet-theme";
|
|
311
|
+
function currentTheme() {
|
|
312
|
+
return document.documentElement.classList.contains(DARK_CLASS) ? "dark" : "light";
|
|
313
|
+
}
|
|
314
|
+
function storedTheme() {
|
|
315
|
+
try {
|
|
316
|
+
const value = localStorage.getItem(STORAGE_KEY);
|
|
317
|
+
return value === "dark" || value === "light" ? value : void 0;
|
|
318
|
+
} catch {
|
|
319
|
+
return void 0;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function applyTheme(theme) {
|
|
323
|
+
document.documentElement.classList.toggle(DARK_CLASS, theme === "dark");
|
|
324
|
+
}
|
|
325
|
+
function setTheme(theme) {
|
|
326
|
+
applyTheme(theme);
|
|
327
|
+
try {
|
|
328
|
+
localStorage.setItem(STORAGE_KEY, theme);
|
|
329
|
+
} catch {
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function initTheme() {
|
|
333
|
+
const prefersDark = window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false;
|
|
334
|
+
applyTheme(storedTheme() ?? (prefersDark ? "dark" : "light"));
|
|
335
|
+
}
|
|
336
|
+
function subscribe(onChange) {
|
|
337
|
+
const observer = new MutationObserver(onChange);
|
|
338
|
+
observer.observe(document.documentElement, {
|
|
339
|
+
attributes: true,
|
|
340
|
+
attributeFilter: ["class"]
|
|
341
|
+
});
|
|
342
|
+
return () => observer.disconnect();
|
|
343
|
+
}
|
|
344
|
+
function useTheme() {
|
|
345
|
+
const theme = useSyncExternalStore(
|
|
346
|
+
subscribe,
|
|
347
|
+
currentTheme,
|
|
348
|
+
() => "light"
|
|
349
|
+
);
|
|
350
|
+
return { theme, setTheme };
|
|
351
|
+
}
|
|
217
352
|
export {
|
|
353
|
+
initTheme,
|
|
354
|
+
setTheme,
|
|
218
355
|
useClipboard,
|
|
219
356
|
useDisclosure,
|
|
220
357
|
useEntityOptions,
|
|
221
358
|
useFormState,
|
|
359
|
+
useLazyTree,
|
|
360
|
+
useLazyTreeReveal,
|
|
361
|
+
useMediaQuery,
|
|
222
362
|
useResizablePanel,
|
|
363
|
+
useTheme,
|
|
223
364
|
useTreeExpansion
|
|
224
365
|
};
|
|
225
366
|
//# sourceMappingURL=index.js.map
|
package/dist/hooks/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/useClipboard.ts","../../src/hooks/useTreeExpansion.ts","../../src/hooks/useDisclosure.ts","../../src/hooks/useResizablePanel.ts","../../src/hooks/useFormState.ts","../../src/hooks/useEntityOptions.ts"],"sourcesContent":["import { useState, useCallback } from \"react\";\n\nexport interface UseClipboardOptions {\n timeout?: number;\n}\n\nexport interface UseClipboardReturn {\n copy: (text: string) => Promise<void>;\n copied: boolean;\n error: Error | null;\n reset: () => void;\n}\n\n/**\n * Custom hook for clipboard operations\n *\n * Replaces Mantine's useClipboard hook with native Clipboard API\n *\n * @param options - Configuration options\n * @param options.timeout - Time in ms before copied state resets (default: 2000)\n * @returns Object with copy function, copied state, error, and reset function\n *\n * @example\n * ```tsx\n * const { copy, copied } = useClipboard({ timeout: 3000 });\n *\n * <button onClick={() => copy('Hello World')}>\n * {copied ? 'Copied!' : 'Copy'}\n * </button>\n * ```\n */\nexport function useClipboard({\n timeout = 2000,\n}: UseClipboardOptions = {}): UseClipboardReturn {\n const [copied, setCopied] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const copy = useCallback(\n async (text: string) => {\n try {\n await navigator.clipboard.writeText(text);\n setCopied(true);\n setError(null);\n\n // Reset copied state after timeout\n setTimeout(() => setCopied(false), timeout);\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n setCopied(false);\n }\n },\n [timeout],\n );\n\n const reset = useCallback(() => {\n setCopied(false);\n setError(null);\n }, []);\n\n return { copy, copied, error, reset };\n}\n","import { useState, useCallback } from \"react\";\n\nexport interface UseTreeExpansionReturn {\n expandedIds: Set<string>;\n onExpand: (id: string) => void;\n onCollapse: (id: string) => void;\n}\n\n/** Manages expand/collapse state for a TreeView. */\nexport function useTreeExpansion(): UseTreeExpansionReturn {\n const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());\n\n const onExpand = useCallback((id: string) => {\n setExpandedIds((prev) => new Set([...prev, id]));\n }, []);\n\n const onCollapse = useCallback((id: string) => {\n setExpandedIds((prev) => {\n const next = new Set(prev);\n next.delete(id);\n return next;\n });\n }, []);\n\n return { expandedIds, onExpand, onCollapse };\n}\n","import { useState, useCallback } from \"react\";\n\nexport interface UseDisclosureReturn {\n open: () => void;\n close: () => void;\n toggle: () => void;\n}\n\n/**\n * Custom hook for managing open/close state\n *\n * Replaces Mantine's useDisclosure hook with simple useState wrapper\n *\n * @param initialState - Initial open state (default: false)\n * @returns Tuple of [opened, handlers] where handlers contains open, close, toggle functions\n *\n * @example\n * ```tsx\n * const [opened, { open, close, toggle }] = useDisclosure();\n *\n * <button onClick={open}>Open Modal</button>\n * <Modal opened={opened} onClose={close}>\n * <button onClick={toggle}>Toggle</button>\n * </Modal>\n * ```\n */\nexport function useDisclosure(\n initialState = false,\n): readonly [boolean, UseDisclosureReturn] {\n const [opened, setOpened] = useState(initialState);\n\n const open = useCallback(() => setOpened(true), []);\n const close = useCallback(() => setOpened(false), []);\n const toggle = useCallback(() => setOpened((prev) => !prev), []);\n\n return [opened, { open, close, toggle }] as const;\n}\n","/**\n * Custom hook for resizable panel functionality.\n * Extracted from GraphViewer to be reusable across components.\n *\n * @module hooks/useResizablePanel\n */\n\nimport { useState, useRef, useCallback, useEffect } from \"react\";\n\ninterface UseResizablePanelOptions {\n defaultWidth?: number;\n minWidth?: number;\n maxWidth?: number;\n /**\n * Which edge the drag handle sits on relative to the panel content.\n *\n * - \"left-edge\" — handle is on the panel's left; dragging right shrinks the\n * panel (default, matches the right-side action panel in\n * GraphViewer where the separator is left of the panel).\n * - \"right-edge\" — handle is on the panel's right; dragging right grows the\n * panel (matches a left-side SidePane where the separator is\n * right of the panel).\n */\n direction?: \"left-edge\" | \"right-edge\";\n}\n\ninterface UseResizablePanelReturn {\n panelWidth: number;\n handlePointerDown: (e: React.PointerEvent) => void;\n handleDoubleClick: () => void;\n separatorProps: {\n role: string;\n \"aria-orientation\": \"vertical\";\n \"aria-valuenow\": number;\n \"aria-valuemin\": number;\n \"aria-valuemax\": number;\n };\n}\n\n/**\n * Hook for managing resizable panel state and interactions.\n * Handles pointer events, constraints, and keyboard shortcuts (Escape to cancel).\n */\nexport function useResizablePanel(\n options: UseResizablePanelOptions = {},\n): UseResizablePanelReturn {\n const {\n defaultWidth = 320,\n minWidth = 260,\n maxWidth = 640,\n direction = \"left-edge\",\n } = options;\n\n const [panelWidth, setPanelWidth] = useState<number>(defaultWidth);\n const resizingRef = useRef(false);\n const startXRef = useRef(0);\n const startWidthRef = useRef(0);\n\n const clamp = useCallback(\n (v: number) => Math.min(Math.max(v, minWidth), maxWidth),\n [minWidth, maxWidth],\n );\n\n const finishResize = useCallback(() => {\n if (!resizingRef.current) return;\n resizingRef.current = false;\n document.body.classList.remove(\"select-none\");\n }, []);\n\n const handlePointerMove = useCallback(\n (e: PointerEvent) => {\n if (!resizingRef.current) return;\n const delta = e.clientX - startXRef.current;\n // left-edge: separator left of panel, drag right shrinks (subtract delta)\n // right-edge: separator right of panel, drag right grows (add delta)\n const newWidth =\n direction === \"right-edge\"\n ? startWidthRef.current + delta\n : startWidthRef.current - delta;\n setPanelWidth(clamp(newWidth));\n },\n [clamp, direction],\n );\n\n const handlePointerDown = useCallback(\n (e: React.PointerEvent) => {\n resizingRef.current = true;\n startXRef.current = e.clientX;\n startWidthRef.current = panelWidth;\n document.body.classList.add(\"select-none\");\n },\n [panelWidth],\n );\n\n const handleDoubleClick = useCallback(\n () => setPanelWidth(defaultWidth),\n [defaultWidth],\n );\n\n useEffect(() => {\n window.addEventListener(\"pointermove\", handlePointerMove);\n window.addEventListener(\"pointerup\", finishResize);\n window.addEventListener(\"pointercancel\", finishResize);\n\n const handleKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\" && resizingRef.current) {\n setPanelWidth(startWidthRef.current);\n finishResize();\n }\n };\n window.addEventListener(\"keydown\", handleKey);\n\n return () => {\n window.removeEventListener(\"pointermove\", handlePointerMove);\n window.removeEventListener(\"pointerup\", finishResize);\n window.removeEventListener(\"pointercancel\", finishResize);\n window.removeEventListener(\"keydown\", handleKey);\n };\n }, [handlePointerMove, finishResize]);\n\n return {\n panelWidth,\n handlePointerDown,\n handleDoubleClick,\n separatorProps: {\n role: \"separator\",\n \"aria-orientation\": \"vertical\",\n \"aria-valuenow\": panelWidth,\n \"aria-valuemin\": minWidth,\n \"aria-valuemax\": maxWidth,\n },\n };\n}\n","/**\n * useFormState Hook\n *\n * Manages form state with automatic dirty tracking and common operations.\n * Tracks changes against original data to determine if form has been modified.\n *\n * @module hooks/useFormState\n */\n\nimport { useState, useCallback, useMemo } from \"react\";\nimport { isEqual } from \"../utils\";\n\n/**\n * Return type for useFormState hook\n */\nexport interface UseFormStateReturn<T> {\n /** Current form data */\n data: T;\n /** Update form data (partial or full) */\n setData: (data: T | ((prev: T) => T)) => void;\n /** Update a single field */\n setField: <K extends keyof T>(field: K, value: T[K]) => void;\n /** Whether form has been modified */\n isDirty: boolean;\n /** Load new data and mark as clean (for switching items) */\n reset: (newData: T) => void;\n /** Revert to original data */\n discard: () => void;\n /** Original data for comparison */\n original: T;\n}\n\n/**\n * Hook for managing form state with dirty tracking\n *\n * Provides state management for forms with automatic change detection.\n * Compares current data against original to determine if form has been modified.\n *\n * @template T - Type of the form data\n * @param initialData - Initial form data\n * @returns Form state and control functions\n *\n * @example\n * ```tsx\n * interface FormData {\n * name: string;\n * email: string;\n * }\n *\n * function MyForm() {\n * const form = useFormState<FormData>({\n * name: '',\n * email: '',\n * });\n *\n * // Update field\n * form.setField('name', 'John');\n *\n * // Check if dirty\n * if (form.isDirty) {\n * console.log('Form has unsaved changes');\n * }\n *\n * // Load new data (e.g., from API)\n * form.reset({ name: 'Jane', email: 'jane@example.com' });\n *\n * // Revert changes\n * form.discard();\n * }\n * ```\n */\nexport function useFormState<T>(initialData: T): UseFormStateReturn<T> {\n const [data, setDataState] = useState<T>(initialData);\n const [original, setOriginal] = useState<T>(initialData);\n\n /**\n * Compute dirty state by comparing current data with original\n */\n const isDirty = useMemo(() => {\n return !isEqual(data, original);\n }, [data, original]);\n\n /**\n * Update form data\n */\n const setData = useCallback((newData: T | ((prev: T) => T)) => {\n setDataState(newData);\n }, []);\n\n /**\n * Update a single field\n */\n const setField = useCallback(<K extends keyof T>(field: K, value: T[K]) => {\n setDataState((prev) => ({\n ...prev,\n [field]: value,\n }));\n }, []);\n\n /**\n * Load new data and mark as clean\n * Use when loading data from API or switching between items\n */\n const reset = useCallback((newData: T) => {\n setDataState(newData);\n setOriginal(newData);\n }, []);\n\n /**\n * Revert to original data\n * Use when user clicks \"Cancel\" or \"Discard Changes\"\n */\n const discard = useCallback(() => {\n setDataState(original);\n }, [original]);\n\n return {\n data,\n setData,\n setField,\n isDirty,\n reset,\n discard,\n original,\n };\n}\n","/**\n * useEntityOptions -- fetch selectable options from any data source.\n *\n * Transport-agnostic: receives an EntityOptionFetcher and returns\n * TanStack Query state. Supports three search modes:\n *\n * eager -- fetches on mount, refetches on debounced search (default)\n * typeahead -- fetches only after the user types >= minChars (debounced)\n * explicit -- fetches only when submit() is called\n *\n * Extractable to @petrarca/sonnet-core/hooks -- no app-specific imports.\n */\n\nimport { useState, useEffect, useCallback } from \"react\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport type { EntityOption, EntityOptionFetcher } from \"../entityOptions\";\n\n/** Search mode for the entity options hook and EntitySelect component. */\nexport type EntitySelectMode = \"eager\" | \"typeahead\" | \"explicit\";\n\nexport interface UseEntityOptionsConfig {\n /** The fetcher function (created by a fetcher factory or provided directly). */\n fetcher: EntityOptionFetcher;\n /** TanStack Query cache key prefix. Must be stable. */\n queryKey: string;\n /**\n * Search mode. Default: \"eager\".\n * eager -- fetch on mount, refetch on debounced typing\n * typeahead -- fetch only after minChars are typed (debounced)\n * explicit -- fetch only when submit() is called\n */\n mode?: EntitySelectMode;\n /** Debounce delay in ms for the search input (default 300). */\n debounceMs?: number;\n /** Minimum characters before typeahead fires (default 1). Ignored in other modes. */\n minChars?: number;\n /** Maximum options to fetch (default 50). */\n limit?: number;\n /** Disable fetching (e.g. when a dependency is not yet satisfied). */\n enabled?: boolean;\n}\n\nexport interface UseEntityOptionsReturn {\n /** Current options from the server. */\n options: EntityOption[];\n /** Whether a fetch is in progress. */\n isLoading: boolean;\n /** Whether the fetch errored. */\n isError: boolean;\n /** The current search string (set by the combobox input). */\n search: string;\n /** Update the search string (drives debounced refetch). */\n setSearch: (value: string) => void;\n /** Trigger a fetch with the current search string. Only meaningful in explicit mode. */\n submit: () => void;\n}\n\n// Determine whether the query should be enabled based on mode and state.\nfunction resolveEnabled(\n enabled: boolean,\n mode: EntitySelectMode,\n activeSearch: string,\n minChars: number,\n committedSearch: string | null,\n): boolean {\n if (!enabled) return false;\n if (mode === \"typeahead\") return activeSearch.length >= minChars;\n if (mode === \"explicit\") return committedSearch !== null;\n return true;\n}\n\nexport function useEntityOptions({\n fetcher,\n queryKey,\n mode = \"eager\",\n debounceMs = 300,\n minChars = 1,\n limit = 50,\n enabled = true,\n}: UseEntityOptionsConfig): UseEntityOptionsReturn {\n const [search, setSearch] = useState(\"\");\n\n // Debounced search drives eager and typeahead modes.\n const [debouncedSearch, setDebouncedSearch] = useState(\"\");\n // Committed search drives explicit mode -- only updated on submit().\n const [committedSearch, setCommittedSearch] = useState<string | null>(null);\n\n useEffect(() => {\n if (mode === \"explicit\") return;\n const timer = setTimeout(() => setDebouncedSearch(search), debounceMs);\n return () => clearTimeout(timer);\n }, [search, debounceMs, mode]);\n\n const submit = useCallback(() => {\n setCommittedSearch(search);\n }, [search]);\n\n const activeSearch =\n mode === \"explicit\" ? (committedSearch ?? \"\") : debouncedSearch;\n\n const queryEnabled = resolveEnabled(\n enabled,\n mode,\n activeSearch,\n minChars,\n committedSearch,\n );\n\n const { data, isLoading, isError } = useQuery({\n queryKey: [queryKey, \"options\", { search: activeSearch, limit }],\n queryFn: () => fetcher({ search: activeSearch || undefined, limit }),\n enabled: queryEnabled,\n staleTime: 5 * 60 * 1000,\n });\n\n const options: EntityOption[] = data ?? [];\n\n return { options, isLoading, isError, search, setSearch, submit };\n}\n"],"mappings":"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","names":["useState","useCallback","useState","useCallback","useState","useCallback","useState","useCallback","useState","useCallback","useState","useEffect","useCallback"]}
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/useClipboard.ts","../../src/hooks/useTreeExpansion.ts","../../src/hooks/useDisclosure.ts","../../src/hooks/useMediaQuery.ts","../../src/hooks/useResizablePanel.ts","../../src/hooks/useFormState.ts","../../src/hooks/useEntityOptions.ts","../../src/hooks/useLazyTree.ts","../../src/hooks/useLazyTreeReveal.ts","../../src/hooks/useTheme.ts"],"sourcesContent":["import { useState, useCallback } from \"react\";\n\nexport interface UseClipboardOptions {\n timeout?: number;\n}\n\nexport interface UseClipboardReturn {\n copy: (text: string) => Promise<void>;\n /**\n * The text on the clipboard; null when it cannot be read (the browser may\n * ask the user first, or refuse) -- `error` then says why.\n */\n paste: () => Promise<string | null>;\n copied: boolean;\n error: Error | null;\n reset: () => void;\n}\n\n/**\n * Custom hook for clipboard operations\n *\n * Replaces Mantine's useClipboard hook with native Clipboard API\n *\n * @param options - Configuration options\n * @param options.timeout - Time in ms before copied state resets (default: 2000)\n * @returns Object with copy and paste functions, copied state, error, and reset function\n *\n * @example\n * ```tsx\n * const { copy, copied } = useClipboard({ timeout: 3000 });\n *\n * <button onClick={() => copy('Hello World')}>\n * {copied ? 'Copied!' : 'Copy'}\n * </button>\n * ```\n */\nexport function useClipboard({\n timeout = 2000,\n}: UseClipboardOptions = {}): UseClipboardReturn {\n const [copied, setCopied] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const copy = useCallback(\n async (text: string) => {\n try {\n await navigator.clipboard.writeText(text);\n setCopied(true);\n setError(null);\n\n // Reset copied state after timeout\n setTimeout(() => setCopied(false), timeout);\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n setCopied(false);\n }\n },\n [timeout],\n );\n\n const paste = useCallback(async (): Promise<string | null> => {\n try {\n const text = await navigator.clipboard.readText();\n setError(null);\n return text;\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n return null;\n }\n }, []);\n\n const reset = useCallback(() => {\n setCopied(false);\n setError(null);\n }, []);\n\n return { copy, paste, copied, error, reset };\n}\n","import { useState, useCallback } from \"react\";\n\nexport interface UseTreeExpansionReturn {\n expandedIds: Set<string>;\n onExpand: (id: string) => void;\n onCollapse: (id: string) => void;\n}\n\n/** Manages expand/collapse state for a TreeView. */\nexport function useTreeExpansion(): UseTreeExpansionReturn {\n const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());\n\n const onExpand = useCallback((id: string) => {\n setExpandedIds((prev) => new Set([...prev, id]));\n }, []);\n\n const onCollapse = useCallback((id: string) => {\n setExpandedIds((prev) => {\n const next = new Set(prev);\n next.delete(id);\n return next;\n });\n }, []);\n\n return { expandedIds, onExpand, onCollapse };\n}\n","import { useState, useCallback } from \"react\";\n\nexport interface UseDisclosureReturn {\n open: () => void;\n close: () => void;\n toggle: () => void;\n}\n\n/**\n * Custom hook for managing open/close state\n *\n * Replaces Mantine's useDisclosure hook with simple useState wrapper\n *\n * @param initialState - Initial open state (default: false)\n * @returns Tuple of [opened, handlers] where handlers contains open, close, toggle functions\n *\n * @example\n * ```tsx\n * const [opened, { open, close, toggle }] = useDisclosure();\n *\n * <button onClick={open}>Open Modal</button>\n * <Modal opened={opened} onClose={close}>\n * <button onClick={toggle}>Toggle</button>\n * </Modal>\n * ```\n */\nexport function useDisclosure(\n initialState = false,\n): readonly [boolean, UseDisclosureReturn] {\n const [opened, setOpened] = useState(initialState);\n\n const open = useCallback(() => setOpened(true), []);\n const close = useCallback(() => setOpened(false), []);\n const toggle = useCallback(() => setOpened((prev) => !prev), []);\n\n return [opened, { open, close, toggle }] as const;\n}\n","import { useState, useEffect } from \"react\";\n\n/**\n * Subscribe to a CSS media query and re-render when it changes.\n *\n * SSR-safe: returns `false` on the server and during the first client render\n * when `window.matchMedia` is unavailable, then syncs on mount.\n *\n * @param query - a media query string, e.g. \"(max-width: 1023px)\"\n * @returns whether the query currently matches\n *\n * @example\n * ```tsx\n * const isTablet = useMediaQuery(\"(max-width: 1023px)\");\n * ```\n */\nexport function useMediaQuery(query: string): boolean {\n const [matches, setMatches] = useState<boolean>(() => {\n if (typeof window === \"undefined\" || !window.matchMedia) return false;\n return window.matchMedia(query).matches;\n });\n\n useEffect(() => {\n if (typeof window === \"undefined\" || !window.matchMedia) return;\n const mql = window.matchMedia(query);\n const onChange = () => setMatches(mql.matches);\n // Sync immediately in case the query changed between render and effect.\n onChange();\n mql.addEventListener(\"change\", onChange);\n return () => mql.removeEventListener(\"change\", onChange);\n }, [query]);\n\n return matches;\n}\n","/**\n * Custom hook for resizable panel functionality.\n * Extracted from GraphViewer to be reusable across components.\n *\n * @module hooks/useResizablePanel\n */\n\nimport { useState, useRef, useCallback, useEffect } from \"react\";\n\ninterface UseResizablePanelOptions {\n defaultWidth?: number;\n minWidth?: number;\n maxWidth?: number;\n /**\n * Which edge the drag handle sits on relative to the panel content.\n *\n * - \"left-edge\" — handle is on the panel's left; dragging right shrinks the\n * panel (default, matches the right-side action panel in\n * GraphViewer where the separator is left of the panel).\n * - \"right-edge\" — handle is on the panel's right; dragging right grows the\n * panel (matches a left-side SidePane where the separator is\n * right of the panel).\n */\n direction?: \"left-edge\" | \"right-edge\";\n}\n\ninterface UseResizablePanelReturn {\n panelWidth: number;\n handlePointerDown: (e: React.PointerEvent) => void;\n handleDoubleClick: () => void;\n separatorProps: {\n role: string;\n \"aria-orientation\": \"vertical\";\n \"aria-valuenow\": number;\n \"aria-valuemin\": number;\n \"aria-valuemax\": number;\n };\n}\n\n/**\n * Hook for managing resizable panel state and interactions.\n * Handles pointer events, constraints, and keyboard shortcuts (Escape to cancel).\n */\nexport function useResizablePanel(\n options: UseResizablePanelOptions = {},\n): UseResizablePanelReturn {\n const {\n defaultWidth = 320,\n minWidth = 260,\n maxWidth = 640,\n direction = \"left-edge\",\n } = options;\n\n const [panelWidth, setPanelWidth] = useState<number>(defaultWidth);\n const resizingRef = useRef(false);\n const startXRef = useRef(0);\n const startWidthRef = useRef(0);\n\n const clamp = useCallback(\n (v: number) => Math.min(Math.max(v, minWidth), maxWidth),\n [minWidth, maxWidth],\n );\n\n const finishResize = useCallback(() => {\n if (!resizingRef.current) return;\n resizingRef.current = false;\n document.body.classList.remove(\"select-none\");\n }, []);\n\n const handlePointerMove = useCallback(\n (e: PointerEvent) => {\n if (!resizingRef.current) return;\n const delta = e.clientX - startXRef.current;\n // left-edge: separator left of panel, drag right shrinks (subtract delta)\n // right-edge: separator right of panel, drag right grows (add delta)\n const newWidth =\n direction === \"right-edge\"\n ? startWidthRef.current + delta\n : startWidthRef.current - delta;\n setPanelWidth(clamp(newWidth));\n },\n [clamp, direction],\n );\n\n const handlePointerDown = useCallback(\n (e: React.PointerEvent) => {\n resizingRef.current = true;\n startXRef.current = e.clientX;\n startWidthRef.current = panelWidth;\n document.body.classList.add(\"select-none\");\n },\n [panelWidth],\n );\n\n const handleDoubleClick = useCallback(\n () => setPanelWidth(defaultWidth),\n [defaultWidth],\n );\n\n useEffect(() => {\n window.addEventListener(\"pointermove\", handlePointerMove);\n window.addEventListener(\"pointerup\", finishResize);\n window.addEventListener(\"pointercancel\", finishResize);\n\n const handleKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\" && resizingRef.current) {\n setPanelWidth(startWidthRef.current);\n finishResize();\n }\n };\n window.addEventListener(\"keydown\", handleKey);\n\n return () => {\n window.removeEventListener(\"pointermove\", handlePointerMove);\n window.removeEventListener(\"pointerup\", finishResize);\n window.removeEventListener(\"pointercancel\", finishResize);\n window.removeEventListener(\"keydown\", handleKey);\n };\n }, [handlePointerMove, finishResize]);\n\n return {\n panelWidth,\n handlePointerDown,\n handleDoubleClick,\n separatorProps: {\n role: \"separator\",\n \"aria-orientation\": \"vertical\",\n \"aria-valuenow\": panelWidth,\n \"aria-valuemin\": minWidth,\n \"aria-valuemax\": maxWidth,\n },\n };\n}\n","/**\n * useFormState Hook\n *\n * Manages form state with automatic dirty tracking and common operations.\n * Tracks changes against original data to determine if form has been modified.\n *\n * @module hooks/useFormState\n */\n\nimport { useState, useCallback, useMemo } from \"react\";\nimport { isEqual } from \"../utils\";\n\n/**\n * Return type for useFormState hook\n */\nexport interface UseFormStateReturn<T> {\n /** Current form data */\n data: T;\n /** Update form data (partial or full) */\n setData: (data: T | ((prev: T) => T)) => void;\n /** Update a single field */\n setField: <K extends keyof T>(field: K, value: T[K]) => void;\n /** Whether form has been modified */\n isDirty: boolean;\n /** Load new data and mark as clean (for switching items) */\n reset: (newData: T) => void;\n /** Revert to original data */\n discard: () => void;\n /** Original data for comparison */\n original: T;\n}\n\n/**\n * Hook for managing form state with dirty tracking\n *\n * Provides state management for forms with automatic change detection.\n * Compares current data against original to determine if form has been modified.\n *\n * @template T - Type of the form data\n * @param initialData - Initial form data\n * @returns Form state and control functions\n *\n * @example\n * ```tsx\n * interface FormData {\n * name: string;\n * email: string;\n * }\n *\n * function MyForm() {\n * const form = useFormState<FormData>({\n * name: '',\n * email: '',\n * });\n *\n * // Update field\n * form.setField('name', 'John');\n *\n * // Check if dirty\n * if (form.isDirty) {\n * console.log('Form has unsaved changes');\n * }\n *\n * // Load new data (e.g., from API)\n * form.reset({ name: 'Jane', email: 'jane@example.com' });\n *\n * // Revert changes\n * form.discard();\n * }\n * ```\n */\nexport function useFormState<T>(initialData: T): UseFormStateReturn<T> {\n const [data, setDataState] = useState<T>(initialData);\n const [original, setOriginal] = useState<T>(initialData);\n\n /**\n * Compute dirty state by comparing current data with original\n */\n const isDirty = useMemo(() => {\n return !isEqual(data, original);\n }, [data, original]);\n\n /**\n * Update form data\n */\n const setData = useCallback((newData: T | ((prev: T) => T)) => {\n setDataState(newData);\n }, []);\n\n /**\n * Update a single field\n */\n const setField = useCallback(<K extends keyof T>(field: K, value: T[K]) => {\n setDataState((prev) => ({\n ...prev,\n [field]: value,\n }));\n }, []);\n\n /**\n * Load new data and mark as clean\n * Use when loading data from API or switching between items\n */\n const reset = useCallback((newData: T) => {\n setDataState(newData);\n setOriginal(newData);\n }, []);\n\n /**\n * Revert to original data\n * Use when user clicks \"Cancel\" or \"Discard Changes\"\n */\n const discard = useCallback(() => {\n setDataState(original);\n }, [original]);\n\n return {\n data,\n setData,\n setField,\n isDirty,\n reset,\n discard,\n original,\n };\n}\n","/**\n * useEntityOptions -- fetch selectable options from any data source.\n *\n * Transport-agnostic: receives an EntityOptionFetcher and returns\n * TanStack Query state. Supports three search modes:\n *\n * eager -- fetches on mount, refetches on debounced search (default)\n * typeahead -- fetches only after the user types >= minChars (debounced)\n * explicit -- fetches only when submit() is called\n *\n * Extractable to @petrarca/sonnet-core/hooks -- no app-specific imports.\n */\n\nimport { useState, useEffect, useCallback } from \"react\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport type { EntityOption, EntityOptionFetcher } from \"../entityOptions\";\n\n/** Search mode for the entity options hook and EntitySelect component. */\nexport type EntitySelectMode = \"eager\" | \"typeahead\" | \"explicit\";\n\nexport interface UseEntityOptionsConfig {\n /** The fetcher function (created by a fetcher factory or provided directly). */\n fetcher: EntityOptionFetcher;\n /** TanStack Query cache key prefix. Must be stable. */\n queryKey: string;\n /**\n * Search mode. Default: \"eager\".\n * eager -- fetch on mount, refetch on debounced typing\n * typeahead -- fetch only after minChars are typed (debounced)\n * explicit -- fetch only when submit() is called\n */\n mode?: EntitySelectMode;\n /** Debounce delay in ms for the search input (default 300). */\n debounceMs?: number;\n /** Minimum characters before typeahead fires (default 1). Ignored in other modes. */\n minChars?: number;\n /** Maximum options to fetch (default 50). */\n limit?: number;\n /** Disable fetching (e.g. when a dependency is not yet satisfied). */\n enabled?: boolean;\n}\n\nexport interface UseEntityOptionsReturn {\n /** Current options from the server. */\n options: EntityOption[];\n /** Whether a fetch is in progress. */\n isLoading: boolean;\n /** Whether the fetch errored. */\n isError: boolean;\n /** The current search string (set by the combobox input). */\n search: string;\n /** Update the search string (drives debounced refetch). */\n setSearch: (value: string) => void;\n /** Trigger a fetch with the current search string. Only meaningful in explicit mode. */\n submit: () => void;\n}\n\n// Determine whether the query should be enabled based on mode and state.\nfunction resolveEnabled(\n enabled: boolean,\n mode: EntitySelectMode,\n activeSearch: string,\n minChars: number,\n committedSearch: string | null,\n): boolean {\n if (!enabled) return false;\n if (mode === \"typeahead\") return activeSearch.length >= minChars;\n if (mode === \"explicit\") return committedSearch !== null;\n return true;\n}\n\nexport function useEntityOptions({\n fetcher,\n queryKey,\n mode = \"eager\",\n debounceMs = 300,\n minChars = 1,\n limit = 50,\n enabled = true,\n}: UseEntityOptionsConfig): UseEntityOptionsReturn {\n const [search, setSearch] = useState(\"\");\n\n // Debounced search drives eager and typeahead modes.\n const [debouncedSearch, setDebouncedSearch] = useState(\"\");\n // Committed search drives explicit mode -- only updated on submit().\n const [committedSearch, setCommittedSearch] = useState<string | null>(null);\n\n useEffect(() => {\n if (mode === \"explicit\") return;\n const timer = setTimeout(() => setDebouncedSearch(search), debounceMs);\n return () => clearTimeout(timer);\n }, [search, debounceMs, mode]);\n\n const submit = useCallback(() => {\n setCommittedSearch(search);\n }, [search]);\n\n const activeSearch =\n mode === \"explicit\" ? (committedSearch ?? \"\") : debouncedSearch;\n\n const queryEnabled = resolveEnabled(\n enabled,\n mode,\n activeSearch,\n minChars,\n committedSearch,\n );\n\n const { data, isLoading, isError } = useQuery({\n queryKey: [queryKey, \"options\", { search: activeSearch, limit }],\n queryFn: () => fetcher({ search: activeSearch || undefined, limit }),\n enabled: queryEnabled,\n staleTime: 5 * 60 * 1000,\n });\n\n const options: EntityOption[] = data ?? [];\n\n return { options, isLoading, isError, search, setSearch, submit };\n}\n","/**\n * useLazyTree -- a tree loaded per level and in pages, on TanStack Query.\n *\n * Transport-agnostic: receives the parameters of a level's page, its query key\n * and a fetcher; returns the nodes for sonnet-ui's TreeView -- each opened node\n * with its level below it, a level closed by a \"more\" row while items remain,\n * an \"empty\" row when it has none, and rows the application adds. Expansion and\n * the pages shown beyond the first stay with the application (controlled), so\n * they can live in its store or URL.\n *\n * The bookkeeping is in `../lazyTree`, usable without TanStack Query.\n */\n\nimport { useQueries, type QueryKey } from \"@tanstack/react-query\";\nimport {\n buildLazyTree,\n lazyTreeLevels,\n lazyTreePages,\n lazyTreeRevealStep,\n type LazyTreeBuildOptions,\n type LazyTreeEntry,\n type LazyTreeLevel,\n type LazyTreeNode,\n type LazyTreePage,\n type LazyTreeRevealStep,\n} from \"../lazyTree\";\n\nexport interface UseLazyTreeConfig<\n TItem,\n TData,\n TParams,\n TPage = LazyTreePage<TItem>,\n TExtra = never,\n> extends Omit<LazyTreeBuildOptions<TItem, TData, TExtra>, \"levelOf\"> {\n /** Pages shown of a level beyond its first, by level id. */\n morePages: Readonly<Record<string, number>>;\n /** The parameters of a level's page. */\n pageParams: (levelId: string, page: number) => TParams;\n /** The query key of a page. */\n queryKey: (params: TParams) => QueryKey;\n fetchPage: (params: TParams) => Promise<TPage>;\n /**\n * A page as fetched to its items and total -- when the API's shape differs;\n * the cache keeps the page as fetched.\n */\n select?: (page: TPage) => LazyTreePage<TItem>;\n}\n\nexport interface UseLazyTreeReturn<TItem, TData, TExtra> {\n /** The nodes for TreeView. */\n nodes: LazyTreeNode<LazyTreeEntry<TData, TExtra>>[];\n /** The top level as fetched so far -- for its loading and error states. */\n root: LazyTreeLevel<TItem>;\n /** A level as fetched so far, by its id. */\n levelOf: (levelId: string) => LazyTreeLevel<TItem>;\n /** The next step of revealing the node at the end of a path (see useLazyTreeReveal). */\n revealStep: (path: readonly string[]) => LazyTreeRevealStep;\n}\n\nexport function useLazyTree<\n TItem,\n TData,\n TParams,\n TPage = LazyTreePage<TItem>,\n TExtra = never,\n>(\n config: UseLazyTreeConfig<TItem, TData, TParams, TPage, TExtra>,\n): UseLazyTreeReturn<TItem, TData, TExtra> {\n const { rootId, expandedIds, morePages, pageParams, queryKey, fetchPage } =\n config;\n const select =\n config.select ?? ((page: TPage) => page as unknown as LazyTreePage<TItem>);\n const requests = lazyTreePages(rootId, expandedIds, morePages, pageParams);\n const states = useQueries({\n queries: requests.map((r) => ({\n queryKey: queryKey(r.params),\n queryFn: () => fetchPage(r.params),\n select,\n })),\n });\n const levelOf = lazyTreeLevels(requests, states);\n return {\n nodes: buildLazyTree({ ...config, levelOf }),\n root: levelOf(rootId),\n levelOf,\n revealStep: (path) =>\n lazyTreeRevealStep(\n rootId,\n path,\n levelOf,\n (item) => config.toNode(item).id,\n ),\n };\n}\n","/**\n * useLazyTreeReveal -- reveal a node of a lazy tree whose levels load in pages,\n * e.g. a search result picked: shows further pages of each level on the node's\n * path until the next node of the path is loaded, then stops.\n *\n * The application opens the path's ancestors itself (its `expandedIds`), as it\n * calls `reveal`; and marks the node (TreeView `activeId`), which scrolls it into\n * view once it is shown.\n */\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport type { LazyTreeRevealStep } from \"../lazyTree\";\n\nexport interface UseLazyTreeRevealConfig {\n /** From useLazyTree. */\n revealStep: (path: readonly string[]) => LazyTreeRevealStep;\n /** Show another page of a level -- what the \"more\" row does. */\n showMore: (levelId: string) => void;\n}\n\nexport interface UseLazyTreeRevealReturn {\n /** Reveal the node at the end of `path` (ids from the top level down to it). */\n reveal: (path: readonly string[]) => void;\n /** Where the current reveal stands; undefined when none is under way. */\n step: LazyTreeRevealStep | undefined;\n}\n\n// The level the next node lies beyond, and how many items it shows.\nfunction moreOf(step: LazyTreeRevealStep | undefined): {\n levelId?: string;\n shown?: number;\n} {\n return step?.state === \"more\"\n ? { levelId: step.levelId, shown: step.shown }\n : {};\n}\n\n// Found, missing or failed: nothing more to do.\nconst SETTLED = new Set<LazyTreeRevealStep[\"state\"]>([\n \"found\",\n \"missing\",\n \"error\",\n]);\n\nexport function useLazyTreeReveal({\n revealStep,\n showMore,\n}: UseLazyTreeRevealConfig): UseLazyTreeRevealReturn {\n const [path, setPath] = useState<readonly string[] | undefined>(undefined);\n const step = path ? revealStep(path) : undefined;\n\n // Another page of the level the next node lies in; keyed on what it shows,\n // so each page that lands without the node asks for the next.\n const { levelId: moreLevel, shown: moreShown } = moreOf(step);\n useEffect(() => {\n if (moreLevel !== undefined) showMore(moreLevel);\n }, [moreLevel, moreShown, showMore]);\n\n // Done -- found, missing or failed: the reveal ends.\n const settled = step !== undefined && SETTLED.has(step.state);\n useEffect(() => {\n if (settled) setPath(undefined);\n }, [settled]);\n\n const reveal = useCallback((next: readonly string[]) => setPath(next), []);\n return { reveal, step };\n}\n","import { useSyncExternalStore } from \"react\";\n\n/** The colour theme: the sonnet tokens' light set, or their dark one. */\nexport type Theme = \"light\" | \"dark\";\n\n// The class on <html> that switches the dark tokens in (the preset's\n// darkMode: [\"class\"]), and where the reader's choice is kept.\nconst DARK_CLASS = \"dark\";\nconst STORAGE_KEY = \"sonnet-theme\";\n\nfunction currentTheme(): Theme {\n return document.documentElement.classList.contains(DARK_CLASS)\n ? \"dark\"\n : \"light\";\n}\n\nfunction storedTheme(): Theme | undefined {\n try {\n const value = localStorage.getItem(STORAGE_KEY);\n return value === \"dark\" || value === \"light\" ? value : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction applyTheme(theme: Theme): void {\n document.documentElement.classList.toggle(DARK_CLASS, theme === \"dark\");\n}\n\n/** Switch the theme, and remember it for the next visit. */\nexport function setTheme(theme: Theme): void {\n applyTheme(theme);\n try {\n localStorage.setItem(STORAGE_KEY, theme);\n } catch {\n // No storage (private mode, say): the choice holds for this page.\n }\n}\n\n/**\n * Apply the theme the reader chose last, else the system's. Call once before\n * the app renders, so the first paint is already in that theme.\n *\n * @example\n * initTheme();\n * ReactDOM.createRoot(root).render(<App />);\n */\nexport function initTheme(): void {\n const prefersDark =\n window.matchMedia?.(\"(prefers-color-scheme: dark)\").matches ?? false;\n applyTheme(storedTheme() ?? (prefersDark ? \"dark\" : \"light\"));\n}\n\n// Changes of the class on <html>, by setTheme or by the app itself.\nfunction subscribe(onChange: () => void): () => void {\n const observer = new MutationObserver(onChange);\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"class\"],\n });\n return () => observer.disconnect();\n}\n\n/**\n * The current theme, re-rendering when it changes, and the way to switch it.\n * Follows the \"dark\" class on <html>, whoever sets it.\n *\n * @example\n * const { theme, setTheme } = useTheme();\n * setTheme(theme === \"dark\" ? \"light\" : \"dark\");\n */\nexport function useTheme(): { theme: Theme; setTheme: (theme: Theme) => void } {\n const theme = useSyncExternalStore(\n subscribe,\n currentTheme,\n (): Theme => \"light\",\n );\n return { theme, setTheme };\n}\n"],"mappings":"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","names":["useState","useCallback","useState","useCallback","useState","useState","useCallback","useEffect","useState","useCallback","useState","useCallback","useState","useEffect","useCallback","useCallback","useEffect","useState"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ClassValue } from 'clsx';
|
|
2
|
+
export { devLog, errorLog, infoLog, warnLog } from './logger/index.js';
|
|
2
3
|
import * as React from 'react';
|
|
4
|
+
export { a as LazyTreeBuildOptions, c as LazyTreeEntry, f as LazyTreeItemNode, d as LazyTreeLevel, b as LazyTreeNode, L as LazyTreePage, g as LazyTreePageRequest, h as LazyTreePageState, e as LazyTreeRevealStep, i as buildLazyTree, j as joinLazyTreePages, l as lazyTreeClosingRows, k as lazyTreeLevels, m as lazyTreePages, n as lazyTreeRevealStep } from './lazyTree-C0oThxCS.js';
|
|
3
5
|
|
|
4
6
|
/** Field name used to embed stable form-internal item IDs on array items. Never sent to the server. */
|
|
5
7
|
declare const FORM_ITEM_ID_FIELD: "_fid";
|
|
@@ -8,16 +10,13 @@ declare const generateId: () => string;
|
|
|
8
10
|
declare function cn(...inputs: ClassValue[]): string;
|
|
9
11
|
declare function isEqual(a: unknown, b: unknown): boolean;
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
declare const
|
|
13
|
-
declare const warnLog: (...a: unknown[]) => void;
|
|
14
|
-
declare const errorLog: (...a: unknown[]) => void;
|
|
15
|
-
|
|
13
|
+
/** What an empty value shows as -- a missing date, an unset field, a table cell without a value. */
|
|
14
|
+
declare const EMPTY_VALUE = "\u2014";
|
|
16
15
|
declare function formatNumber(value: number | string | undefined | null): string;
|
|
17
16
|
declare function formatNumberCompact(value: number | string | undefined | null): string;
|
|
18
|
-
/** Format an ISO 8601 string as a short date (e.g. "Jan 15, 2026"). */
|
|
17
|
+
/** Format an ISO 8601 string as a short date (e.g. "Jan 15, 2026"); one that does not parse as given. */
|
|
19
18
|
declare function formatDate(iso: string | null | undefined): string;
|
|
20
|
-
/** Format an ISO 8601 string as date + time (e.g. "Jan 15, 2026, 09:32 AM"). */
|
|
19
|
+
/** Format an ISO 8601 string as date + time (e.g. "Jan 15, 2026, 09:32 AM"); one that does not parse as given. */
|
|
21
20
|
declare function formatDateTime(iso: string | null | undefined): string;
|
|
22
21
|
/**
|
|
23
22
|
* Format a timezone-aware ISO 8601 string preserving the original offset.
|
|
@@ -329,4 +328,5 @@ interface PageResult<T> {
|
|
|
329
328
|
*/
|
|
330
329
|
declare function toPageResult<T>(rows: T[]): PageResult<T>;
|
|
331
330
|
|
|
332
|
-
export {
|
|
331
|
+
export { EMPTY_VALUE, FORM_ITEM_ID_FIELD, JSON_CSS_VARIABLES, cn, compileDateFormat, copyTextToClipboard, escapeHtml, formatDate, formatDateInput, formatDateTime, formatDateTimeTz, formatIsoDate, formatIsoTime, formatJsonValue, formatNumber, formatNumberCompact, generateId, getFirstDayOfWeek, getLocaleDateFormat, getLocaleDateFormats, getWeekdayLabels, isDateEqual, isEqual, isoDatePrecision, joinDateTime, normalizeError, normalizeTime, parseDate, parseIsoDate, parseTime, pluralize, resolvePointerRanges, snakeCaseToTitleCase, splitDateTime, toPageResult, useComposedRefs };
|
|
332
|
+
export type { CompiledDateFormat, DateFormatOptions, DateParseResult, DatePrecision, LocaleDateFormats, NormalizedError, OffsetRange, PageMeta, PageResult, TimeGranularity, TimeParseResult };
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
buildLazyTree,
|
|
3
|
+
joinLazyTreePages,
|
|
4
|
+
lazyTreeClosingRows,
|
|
5
|
+
lazyTreeLevels,
|
|
6
|
+
lazyTreePages,
|
|
7
|
+
lazyTreeRevealStep
|
|
8
|
+
} from "./chunk-ZM3U3HR2.js";
|
|
7
9
|
import {
|
|
8
10
|
FORM_ITEM_ID_FIELD,
|
|
9
11
|
cn,
|
|
10
12
|
generateId,
|
|
11
13
|
isEqual
|
|
12
14
|
} from "./chunk-XCCOEUVC.js";
|
|
15
|
+
import {
|
|
16
|
+
devLog,
|
|
17
|
+
errorLog,
|
|
18
|
+
infoLog,
|
|
19
|
+
warnLog
|
|
20
|
+
} from "./chunk-LOWDCP73.js";
|
|
13
21
|
|
|
14
22
|
// src/format.ts
|
|
23
|
+
var EMPTY_VALUE = "\u2014";
|
|
15
24
|
var numberFormatter = new Intl.NumberFormat(void 0, {
|
|
16
25
|
maximumFractionDigits: 0
|
|
17
26
|
});
|
|
@@ -30,17 +39,25 @@ function formatNumberCompact(value) {
|
|
|
30
39
|
maximumFractionDigits: 1
|
|
31
40
|
}).format(num);
|
|
32
41
|
}
|
|
42
|
+
function parseTimestamp(iso) {
|
|
43
|
+
const d = new Date(iso);
|
|
44
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
45
|
+
}
|
|
33
46
|
function formatDate(iso) {
|
|
34
|
-
if (!iso) return
|
|
35
|
-
|
|
47
|
+
if (!iso) return EMPTY_VALUE;
|
|
48
|
+
const d = parseTimestamp(iso);
|
|
49
|
+
if (!d) return iso;
|
|
50
|
+
return d.toLocaleDateString(void 0, {
|
|
36
51
|
year: "numeric",
|
|
37
52
|
month: "short",
|
|
38
53
|
day: "numeric"
|
|
39
54
|
});
|
|
40
55
|
}
|
|
41
56
|
function formatDateTime(iso) {
|
|
42
|
-
if (!iso) return
|
|
43
|
-
|
|
57
|
+
if (!iso) return EMPTY_VALUE;
|
|
58
|
+
const d = parseTimestamp(iso);
|
|
59
|
+
if (!d) return iso;
|
|
60
|
+
return d.toLocaleString(void 0, {
|
|
44
61
|
year: "numeric",
|
|
45
62
|
month: "short",
|
|
46
63
|
day: "numeric",
|
|
@@ -49,7 +66,7 @@ function formatDateTime(iso) {
|
|
|
49
66
|
});
|
|
50
67
|
}
|
|
51
68
|
function formatDateTimeTz(iso) {
|
|
52
|
-
if (!iso) return
|
|
69
|
+
if (!iso) return EMPTY_VALUE;
|
|
53
70
|
const match = iso.match(
|
|
54
71
|
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::\d{2}(?:\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/
|
|
55
72
|
);
|
|
@@ -176,9 +193,9 @@ function isValidDate(day, month, year) {
|
|
|
176
193
|
}
|
|
177
194
|
function buildDateResult(parsed, fmt) {
|
|
178
195
|
const today = /* @__PURE__ */ new Date();
|
|
179
|
-
const day = parsed.day ?? today.getDate();
|
|
180
196
|
const month = parsed.month ?? today.getMonth() + 1;
|
|
181
197
|
const year = parsed.year ?? today.getFullYear();
|
|
198
|
+
const day = parsed.day ?? Math.min(today.getDate(), daysInMonth(month, year));
|
|
182
199
|
if (!isValidDate(day, month, year)) {
|
|
183
200
|
return { value: "", date: null, error: "Invalid date" };
|
|
184
201
|
}
|
|
@@ -692,8 +709,10 @@ function toPageResult(rows) {
|
|
|
692
709
|
};
|
|
693
710
|
}
|
|
694
711
|
export {
|
|
712
|
+
EMPTY_VALUE,
|
|
695
713
|
FORM_ITEM_ID_FIELD,
|
|
696
714
|
JSON_CSS_VARIABLES,
|
|
715
|
+
buildLazyTree,
|
|
697
716
|
cn,
|
|
698
717
|
compileDateFormat,
|
|
699
718
|
copyTextToClipboard,
|
|
@@ -719,6 +738,11 @@ export {
|
|
|
719
738
|
isEqual,
|
|
720
739
|
isoDatePrecision,
|
|
721
740
|
joinDateTime,
|
|
741
|
+
joinLazyTreePages,
|
|
742
|
+
lazyTreeClosingRows,
|
|
743
|
+
lazyTreeLevels,
|
|
744
|
+
lazyTreePages,
|
|
745
|
+
lazyTreeRevealStep,
|
|
722
746
|
normalizeError,
|
|
723
747
|
normalizeTime,
|
|
724
748
|
parseDate,
|