@petrarca/sonnet-core 0.8.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 +2 -1
- package/dist/entityOptions/index.d.ts +2 -2
- package/dist/hooks/index.d.ts +100 -3
- package/dist/hooks/index.js +123 -1
- 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 +106 -407
- package/dist/schema/index.js +408 -41
- 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/README.md
CHANGED
|
@@ -10,6 +10,15 @@ attached. Every other `@petrarca/sonnet-*` package depends on it.
|
|
|
10
10
|
of the workspace agrees on, plus `$ref` and conditional resolution, validation
|
|
11
11
|
through Ajv, visibility rules and form diffing.
|
|
12
12
|
|
|
13
|
+
**Core value types.** A quantity, a coding and a codeable concept, each defined
|
|
14
|
+
once and referenced by `$ref`, aligned with FHIR and openEHR, with the bindings
|
|
15
|
+
that tie a field to the lists its codes come from.
|
|
16
|
+
|
|
17
|
+
**A terminology service contract.** `TerminologyService` -- search, look up,
|
|
18
|
+
validate, translate -- with a static and a caching implementation and a
|
|
19
|
+
`TerminologyProvider`, so the coded-value widgets ask the application's own
|
|
20
|
+
terminology server.
|
|
21
|
+
|
|
13
22
|
**A Zod bridge.** Author a schema in Zod, carry the `x-ui-*` presentation hints
|
|
14
23
|
on `.meta()`, and convert once to the JSON Schema the renderers read.
|
|
15
24
|
|
|
@@ -32,7 +41,7 @@ pnpm add @petrarca/sonnet-core
|
|
|
32
41
|
|
|
33
42
|
| Peer | Range | Needed for |
|
|
34
43
|
|---|---|---|
|
|
35
|
-
| `react` | `>=19` | The `./hooks`, `./config` and `./
|
|
44
|
+
| `react` | `>=19` | The `./hooks`, `./config`, `./entityOptions` and `./terminology` subpaths |
|
|
36
45
|
| `@tanstack/react-query` | `>=5` | `useEntityOptions` only; optional otherwise |
|
|
37
46
|
|
|
38
47
|
Ajv, Zod, `nanoid`, `clsx`, `tailwind-merge` and `class-variance-authority`
|
|
@@ -40,8 +49,8 @@ arrive as regular dependencies. There is nothing else to install.
|
|
|
40
49
|
|
|
41
50
|
## Usage
|
|
42
51
|
|
|
43
|
-
The package is
|
|
44
|
-
the
|
|
52
|
+
The package is eight entry points, and the root deliberately re-exports none of
|
|
53
|
+
the others but the logger: a caller who wants `formatDate` should not pay for Ajv, and one
|
|
45
54
|
who wants the filter AST should not pull in React.
|
|
46
55
|
|
|
47
56
|
```ts
|
|
@@ -51,6 +60,8 @@ import { buildListParams } from "@petrarca/sonnet-core/search";
|
|
|
51
60
|
import { useDisclosure } from "@petrarca/sonnet-core/hooks";
|
|
52
61
|
import { FetcherContext } from "@petrarca/sonnet-core/entityOptions";
|
|
53
62
|
import { SonnetConfigProvider } from "@petrarca/sonnet-core/config";
|
|
63
|
+
import { TerminologyProvider, staticTerminology } from "@petrarca/sonnet-core/terminology";
|
|
64
|
+
import { devLog } from "@petrarca/sonnet-core/logger";
|
|
54
65
|
```
|
|
55
66
|
|
|
56
67
|
Which symbol lives in which subpath, and what each one costs to import, is in
|
|
@@ -59,13 +70,15 @@ the usage guides:
|
|
|
59
70
|
[schemas from Zod](docs/usage/zod-schemas.md),
|
|
60
71
|
[search filters](docs/usage/search-filters.md),
|
|
61
72
|
[component config](docs/usage/component-config.md) and
|
|
62
|
-
[hooks and utilities](docs/usage/hooks-and-utilities.md)
|
|
73
|
+
[hooks and utilities](docs/usage/hooks-and-utilities.md); the terminology
|
|
74
|
+
service in the workspace guide to
|
|
75
|
+
[coded values](../../docs/usage/coded-values.md).
|
|
63
76
|
|
|
64
77
|
## Documentation
|
|
65
78
|
|
|
66
79
|
- [Usage guides](docs/usage/index.md) -- task-oriented, for consumers.
|
|
67
80
|
- [Design docs](docs/design/index.md) -- what belongs in this package and why
|
|
68
|
-
it is split into
|
|
81
|
+
it is split into eight subpaths.
|
|
69
82
|
- [Workspace documentation](../../docs/README.md) -- how the packages layer, the
|
|
70
83
|
shared schema extension contract, and the architecture decision records.
|
|
71
84
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// src/lazyTree.ts
|
|
2
|
+
function lazyTreePages(rootId, expandedIds, morePages, pageParams) {
|
|
3
|
+
const levels = [rootId, ...[...expandedIds].filter((id) => id !== rootId)];
|
|
4
|
+
return levels.flatMap(
|
|
5
|
+
(levelId) => Array.from({ length: 1 + (morePages[levelId] ?? 0) }, (_, page) => ({
|
|
6
|
+
levelId,
|
|
7
|
+
page,
|
|
8
|
+
params: pageParams(levelId, page)
|
|
9
|
+
}))
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
function joinLazyTreePages(pages) {
|
|
13
|
+
const loaded = pages.flatMap((p) => p.data ? [p.data] : []);
|
|
14
|
+
return {
|
|
15
|
+
items: loaded.flatMap((p) => p.items),
|
|
16
|
+
total: loaded.at(-1)?.total ?? 0,
|
|
17
|
+
isPending: pages.some((p) => p.isPending),
|
|
18
|
+
isFirstPending: pages[0]?.isPending ?? true,
|
|
19
|
+
isError: pages.some((p) => p.isError)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function lazyTreeLevels(requests, states) {
|
|
23
|
+
const byLevel = /* @__PURE__ */ new Map();
|
|
24
|
+
requests.forEach((r, i) => {
|
|
25
|
+
const list = byLevel.get(r.levelId) ?? [];
|
|
26
|
+
list.push(states[i]);
|
|
27
|
+
byLevel.set(r.levelId, list);
|
|
28
|
+
});
|
|
29
|
+
return (levelId) => joinLazyTreePages(byLevel.get(levelId) ?? []);
|
|
30
|
+
}
|
|
31
|
+
function lazyTreeClosingRows(level, levelId, options) {
|
|
32
|
+
if (level.isFirstPending) return [];
|
|
33
|
+
const row = (id, data) => ({
|
|
34
|
+
id,
|
|
35
|
+
data,
|
|
36
|
+
hasChildren: false
|
|
37
|
+
});
|
|
38
|
+
const rows = [];
|
|
39
|
+
if (level.total === 0 && (options.showEmpty?.(levelId) ?? true))
|
|
40
|
+
rows.push(row(`empty:${levelId}`, { row: "empty", levelId }));
|
|
41
|
+
if (level.total > level.items.length)
|
|
42
|
+
rows.push(
|
|
43
|
+
row(`more:${levelId}:${level.items.length}`, {
|
|
44
|
+
row: "more",
|
|
45
|
+
levelId,
|
|
46
|
+
shown: level.items.length,
|
|
47
|
+
total: level.total,
|
|
48
|
+
loading: level.isPending
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
(options.extraRows?.(level, levelId) ?? []).forEach(
|
|
52
|
+
(extra, i) => rows.push(row(`extra${i}:${levelId}`, { row: "extra", levelId, extra }))
|
|
53
|
+
);
|
|
54
|
+
return rows;
|
|
55
|
+
}
|
|
56
|
+
function buildLazyTree(options) {
|
|
57
|
+
const { expandedIds, levelOf, toNode } = options;
|
|
58
|
+
const nodesOf = (level, levelId) => [
|
|
59
|
+
...level.items.map(build),
|
|
60
|
+
...lazyTreeClosingRows(level, levelId, options)
|
|
61
|
+
];
|
|
62
|
+
const build = (item) => {
|
|
63
|
+
const { id, data, hasChildren } = toNode(item);
|
|
64
|
+
const level = hasChildren && expandedIds.has(id) ? levelOf(id) : void 0;
|
|
65
|
+
return {
|
|
66
|
+
id,
|
|
67
|
+
data: { row: "node", data },
|
|
68
|
+
hasChildren,
|
|
69
|
+
isLoading: level?.isFirstPending,
|
|
70
|
+
isError: level?.isError,
|
|
71
|
+
children: level && !level.isFirstPending ? nodesOf(level, id) : void 0
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
return nodesOf(levelOf(options.rootId), options.rootId);
|
|
75
|
+
}
|
|
76
|
+
function lazyTreeRevealStep(rootId, path, levelOf, idOf) {
|
|
77
|
+
let levelId = rootId;
|
|
78
|
+
for (const id of path) {
|
|
79
|
+
const level = levelOf(levelId);
|
|
80
|
+
if (level.isError) return { state: "error", levelId };
|
|
81
|
+
if (level.isPending || level.isFirstPending) return { state: "loading" };
|
|
82
|
+
if (!level.items.some((item) => idOf(item) === id)) {
|
|
83
|
+
return level.total > level.items.length ? { state: "more", levelId, shown: level.items.length } : { state: "missing", id };
|
|
84
|
+
}
|
|
85
|
+
levelId = id;
|
|
86
|
+
}
|
|
87
|
+
return { state: "found" };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export {
|
|
91
|
+
lazyTreePages,
|
|
92
|
+
joinLazyTreePages,
|
|
93
|
+
lazyTreeLevels,
|
|
94
|
+
lazyTreeClosingRows,
|
|
95
|
+
buildLazyTree,
|
|
96
|
+
lazyTreeRevealStep
|
|
97
|
+
};
|
|
98
|
+
//# sourceMappingURL=chunk-ZM3U3HR2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lazyTree.ts"],"sourcesContent":["/**\n * Lazy tree -- the loading side of a tree too large to fetch at once: a level is\n * fetched when its node opens, a page at a time; a level larger than what is\n * loaded ends in a \"more\" row, an empty one in an \"empty\" row. Pure functions,\n * no fetching: `useLazyTree` (in `./hooks`) connects them to TanStack Query; an\n * application fetching otherwise uses them directly.\n *\n * The nodes built have the shape of sonnet-ui's `TreeNode`, so they go to\n * `TreeView` as they are.\n */\n\n/** A page of a level as fetched: its items in order, and the level's total. */\nexport interface LazyTreePage<TItem> {\n items: TItem[];\n total: number;\n}\n\n/** The state of one page's fetch -- a TanStack Query result is one. */\nexport interface LazyTreePageState<TPage> {\n data?: TPage;\n isPending: boolean;\n isError: boolean;\n}\n\n/** A level as fetched so far: its pages, put together. */\nexport interface LazyTreeLevel<TItem> {\n items: TItem[];\n total: number;\n /** A page is loading (a further one shows as the rows already there). */\n isPending: boolean;\n /** Nothing to show yet: the first page is loading. */\n isFirstPending: boolean;\n isError: boolean;\n}\n\n/** A page to fetch: of which level, which one, and with what parameters. */\nexport interface LazyTreePageRequest<TParams> {\n levelId: string;\n page: number;\n params: TParams;\n}\n\n/**\n * A row of the tree: an item's node, or a row closing a level -- \"more\" while\n * items remain to load (`loading` while the next page is on its way), \"empty\"\n * for a level without any, or one the application adds (\"extra\", e.g. a switch\n * of what the level shows).\n */\nexport type LazyTreeEntry<TData, TExtra = never> =\n | { row: \"node\"; data: TData }\n | {\n row: \"more\";\n levelId: string;\n shown: number;\n total: number;\n loading: boolean;\n }\n | { row: \"empty\"; levelId: string }\n | { row: \"extra\"; levelId: string; extra: TExtra };\n\n/** A node as sonnet-ui's `TreeView` takes it. */\nexport interface LazyTreeNode<TEntry> {\n id: string;\n data: TEntry;\n hasChildren: boolean;\n children?: LazyTreeNode<TEntry>[];\n isLoading?: boolean;\n isError?: boolean;\n}\n\n/** What an item becomes in the tree; its id also names the level below it. */\nexport interface LazyTreeItemNode<TData> {\n id: string;\n data: TData;\n hasChildren: boolean;\n}\n\nexport interface LazyTreeBuildOptions<TItem, TData, TExtra> {\n rootId: string;\n expandedIds: ReadonlySet<string>;\n /** The level of a level's id, as fetched so far. */\n levelOf: (levelId: string) => LazyTreeLevel<TItem>;\n toNode: (item: TItem) => LazyTreeItemNode<TData>;\n /** Rows the application closes a level with, after \"more\". */\n extraRows?: (level: LazyTreeLevel<TItem>, levelId: string) => TExtra[];\n /** Whether an empty level gets an \"empty\" row (default: every one). */\n showEmpty?: (levelId: string) => boolean;\n}\n\n/**\n * The pages to fetch: the top level, then each opened node's -- its first page\n * and as many more as shown (`morePages`, by level id).\n */\nexport function lazyTreePages<TParams>(\n rootId: string,\n expandedIds: Iterable<string>,\n morePages: Readonly<Record<string, number>>,\n pageParams: (levelId: string, page: number) => TParams,\n): LazyTreePageRequest<TParams>[] {\n const levels = [rootId, ...[...expandedIds].filter((id) => id !== rootId)];\n return levels.flatMap((levelId) =>\n Array.from({ length: 1 + (morePages[levelId] ?? 0) }, (_, page) => ({\n levelId,\n page,\n params: pageParams(levelId, page),\n })),\n );\n}\n\n/** A level's pages put together, in order; the total is the latest page's. */\nexport function joinLazyTreePages<TItem>(\n pages: LazyTreePageState<LazyTreePage<TItem>>[],\n): LazyTreeLevel<TItem> {\n const loaded = pages.flatMap((p) => (p.data ? [p.data] : []));\n return {\n items: loaded.flatMap((p) => p.items),\n total: loaded.at(-1)?.total ?? 0,\n isPending: pages.some((p) => p.isPending),\n isFirstPending: pages[0]?.isPending ?? true,\n isError: pages.some((p) => p.isError),\n };\n}\n\n/** Each level's pages put together, by level id, from the requests and their states. */\nexport function lazyTreeLevels<TItem>(\n requests: LazyTreePageRequest<unknown>[],\n states: LazyTreePageState<LazyTreePage<TItem>>[],\n): (levelId: string) => LazyTreeLevel<TItem> {\n const byLevel = new Map<string, LazyTreePageState<LazyTreePage<TItem>>[]>();\n requests.forEach((r, i) => {\n const list = byLevel.get(r.levelId) ?? [];\n list.push(states[i]);\n byLevel.set(r.levelId, list);\n });\n return (levelId) => joinLazyTreePages(byLevel.get(levelId) ?? []);\n}\n\n/**\n * The rows closing a level: \"empty\", \"more\", then the application's own.\n *\n * The \"more\" row stays while the page it asked for loads (`loading`), and its\n * id names how many items it follows: when the page lands, the row is a new\n * one at the end, and a TreeView passes the focus it had to the first item of\n * the page, now in its place.\n */\nexport function lazyTreeClosingRows<TItem, TData, TExtra>(\n level: LazyTreeLevel<TItem>,\n levelId: string,\n options: Pick<\n LazyTreeBuildOptions<TItem, TData, TExtra>,\n \"extraRows\" | \"showEmpty\"\n >,\n): LazyTreeNode<LazyTreeEntry<TData, TExtra>>[] {\n if (level.isFirstPending) return [];\n const row = (\n id: string,\n data: LazyTreeEntry<TData, TExtra>,\n ): LazyTreeNode<LazyTreeEntry<TData, TExtra>> => ({\n id,\n data,\n hasChildren: false,\n });\n const rows: LazyTreeNode<LazyTreeEntry<TData, TExtra>>[] = [];\n if (level.total === 0 && (options.showEmpty?.(levelId) ?? true))\n rows.push(row(`empty:${levelId}`, { row: \"empty\", levelId }));\n if (level.total > level.items.length)\n rows.push(\n row(`more:${levelId}:${level.items.length}`, {\n row: \"more\",\n levelId,\n shown: level.items.length,\n total: level.total,\n loading: level.isPending,\n }),\n );\n (options.extraRows?.(level, levelId) ?? []).forEach((extra, i) =>\n rows.push(row(`extra${i}:${levelId}`, { row: \"extra\", levelId, extra })),\n );\n return rows;\n}\n\n/** The tree: the top level's nodes, each opened one with its level below it. */\nexport function buildLazyTree<TItem, TData, TExtra = never>(\n options: LazyTreeBuildOptions<TItem, TData, TExtra>,\n): LazyTreeNode<LazyTreeEntry<TData, TExtra>>[] {\n const { expandedIds, levelOf, toNode } = options;\n const nodesOf = (\n level: LazyTreeLevel<TItem>,\n levelId: string,\n ): LazyTreeNode<LazyTreeEntry<TData, TExtra>>[] => [\n ...level.items.map(build),\n ...lazyTreeClosingRows<TItem, TData, TExtra>(level, levelId, options),\n ];\n const build = (item: TItem): LazyTreeNode<LazyTreeEntry<TData, TExtra>> => {\n const { id, data, hasChildren } = toNode(item);\n const level = hasChildren && expandedIds.has(id) ? levelOf(id) : undefined;\n return {\n id,\n data: { row: \"node\", data },\n hasChildren,\n isLoading: level?.isFirstPending,\n isError: level?.isError,\n children: level && !level.isFirstPending ? nodesOf(level, id) : undefined,\n };\n };\n return nodesOf(levelOf(options.rootId), options.rootId);\n}\n\n/**\n * What revealing a node still needs -- e.g. a search result picked in a tree\n * whose levels load in pages: \"loading\" while a level on its path loads;\n * \"more\" when the next node of the path lies beyond the pages loaded of its\n * level (show another one: `shown` changes with each, so an effect can key on\n * it); \"found\" when the node is in the tree; \"missing\" when a complete level\n * lacks the next node; \"error\" when a level on the path failed.\n */\nexport type LazyTreeRevealStep =\n | { state: \"loading\" }\n | { state: \"more\"; levelId: string; shown: number }\n | { state: \"found\" }\n | { state: \"missing\"; id: string }\n | { state: \"error\"; levelId: string };\n\n/**\n * The next step of revealing the node at the end of `path` (ids from the top\n * level down to the node). The application opens the path's ancestors\n * (`expandedIds`); this says which level needs another page, until each node\n * of the path is in its level.\n */\nexport function lazyTreeRevealStep<TItem>(\n rootId: string,\n path: readonly string[],\n levelOf: (levelId: string) => LazyTreeLevel<TItem>,\n idOf: (item: TItem) => string,\n): LazyTreeRevealStep {\n let levelId = rootId;\n for (const id of path) {\n const level = levelOf(levelId);\n if (level.isError) return { state: \"error\", levelId };\n // Pending, or not asked for yet (its node opened only now).\n if (level.isPending || level.isFirstPending) return { state: \"loading\" };\n if (!level.items.some((item) => idOf(item) === id)) {\n return level.total > level.items.length\n ? { state: \"more\", levelId, shown: level.items.length }\n : { state: \"missing\", id };\n }\n levelId = id;\n }\n return { state: \"found\" };\n}\n"],"mappings":"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","names":[]}
|
package/dist/config/index.d.ts
CHANGED
|
@@ -148,4 +148,5 @@ declare function useValidationDisplay(override?: SonnetGlobalConfig["validationD
|
|
|
148
148
|
/** Resolve whether Save is available on an invalid form. Defaults to `"reveal"`. */
|
|
149
149
|
declare function useSaveWhenInvalid(override?: SonnetGlobalConfig["saveWhenInvalid"]): NonNullable<SonnetGlobalConfig["saveWhenInvalid"]>;
|
|
150
150
|
|
|
151
|
-
export {
|
|
151
|
+
export { SonnetConfigContext, SonnetConfigProvider, getBrowserLocale, useComponentDefaults, useGlobalSetting, useLocale, useSaveWhenInvalid, useValidationDisplay };
|
|
152
|
+
export type { SaveWhenInvalid, SonnetComponentDefaults, SonnetConfig, SonnetConfigProviderProps, SonnetGlobalConfig, ValidationDisplay };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { c as EntityFetchParams, G as GraphqlFetcherConfig, R as RestFetcherConfig } from '../types-
|
|
1
|
+
import { E as EntityFetcherFactory, a as EntityOption, b as EntityOptionFetcher } from '../types-BiFVnfqr.js';
|
|
2
|
+
export { c as EntityFetchParams, G as GraphqlFetcherConfig, R as RestFetcherConfig } from '../types-BiFVnfqr.js';
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
|
|
5
5
|
declare const FetcherContext: React.Context<EntityFetcherFactory | null>;
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { EntityOptionFetcher, EntityOption } from '../entityOptions/index.js';
|
|
2
|
+
import { QueryKey } from '@tanstack/react-query';
|
|
3
|
+
import { LazyTreePage, LazyTreeBuildOptions, LazyTreeNode, LazyTreeEntry, LazyTreeLevel, LazyTreeRevealStep } from '../index.js';
|
|
2
4
|
|
|
3
5
|
interface UseClipboardOptions {
|
|
4
6
|
timeout?: number;
|
|
5
7
|
}
|
|
6
8
|
interface UseClipboardReturn {
|
|
7
9
|
copy: (text: string) => Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* The text on the clipboard; null when it cannot be read (the browser may
|
|
12
|
+
* ask the user first, or refuse) -- `error` then says why.
|
|
13
|
+
*/
|
|
14
|
+
paste: () => Promise<string | null>;
|
|
8
15
|
copied: boolean;
|
|
9
16
|
error: Error | null;
|
|
10
17
|
reset: () => void;
|
|
@@ -16,7 +23,7 @@ interface UseClipboardReturn {
|
|
|
16
23
|
*
|
|
17
24
|
* @param options - Configuration options
|
|
18
25
|
* @param options.timeout - Time in ms before copied state resets (default: 2000)
|
|
19
|
-
* @returns Object with copy
|
|
26
|
+
* @returns Object with copy and paste functions, copied state, error, and reset function
|
|
20
27
|
*
|
|
21
28
|
* @example
|
|
22
29
|
* ```tsx
|
|
@@ -238,6 +245,69 @@ interface UseEntityOptionsReturn {
|
|
|
238
245
|
}
|
|
239
246
|
declare function useEntityOptions({ fetcher, queryKey, mode, debounceMs, minChars, limit, enabled, }: UseEntityOptionsConfig): UseEntityOptionsReturn;
|
|
240
247
|
|
|
248
|
+
/**
|
|
249
|
+
* useLazyTree -- a tree loaded per level and in pages, on TanStack Query.
|
|
250
|
+
*
|
|
251
|
+
* Transport-agnostic: receives the parameters of a level's page, its query key
|
|
252
|
+
* and a fetcher; returns the nodes for sonnet-ui's TreeView -- each opened node
|
|
253
|
+
* with its level below it, a level closed by a "more" row while items remain,
|
|
254
|
+
* an "empty" row when it has none, and rows the application adds. Expansion and
|
|
255
|
+
* the pages shown beyond the first stay with the application (controlled), so
|
|
256
|
+
* they can live in its store or URL.
|
|
257
|
+
*
|
|
258
|
+
* The bookkeeping is in `../lazyTree`, usable without TanStack Query.
|
|
259
|
+
*/
|
|
260
|
+
|
|
261
|
+
interface UseLazyTreeConfig<TItem, TData, TParams, TPage = LazyTreePage<TItem>, TExtra = never> extends Omit<LazyTreeBuildOptions<TItem, TData, TExtra>, "levelOf"> {
|
|
262
|
+
/** Pages shown of a level beyond its first, by level id. */
|
|
263
|
+
morePages: Readonly<Record<string, number>>;
|
|
264
|
+
/** The parameters of a level's page. */
|
|
265
|
+
pageParams: (levelId: string, page: number) => TParams;
|
|
266
|
+
/** The query key of a page. */
|
|
267
|
+
queryKey: (params: TParams) => QueryKey;
|
|
268
|
+
fetchPage: (params: TParams) => Promise<TPage>;
|
|
269
|
+
/**
|
|
270
|
+
* A page as fetched to its items and total -- when the API's shape differs;
|
|
271
|
+
* the cache keeps the page as fetched.
|
|
272
|
+
*/
|
|
273
|
+
select?: (page: TPage) => LazyTreePage<TItem>;
|
|
274
|
+
}
|
|
275
|
+
interface UseLazyTreeReturn<TItem, TData, TExtra> {
|
|
276
|
+
/** The nodes for TreeView. */
|
|
277
|
+
nodes: LazyTreeNode<LazyTreeEntry<TData, TExtra>>[];
|
|
278
|
+
/** The top level as fetched so far -- for its loading and error states. */
|
|
279
|
+
root: LazyTreeLevel<TItem>;
|
|
280
|
+
/** A level as fetched so far, by its id. */
|
|
281
|
+
levelOf: (levelId: string) => LazyTreeLevel<TItem>;
|
|
282
|
+
/** The next step of revealing the node at the end of a path (see useLazyTreeReveal). */
|
|
283
|
+
revealStep: (path: readonly string[]) => LazyTreeRevealStep;
|
|
284
|
+
}
|
|
285
|
+
declare function useLazyTree<TItem, TData, TParams, TPage = LazyTreePage<TItem>, TExtra = never>(config: UseLazyTreeConfig<TItem, TData, TParams, TPage, TExtra>): UseLazyTreeReturn<TItem, TData, TExtra>;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* useLazyTreeReveal -- reveal a node of a lazy tree whose levels load in pages,
|
|
289
|
+
* e.g. a search result picked: shows further pages of each level on the node's
|
|
290
|
+
* path until the next node of the path is loaded, then stops.
|
|
291
|
+
*
|
|
292
|
+
* The application opens the path's ancestors itself (its `expandedIds`), as it
|
|
293
|
+
* calls `reveal`; and marks the node (TreeView `activeId`), which scrolls it into
|
|
294
|
+
* view once it is shown.
|
|
295
|
+
*/
|
|
296
|
+
|
|
297
|
+
interface UseLazyTreeRevealConfig {
|
|
298
|
+
/** From useLazyTree. */
|
|
299
|
+
revealStep: (path: readonly string[]) => LazyTreeRevealStep;
|
|
300
|
+
/** Show another page of a level -- what the "more" row does. */
|
|
301
|
+
showMore: (levelId: string) => void;
|
|
302
|
+
}
|
|
303
|
+
interface UseLazyTreeRevealReturn {
|
|
304
|
+
/** Reveal the node at the end of `path` (ids from the top level down to it). */
|
|
305
|
+
reveal: (path: readonly string[]) => void;
|
|
306
|
+
/** Where the current reveal stands; undefined when none is under way. */
|
|
307
|
+
step: LazyTreeRevealStep | undefined;
|
|
308
|
+
}
|
|
309
|
+
declare function useLazyTreeReveal({ revealStep, showMore, }: UseLazyTreeRevealConfig): UseLazyTreeRevealReturn;
|
|
310
|
+
|
|
241
311
|
/**
|
|
242
312
|
* Shared types for React Query hooks.
|
|
243
313
|
*
|
|
@@ -250,4 +320,31 @@ interface QueryHookOptions {
|
|
|
250
320
|
enabled?: boolean;
|
|
251
321
|
}
|
|
252
322
|
|
|
253
|
-
|
|
323
|
+
/** The colour theme: the sonnet tokens' light set, or their dark one. */
|
|
324
|
+
type Theme = "light" | "dark";
|
|
325
|
+
/** Switch the theme, and remember it for the next visit. */
|
|
326
|
+
declare function setTheme(theme: Theme): void;
|
|
327
|
+
/**
|
|
328
|
+
* Apply the theme the reader chose last, else the system's. Call once before
|
|
329
|
+
* the app renders, so the first paint is already in that theme.
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* initTheme();
|
|
333
|
+
* ReactDOM.createRoot(root).render(<App />);
|
|
334
|
+
*/
|
|
335
|
+
declare function initTheme(): void;
|
|
336
|
+
/**
|
|
337
|
+
* The current theme, re-rendering when it changes, and the way to switch it.
|
|
338
|
+
* Follows the "dark" class on <html>, whoever sets it.
|
|
339
|
+
*
|
|
340
|
+
* @example
|
|
341
|
+
* const { theme, setTheme } = useTheme();
|
|
342
|
+
* setTheme(theme === "dark" ? "light" : "dark");
|
|
343
|
+
*/
|
|
344
|
+
declare function useTheme(): {
|
|
345
|
+
theme: Theme;
|
|
346
|
+
setTheme: (theme: Theme) => void;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
export { initTheme, setTheme, useClipboard, useDisclosure, useEntityOptions, useFormState, useLazyTree, useLazyTreeReveal, useMediaQuery, useResizablePanel, useTheme, useTreeExpansion };
|
|
350
|
+
export type { EntitySelectMode, QueryHookOptions, Theme, UseClipboardOptions, UseClipboardReturn, UseEntityOptionsConfig, UseEntityOptionsReturn, UseLazyTreeConfig, UseLazyTreeReturn, UseLazyTreeRevealConfig, UseLazyTreeRevealReturn, UseTreeExpansionReturn };
|
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
|
|
@@ -232,13 +248,119 @@ function useEntityOptions({
|
|
|
232
248
|
const options = data ?? [];
|
|
233
249
|
return { options, isLoading, isError, search, setSearch, submit };
|
|
234
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
|
+
}
|
|
235
352
|
export {
|
|
353
|
+
initTheme,
|
|
354
|
+
setTheme,
|
|
236
355
|
useClipboard,
|
|
237
356
|
useDisclosure,
|
|
238
357
|
useEntityOptions,
|
|
239
358
|
useFormState,
|
|
359
|
+
useLazyTree,
|
|
360
|
+
useLazyTreeReveal,
|
|
240
361
|
useMediaQuery,
|
|
241
362
|
useResizablePanel,
|
|
363
|
+
useTheme,
|
|
242
364
|
useTreeExpansion
|
|
243
365
|
};
|
|
244
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/useMediaQuery.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","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"],"mappings":"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","names":["useState","useCallback","useState","useCallback","useState","useState","useCallback","useEffect","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"]}
|