@deftui/react 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/index.d.mts +4 -3
  2. package/dist/index.mjs +2 -3
  3. package/dist/vendor/README.md +2 -1
  4. package/dist/vendor/reui.io/cascader/cascader-async.d.mts +20 -0
  5. package/dist/vendor/reui.io/cascader/cascader-async.mjs +578 -0
  6. package/dist/vendor/reui.io/cascader/cascader-context.d.mts +12 -0
  7. package/dist/vendor/reui.io/cascader/cascader-context.mjs +56 -0
  8. package/dist/vendor/reui.io/cascader/cascader-footer.d.mts +60 -0
  9. package/dist/vendor/reui.io/cascader/cascader-footer.mjs +445 -0
  10. package/dist/vendor/reui.io/cascader/cascader-i18n.mjs +64 -0
  11. package/dist/vendor/reui.io/cascader/cascader-item.d.mts +92 -0
  12. package/dist/vendor/reui.io/cascader/cascader-item.mjs +533 -0
  13. package/dist/vendor/reui.io/cascader/cascader-lib.d.mts +3 -0
  14. package/dist/vendor/reui.io/cascader/cascader-lib.mjs +688 -0
  15. package/dist/vendor/reui.io/cascader/cascader-nav.d.mts +56 -0
  16. package/dist/vendor/reui.io/cascader/cascader-nav.mjs +310 -0
  17. package/dist/vendor/reui.io/cascader/cascader-types.d.mts +140 -0
  18. package/dist/vendor/reui.io/cascader/cascader-virtual.d.mts +18 -0
  19. package/dist/vendor/reui.io/cascader/cascader-virtual.mjs +159 -0
  20. package/dist/vendor/reui.io/cascader/cascader.d.mts +299 -0
  21. package/dist/vendor/reui.io/cascader/cascader.mjs +1470 -0
  22. package/dist/vendor/reui.io/timeline.d.mts +21 -0
  23. package/dist/vendor/reui.io/timeline.mjs +127 -0
  24. package/dist/widgets/export.d.mts +28 -0
  25. package/dist/widgets/index.d.mts +55 -11
  26. package/dist/widgets/index.mjs +45 -2
  27. package/package.json +2 -2
  28. package/dist/vendor/tailored/file-tree.d.mts +0 -21
  29. package/dist/vendor/tailored/file-tree.mjs +0 -146
package/dist/index.d.mts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { useCopyToClipboard } from "./vendor/coss/hooks/use-copy-to-clipboard.mjs";
2
2
  import { useIsMobile, useMediaQuery } from "./vendor/coss/hooks/use-media-query.mjs";
3
+ import { CascaderChangeDetails, CascaderLabels, CascaderMode, CascaderNode } from "./vendor/reui.io/cascader/cascader-types.mjs";
4
+ import { CascaderProps, CascaderSingleProps } from "./vendor/reui.io/cascader/cascader.mjs";
3
5
  import { Separator } from "./vendor/shadcn/separator.mjs";
4
6
  import { Label } from "./vendor/shadcn/label.mjs";
5
7
  import { Button } from "./vendor/shadcn/button.mjs";
@@ -23,10 +25,9 @@ import { Form } from "./vendor/coss/form.mjs";
23
25
  import { ScrollArea } from "./vendor/coss/scroll-area.mjs";
24
26
  import { IconSwap } from "./vendor/tailored/icon-swap.mjs";
25
27
  import { JsonViewer } from "./vendor/tailored/json-viewer.mjs";
26
- import { FileTree, FileTreeNode } from "./vendor/tailored/file-tree.mjs";
27
- import { AlertDialog, Breadcrumb, ButtonGroup, Card, Combobox, Dialog, Drawer, Dropdown, Empty, Field, Frame, InputGroup, Kbd, Popover, Progress, Radio, Select, Table, Tabs, Tooltip } from "./widgets/index.mjs";
28
+ import { AlertDialog, Breadcrumb, ButtonGroup, Card, Cascader, Combobox, Dialog, Drawer, Dropdown, Empty, Field, Frame, InputGroup, Kbd, Popover, Progress, Radio, Select, Table, Tabs, Timeline, Tooltip } from "./widgets/index.mjs";
28
29
  import { cn } from "cn";
29
30
  import { cva } from "class-variance-authority";
30
31
  import { mergeProps } from "@base-ui/react/merge-props";
31
32
  export * from "react-dropzone";
32
- export { AlertDialog, Badge, Breadcrumb, Button, ButtonGroup, Calendar, Card, Carousel, Checkbox, Combobox, DatePicker, type DateRange, DateRangePicker, Dialog, Drawer, Dropdown, Dropzone, Empty, Field, FileTree, type FileTreeNode, Form, Frame, IconSwap, Image, ImageProps, Input, InputGroup, JsonViewer, Kbd, Label, Lightbox, type LightboxActions, type LightboxBackdrop, type LightboxCaptions, type LightboxChangeReason, type LightboxClose, type LightboxContent, type LightboxControl, type LightboxControlContext, type LightboxCounter, type LightboxDownload, type LightboxImageChangeEventDetails, type LightboxImageItem, type LightboxItem, type LightboxMore, type LightboxNext, type LightboxOpenEventDetails, type LightboxPrevious, type LightboxProps, type LightboxSlide, type LightboxSlides, type LightboxThumbnail, type LightboxThumbnails, type LightboxToolbar, type LightboxTrigger, type LightboxTriggerProps, type LightboxVideoItem, type LightboxViewport, type LightboxZoom, type LightboxZoomButton, Pagination, Popover, Progress, Radio, ScrollArea, Select, Separator, Sidebar, Skeleton, Spinner, Switch, Table, Tabs, Textarea, TimePicker, Toaster as ToastProvider, Tooltip, cn, cva, formatBytes, mergeProps, toast, useCopyToClipboard, useDropzone, useIsMobile, type useLightbox, useMediaQuery, useSidebar };
33
+ export { AlertDialog, Badge, Breadcrumb, Button, ButtonGroup, Calendar, Card, Carousel, Cascader, type CascaderChangeDetails, type CascaderLabels, type CascaderMode, type CascaderNode, type CascaderProps, type CascaderSingleProps, Checkbox, Combobox, DatePicker, type DateRange, DateRangePicker, Dialog, Drawer, Dropdown, Dropzone, Empty, Field, Form, Frame, IconSwap, Image, ImageProps, Input, InputGroup, JsonViewer, Kbd, Label, Lightbox, type LightboxActions, type LightboxBackdrop, type LightboxCaptions, type LightboxChangeReason, type LightboxClose, type LightboxContent, type LightboxControl, type LightboxControlContext, type LightboxCounter, type LightboxDownload, type LightboxImageChangeEventDetails, type LightboxImageItem, type LightboxItem, type LightboxMore, type LightboxNext, type LightboxOpenEventDetails, type LightboxPrevious, type LightboxProps, type LightboxSlide, type LightboxSlides, type LightboxThumbnail, type LightboxThumbnails, type LightboxToolbar, type LightboxTrigger, type LightboxTriggerProps, type LightboxVideoItem, type LightboxViewport, type LightboxZoom, type LightboxZoomButton, Pagination, Popover, Progress, Radio, ScrollArea, Select, Separator, Sidebar, Skeleton, Spinner, Switch, Table, Tabs, Textarea, TimePicker, Timeline, Toaster as ToastProvider, Tooltip, cn, cva, formatBytes, mergeProps, toast, useCopyToClipboard, useDropzone, useIsMobile, type useLightbox, useMediaQuery, useSidebar };
package/dist/index.mjs CHANGED
@@ -22,9 +22,8 @@ import { Checkbox } from "./vendor/shadcn/checkbox.mjs";
22
22
  import { Form } from "./vendor/coss/form.mjs";
23
23
  import { IconSwap } from "./vendor/tailored/icon-swap.mjs";
24
24
  import { JsonViewer } from "./vendor/tailored/json-viewer.mjs";
25
- import { FileTree } from "./vendor/tailored/file-tree.mjs";
26
- import { AlertDialog, Breadcrumb, ButtonGroup, Card, Combobox, Dialog, Drawer, Dropdown, Empty, Field, Frame, InputGroup, Kbd, Popover, Progress, Radio, Select, Table, Tabs, Tooltip } from "./widgets/index.mjs";
25
+ import { AlertDialog, Breadcrumb, ButtonGroup, Card, Cascader, Combobox, Dialog, Drawer, Dropdown, Empty, Field, Frame, InputGroup, Kbd, Popover, Progress, Radio, Select, Table, Tabs, Timeline, Tooltip } from "./widgets/index.mjs";
27
26
  import { cn } from "cn";
28
27
  import { cva } from "class-variance-authority";
29
28
  import { mergeProps } from "@base-ui/react/merge-props";
30
- export { AlertDialog, Badge, Breadcrumb, Button, ButtonGroup, Calendar, Card, Carousel, Checkbox, Combobox, DatePicker, DateRangePicker, Dialog, Drawer, Dropdown, Dropzone, Empty, Field, FileTree, Form, Frame, IconSwap, Image, Input, InputGroup, JsonViewer, Kbd, Label, Lightbox, Pagination, Popover, Progress, Radio, ScrollArea, Select, Separator, Sidebar, Skeleton, Spinner, Switch, Table, Tabs, Textarea, TimePicker, Toaster as ToastProvider, Tooltip, cn, cva, formatBytes, mergeProps, toast, useCopyToClipboard, useDropzone, useIsMobile, useMediaQuery, useSidebar };
29
+ export { AlertDialog, Badge, Breadcrumb, Button, ButtonGroup, Calendar, Card, Carousel, Cascader, Checkbox, Combobox, DatePicker, DateRangePicker, Dialog, Drawer, Dropdown, Dropzone, Empty, Field, Form, Frame, IconSwap, Image, Input, InputGroup, JsonViewer, Kbd, Label, Lightbox, Pagination, Popover, Progress, Radio, ScrollArea, Select, Separator, Sidebar, Skeleton, Spinner, Switch, Table, Tabs, Textarea, TimePicker, Timeline, Toaster as ToastProvider, Tooltip, cn, cva, formatBytes, mergeProps, toast, useCopyToClipboard, useDropzone, useIsMobile, useMediaQuery, useSidebar };
@@ -22,6 +22,8 @@ shadcn 组件使用 `components.json` 中的 `base-vega` 风格和 Lucide 图标
22
22
  | `@coss/table` | [coss/table.tsx](coss/table.tsx) | [Table](https://coss.com/ui/docs/components/table) | 默认变体的 `TableFooter` 移除底色,与 `card` 变体一样保持透明 |
23
23
  | `@coss/use-copy-to-clipboard` | [coss/hooks/use-copy-to-clipboard.ts](coss/hooks/use-copy-to-clipboard.ts) | [useCopyToClipboard](https://coss.com/ui/docs/hooks/use-copy-to-clipboard) | — |
24
24
  | `@coss/use-media-query` | [coss/hooks/use-media-query.ts](coss/hooks/use-media-query.ts) | [useMediaQuery](https://coss.com/ui/docs/hooks/use-media-query) | 断点:`md` 设为 768px,与 Tailwind 默认值一致 |
25
+ | `@reui/cascader` | [reui.io/cascader/cascader.tsx](reui.io/cascader/cascader.tsx) | [Cascader](https://reui.io/docs/components/cascader) | 重映射 `scroll-area` 与 `spinner` 的导入 |
26
+ | `@reui/timeline` | [reui.io/timeline.tsx](reui.io/timeline.tsx) | [Timeline](https://reui.io/docs/components/timeline) | — |
25
27
  | `badge` | [shadcn/badge.tsx](shadcn/badge.tsx) | [Badge](https://ui.shadcn.com/docs/components/base/badge) | 提供 `info`、`success`、`warning` 语义变体 |
26
28
  | `breadcrumb` | [shadcn/breadcrumb.tsx](shadcn/breadcrumb.tsx) | [Breadcrumb](https://ui.shadcn.com/docs/components/base/breadcrumb) | 未显式指定 `BreadcrumbList` 则自动包裹 |
27
29
  | `button` | [shadcn/button.tsx](shadcn/button.tsx) | [Button](https://ui.shadcn.com/docs/components/base/button) | `pill` 提供全圆角;`isPending` 提供加载态;提供额外 `destructive` 系列样式变体 |
@@ -64,5 +66,4 @@ shadcn 组件使用 `components.json` 中的 `base-vega` 风格和 Lucide 图标
64
66
  | `https://ui.iandefined.com/r/icon-swap.json` | [tailored/icon-swap.tsx](tailored/icon-swap.tsx) | [Icon Swap](https://ui.iandefined.com/docs/utilities/icon-swap/) | 进出场动画减弱模糊为 2px,缩短为 0.2s 时长 |
65
67
  | `https://ui.iandefined.com/r/lightbox.json` | [tailored/lightbox.tsx](tailored/lightbox.tsx) | [Lightbox](https://ui.iandefined.com/docs/components/lightbox/) | 适配本地组件和尺寸;修正嵌套弹层层级与点击隔离;补充加载失败反馈 |
66
68
  | `https://ui.iandefined.com/r/scroll-area.json` | [tailored/lightbox.tsx](tailored/lightbox.tsx) | [Scroll Area](https://ui.iandefined.com/docs/components/scroll-area/) | 作为 Lightbox 的私有依赖内联 |
67
- | `Tinkerers-Labs/mischief-ui/file-tree` | [tailored/file-tree.tsx](tailored/file-tree.tsx) | [File Tree](https://ui.tinkererslabs.com/docs/components/file-tree/) | — |
68
69
  | `Tinkerers-Labs/mischief-ui/json-viewer` | [tailored/json-viewer.tsx](tailored/json-viewer.tsx) | [JSON Viewer](https://ui.tinkererslabs.com/docs/components/json-viewer/) | — |
@@ -0,0 +1,20 @@
1
+ import { CascaderLoadContext, CascaderLoadResult, CascaderNode, CascaderSearchContext } from "./cascader-types.mjs";
2
+ /**
3
+ * Async data for the cascader. Loaded pages live in their own store and are
4
+ * MERGED onto the index built from `items`, never folded into that build:
5
+ * `items` changes identity on any parent re-render, and folding would discard
6
+ * every level the user drilled into. Map membership is the load discriminator:
7
+ * no `states` entry means never fetched, while an entry with no `loading`, no
8
+ * `error` and no children means empty for real.
9
+ */
10
+ /**
11
+ * Fetches one level. `node` is `null` for the root level, and only
12
+ * `{ value, label: value }`, no `data`, for a level on screen whose node has
13
+ * not loaded: a deep link before the level above loads, or a page-loaded
14
+ * level after a `loadKey` change.
15
+ */
16
+ export type CascaderGetChildren<T = unknown> = (node: CascaderNode<T> | null, context: CascaderLoadContext) => CascaderNode<T>[] | CascaderLoadResult<T> | Promise<CascaderNode<T>[] | CascaderLoadResult<T>>;
17
+ /** Server-side search, replacing the local index scan while the query is set. */
18
+ export type CascaderOnSearch<T = unknown> = (query: string, context: CascaderSearchContext) => CascaderNode<T>[] | CascaderLoadResult<T> | Promise<CascaderNode<T>[] | CascaderLoadResult<T>>;
19
+ /** Resolves a selected value to its ancestor chain, root first, node last. */
20
+ export type CascaderResolveValue<T = unknown> = (value: string, context: CascaderLoadContext) => CascaderNode<T>[] | Promise<CascaderNode<T>[]>;
@@ -0,0 +1,578 @@
1
+ "use client";
2
+ import { useCascaderState } from "./cascader-context.mjs";
3
+ import { CASCADER_ROOT_KEY, isCascaderMoreNode } from "./cascader-lib.mjs";
4
+ import * as React$1 from "react";
5
+ // src/vendor/reui.io/cascader/cascader-async.tsx
6
+ /**
7
+ * Highlight dwell before `prefetch` fetches: long enough that holding ArrowDown
8
+ * does not fire a request per row, short enough to beat the ArrowRight press.
9
+ */
10
+ const PREFETCH_DELAY = 150;
11
+ /** Request keys for the two non-level requests. Never collide with a value. */
12
+ const SEARCH_KEY = "\0search";
13
+ const RESOLVE_PREFIX = "\0resolve:";
14
+ const NO_STATE = {
15
+ loading: false,
16
+ error: false,
17
+ hasMore: false
18
+ };
19
+ /** Stable empty result, so an idle search never churns the state context. */
20
+ const NO_RESULTS = [];
21
+ function createStore() {
22
+ return {
23
+ pages: /* @__PURE__ */ new Map(),
24
+ states: /* @__PURE__ */ new Map(),
25
+ detached: /* @__PURE__ */ new Map(),
26
+ links: /* @__PURE__ */ new Map()
27
+ };
28
+ }
29
+ function sameLoadState(a, b) {
30
+ return a.loading === b.loading && a.error === b.error && a.hasMore === b.hasMore && a.cursor === b.cursor;
31
+ }
32
+ /**
33
+ * Copy-on-write, and a NO-OP when nothing changed: the merged index is memoised
34
+ * on store identity, so a fresh object for an unchanged state would rebuild it.
35
+ */
36
+ function withLoadState(store, key, update) {
37
+ const current = store.states.get(key) ?? NO_STATE;
38
+ const next = update(current);
39
+ if (store.states.has(key) && sameLoadState(current, next)) return store;
40
+ const states = new Map(store.states);
41
+ states.set(key, next);
42
+ return {
43
+ ...store,
44
+ states
45
+ };
46
+ }
47
+ function withPage(store, key, items, options) {
48
+ const previous = options.append ? store.pages.get(key) ?? [] : [];
49
+ const seen = new Set(previous.map((node) => node.value));
50
+ const merged = previous.slice();
51
+ for (const item of items) {
52
+ if (seen.has(item.value)) continue;
53
+ seen.add(item.value);
54
+ merged.push(item);
55
+ }
56
+ const pages = new Map(store.pages);
57
+ pages.set(key, merged);
58
+ const states = new Map(store.states);
59
+ states.set(key, {
60
+ loading: false,
61
+ error: false,
62
+ hasMore: options.hasMore,
63
+ cursor: options.cursor
64
+ });
65
+ return {
66
+ ...store,
67
+ pages,
68
+ states
69
+ };
70
+ }
71
+ function withDetached(store, items) {
72
+ let detached = null;
73
+ for (const item of items) {
74
+ if (store.detached.get(item.value) === item) continue;
75
+ detached = detached ?? new Map(store.detached);
76
+ detached.set(item.value, item);
77
+ }
78
+ if (!detached) return store;
79
+ return {
80
+ ...store,
81
+ detached
82
+ };
83
+ }
84
+ /**
85
+ * Records a resolved ancestor chain, root first, as `detached` nodes plus
86
+ * `links`, never as rows in `pages`: a stub there made an unloaded level read
87
+ * as loaded. Writes no `states`, so every level on it still fetches for real.
88
+ */
89
+ function withChain(store, chain) {
90
+ if (chain.length === 0) return store;
91
+ const detached = new Map(store.detached);
92
+ const links = new Map(store.links);
93
+ let parentKey = CASCADER_ROOT_KEY;
94
+ for (const node of chain) {
95
+ detached.set(node.value, node);
96
+ links.set(node.value, parentKey);
97
+ parentKey = node.value;
98
+ }
99
+ return {
100
+ ...store,
101
+ detached,
102
+ links
103
+ };
104
+ }
105
+ /**
106
+ * The loader. A SIBLING of the `buildCascaderIndex` memo, never inside it: the
107
+ * build stays pure in `items`, the merge pure in that build plus this store.
108
+ */
109
+ function useCascaderLoader({ base, getChildren, onSearch, resolveValue, searchDebounce = 250, loadKey, prefetch = false, onLoadError, enabled, query, levels, path, values }) {
110
+ const [store, setStore] = React$1.useState(createStore);
111
+ const [search, setSearch] = React$1.useState(null);
112
+ /**
113
+ * Latest callbacks, WRITTEN IN AN EFFECT: `getChildren` is inline in most
114
+ * consumers, so closing over it would refire every in-flight request per
115
+ * re-render. The ref is what keeps the request machinery `[]`-dep. Declared
116
+ * FIRST, since effects run in declaration order, so the level effect below
117
+ * already sees the current commit.
118
+ */
119
+ const latest = React$1.useRef({
120
+ base,
121
+ store,
122
+ getChildren,
123
+ onSearch,
124
+ resolveValue,
125
+ onLoadError,
126
+ prefetch,
127
+ path,
128
+ query
129
+ });
130
+ React$1.useEffect(() => {
131
+ latest.current = {
132
+ base,
133
+ store,
134
+ getChildren,
135
+ onSearch,
136
+ resolveValue,
137
+ onLoadError,
138
+ prefetch,
139
+ path,
140
+ query
141
+ };
142
+ });
143
+ /** One AbortController PER KEY: columns mode runs several levels at once. */
144
+ const controllers = React$1.useRef(/* @__PURE__ */ new Map());
145
+ /** Monotonic per key. The stale guard for out-of-order responses. */
146
+ const requestIds = React$1.useRef(/* @__PURE__ */ new Map());
147
+ /** In-flight `(level, cursor)` signatures, so a duplicate ask is free. */
148
+ const inflight = React$1.useRef(/* @__PURE__ */ new Map());
149
+ /**
150
+ * The `(child count, cursor)` signature at the last paging fire, per level.
151
+ * Guards what `hasMore` cannot: a page of zero new items while the server
152
+ * still says `hasMore`. Cursor is IN the signature because an all-duplicates
153
+ * page advances it while the count stands still - real progress, which a
154
+ * count-only latch would brick forever.
155
+ */
156
+ const moreLatch = React$1.useRef(/* @__PURE__ */ new Map());
157
+ /** Values `resolveValue` has already been asked about, so it asks once. */
158
+ const attempted = React$1.useRef(/* @__PURE__ */ new Set());
159
+ /**
160
+ * Values whose resolve failed. They stay `attempted` until the next open
161
+ * rather than the next values pass: with inline `items` every parent render
162
+ * is one, and an `onLoadError` that set parent state looped at network speed.
163
+ */
164
+ const failed = React$1.useRef(/* @__PURE__ */ new Set());
165
+ /** Every node the loader has seen, so a level key can name its own node. */
166
+ const known = React$1.useRef(/* @__PURE__ */ new Map());
167
+ const timers = React$1.useRef({ prefetch: null });
168
+ /** Bumped by a `loadKey` change, so responses from before it are dropped. */
169
+ const epoch = React$1.useRef(0);
170
+ const remember = React$1.useCallback((nodes) => {
171
+ const map = known.current;
172
+ const walk = (list) => {
173
+ for (const node of list) {
174
+ map.set(node.value, node);
175
+ if (node.children?.length) walk(node.children);
176
+ }
177
+ };
178
+ walk(nodes);
179
+ }, []);
180
+ const abortKey = React$1.useCallback((key) => {
181
+ const controller = controllers.current.get(key);
182
+ if (!controller) return;
183
+ controllers.current.delete(key);
184
+ inflight.current.delete(key);
185
+ controller.abort();
186
+ }, []);
187
+ const runLoad = React$1.useCallback((key, reason, cursor, append) => {
188
+ const { getChildren: loader, base: currentBase } = latest.current;
189
+ if (!loader) return;
190
+ const signature = `${append ? "1" : "0"}:${cursor ?? ""}`;
191
+ if (inflight.current.get(key) === signature) return;
192
+ abortKey(key);
193
+ const controller = new AbortController();
194
+ controllers.current.set(key, controller);
195
+ inflight.current.set(key, signature);
196
+ const requestId = (requestIds.current.get(key) ?? 0) + 1;
197
+ requestIds.current.set(key, requestId);
198
+ const startEpoch = epoch.current;
199
+ const node = key === "\0root" ? null : currentBase.byValue.get(key) ?? known.current.get(key) ?? {
200
+ value: key,
201
+ label: key
202
+ };
203
+ setStore((prev) => withLoadState(prev, key, (state) => ({
204
+ ...state,
205
+ loading: true,
206
+ error: false
207
+ })));
208
+ const settle = () => {
209
+ if (inflight.current.get(key) === signature) inflight.current.delete(key);
210
+ if (controllers.current.get(key) === controller) controllers.current.delete(key);
211
+ };
212
+ const stale = () => controller.signal.aborted || startEpoch !== epoch.current || requestId !== requestIds.current.get(key);
213
+ Promise.resolve().then(() => loader(node, {
214
+ signal: controller.signal,
215
+ cursor,
216
+ reason
217
+ })).then((result) => {
218
+ settle();
219
+ if (stale()) return;
220
+ const items = Array.isArray(result) ? result : result.items;
221
+ const nextCursor = Array.isArray(result) ? void 0 : result.nextCursor;
222
+ const hasMore = Array.isArray(result) ? false : result.hasMore ?? nextCursor != null;
223
+ if (!append) moreLatch.current.delete(key);
224
+ remember(items);
225
+ setStore((prev) => withPage(prev, key, items, {
226
+ append,
227
+ hasMore,
228
+ cursor: nextCursor
229
+ }));
230
+ }).catch((error) => {
231
+ settle();
232
+ if (stale()) return;
233
+ setStore((prev) => withLoadState(prev, key, (state) => ({
234
+ ...state,
235
+ loading: false,
236
+ error: true
237
+ })));
238
+ latest.current.onLoadError?.(error, {
239
+ parent: key === "\0root" ? null : key,
240
+ reason
241
+ });
242
+ });
243
+ }, [abortKey, remember]);
244
+ const ensureLevel = React$1.useCallback((key, reason) => {
245
+ const { getChildren: loader, store: current, base: index } = latest.current;
246
+ if (!loader) return;
247
+ if (current.states.has(key)) return;
248
+ if (index.childrenOf.has(key)) return;
249
+ runLoad(key, reason, void 0, false);
250
+ }, [runLoad]);
251
+ const loadMore = React$1.useCallback((key) => {
252
+ const { getChildren: loader, store: current } = latest.current;
253
+ if (!loader) return;
254
+ const state = current.states.get(key);
255
+ if (!state || state.loading || !state.hasMore) return;
256
+ const signature = `${current.pages.get(key)?.length ?? 0}:${state.cursor ?? ""}`;
257
+ if (moreLatch.current.get(key) === signature) return;
258
+ moreLatch.current.set(key, signature);
259
+ runLoad(key, "more", state.cursor, true);
260
+ }, [runLoad]);
261
+ const retryLevel = React$1.useCallback((key) => {
262
+ const { getChildren: loader, store: current } = latest.current;
263
+ if (!loader) return;
264
+ const state = current.states.get(key);
265
+ if (!state?.error) return;
266
+ const loaded = current.pages.get(key)?.length ?? 0;
267
+ moreLatch.current.delete(key);
268
+ runLoad(key, "retry", loaded > 0 ? state.cursor : void 0, loaded > 0);
269
+ }, [runLoad]);
270
+ /**
271
+ * `detached` and `links` are deliberately untouched: chains and search hits
272
+ * belong to no level, and the trigger needs their path while the new page is
273
+ * in flight.
274
+ */
275
+ const invalidateLevel = React$1.useCallback((value) => {
276
+ const key = value ?? "\0root";
277
+ abortKey(key);
278
+ requestIds.current.set(key, (requestIds.current.get(key) ?? 0) + 1);
279
+ moreLatch.current.delete(key);
280
+ setStore((prev) => {
281
+ if (!prev.states.has(key) && !prev.pages.has(key)) return prev;
282
+ const states = new Map(prev.states);
283
+ states.delete(key);
284
+ const pages = new Map(prev.pages);
285
+ pages.delete(key);
286
+ return {
287
+ ...prev,
288
+ pages,
289
+ states
290
+ };
291
+ });
292
+ }, [abortKey]);
293
+ const prefetchNode = React$1.useCallback((node) => {
294
+ const { prefetch: on, getChildren: loader, store: current } = latest.current;
295
+ if (!on || !loader || !node) return;
296
+ if (isCascaderMoreNode(node)) return;
297
+ if (!node.hasChildren) return;
298
+ if (current.states.has(node.value)) return;
299
+ const holder = timers.current;
300
+ if (holder.prefetch) clearTimeout(holder.prefetch);
301
+ holder.prefetch = setTimeout(() => {
302
+ holder.prefetch = null;
303
+ ensureLevel(node.value, "prefetch");
304
+ }, PREFETCH_DELAY);
305
+ }, [ensureLevel]);
306
+ const runSearch = React$1.useCallback((text) => {
307
+ const { onSearch: searcher, path: currentPath } = latest.current;
308
+ if (!searcher) return;
309
+ abortKey(SEARCH_KEY);
310
+ const controller = new AbortController();
311
+ controllers.current.set(SEARCH_KEY, controller);
312
+ const requestId = (requestIds.current.get(SEARCH_KEY) ?? 0) + 1;
313
+ requestIds.current.set(SEARCH_KEY, requestId);
314
+ const startEpoch = epoch.current;
315
+ setSearch((prev) => ({
316
+ query: text,
317
+ results: prev?.results ?? [],
318
+ loading: true,
319
+ error: false
320
+ }));
321
+ const settle = () => {
322
+ if (controllers.current.get(SEARCH_KEY) === controller) controllers.current.delete(SEARCH_KEY);
323
+ };
324
+ const stale = () => controller.signal.aborted || startEpoch !== epoch.current || requestId !== requestIds.current.get(SEARCH_KEY);
325
+ Promise.resolve().then(() => searcher(text, {
326
+ signal: controller.signal,
327
+ path: currentPath
328
+ })).then((result) => {
329
+ settle();
330
+ if (stale()) return;
331
+ const items = Array.isArray(result) ? result : result.items;
332
+ remember(items);
333
+ setStore((prev) => withDetached(prev, items));
334
+ setSearch({
335
+ query: text,
336
+ results: items,
337
+ loading: false,
338
+ error: false
339
+ });
340
+ }).catch((error) => {
341
+ settle();
342
+ if (stale()) return;
343
+ setSearch((prev) => ({
344
+ query: text,
345
+ results: prev?.results ?? [],
346
+ loading: false,
347
+ error: true
348
+ }));
349
+ latest.current.onLoadError?.(error, {
350
+ parent: null,
351
+ reason: "search"
352
+ });
353
+ });
354
+ }, [abortKey, remember]);
355
+ const retrySearch = React$1.useCallback(() => {
356
+ const text = latest.current.query.trim();
357
+ if (text) runSearch(text);
358
+ }, [runSearch]);
359
+ const runResolve = React$1.useCallback((value) => {
360
+ const { resolveValue: resolver } = latest.current;
361
+ if (!resolver) return;
362
+ const key = `${RESOLVE_PREFIX}${value}`;
363
+ abortKey(key);
364
+ const controller = new AbortController();
365
+ controllers.current.set(key, controller);
366
+ const startEpoch = epoch.current;
367
+ const settle = () => {
368
+ if (controllers.current.get(key) === controller) controllers.current.delete(key);
369
+ };
370
+ Promise.resolve().then(() => resolver(value, {
371
+ signal: controller.signal,
372
+ reason: "resolve"
373
+ })).then((chain) => {
374
+ settle();
375
+ if (controller.signal.aborted || startEpoch !== epoch.current) return;
376
+ if (!chain?.length) return;
377
+ remember(chain);
378
+ setStore((prev) => withChain(prev, chain));
379
+ }).catch((error) => {
380
+ const live = controllers.current.get(key) === controller;
381
+ settle();
382
+ if (live) failed.current.add(value);
383
+ if (controller.signal.aborted || startEpoch !== epoch.current) return;
384
+ latest.current.onLoadError?.(error, {
385
+ parent: null,
386
+ reason: "resolve"
387
+ });
388
+ });
389
+ }, [abortKey, remember]);
390
+ const cancelPanel = React$1.useCallback(() => {
391
+ const keys = Array.from(controllers.current.keys()).filter((key) => !key.startsWith(RESOLVE_PREFIX));
392
+ for (const key of keys) {
393
+ controllers.current.get(key)?.abort();
394
+ controllers.current.delete(key);
395
+ }
396
+ inflight.current.clear();
397
+ const holder = timers.current;
398
+ if (holder.prefetch) {
399
+ clearTimeout(holder.prefetch);
400
+ holder.prefetch = null;
401
+ }
402
+ if (keys.length === 0) return;
403
+ setStore((prev) => {
404
+ let states = null;
405
+ for (const key of keys) {
406
+ const state = prev.states.get(key);
407
+ if (!state?.loading) continue;
408
+ states = states ?? new Map(prev.states);
409
+ if ((prev.pages.get(key)?.length ?? 0) > 0) states.set(key, {
410
+ ...state,
411
+ loading: false
412
+ });
413
+ else states.delete(key);
414
+ }
415
+ return states ? {
416
+ ...prev,
417
+ states
418
+ } : prev;
419
+ });
420
+ setSearch((prev) => prev?.loading ? {
421
+ ...prev,
422
+ loading: false
423
+ } : prev);
424
+ }, []);
425
+ const loadKeyRef = React$1.useRef(null);
426
+ React$1.useEffect(() => {
427
+ const previous = loadKeyRef.current;
428
+ loadKeyRef.current = { key: loadKey };
429
+ if (!previous || Object.is(previous.key, loadKey)) return;
430
+ epoch.current += 1;
431
+ for (const controller of controllers.current.values()) controller.abort();
432
+ controllers.current.clear();
433
+ inflight.current.clear();
434
+ moreLatch.current.clear();
435
+ attempted.current.clear();
436
+ failed.current.clear();
437
+ known.current.clear();
438
+ const holder = timers.current;
439
+ if (holder.prefetch) {
440
+ clearTimeout(holder.prefetch);
441
+ holder.prefetch = null;
442
+ }
443
+ setStore(createStore);
444
+ setSearch(null);
445
+ }, [loadKey]);
446
+ React$1.useEffect(() => {
447
+ if (enabled) return;
448
+ if (controllers.current.size === 0) return;
449
+ cancelPanel();
450
+ }, [enabled, cancelPanel]);
451
+ React$1.useEffect(() => {
452
+ const active = controllers.current;
453
+ const holder = timers.current;
454
+ const pending = inflight.current;
455
+ const ids = requestIds.current;
456
+ const tried = attempted.current;
457
+ const parked = failed.current;
458
+ return () => {
459
+ for (const controller of active.values()) controller.abort();
460
+ active.clear();
461
+ pending.clear();
462
+ ids.clear();
463
+ tried.clear();
464
+ parked.clear();
465
+ if (holder.prefetch) clearTimeout(holder.prefetch);
466
+ };
467
+ }, []);
468
+ const hasLoader = typeof getChildren === "function";
469
+ const hasSearch = typeof onSearch === "function";
470
+ const hasResolve = typeof resolveValue === "function";
471
+ const trimmed = query.trim();
472
+ const levelsKey = JSON.stringify(levels);
473
+ const valuesKey = JSON.stringify(values);
474
+ React$1.useEffect(() => {
475
+ if (!enabled || !hasLoader) return;
476
+ for (const key of levels) ensureLevel(key, "level");
477
+ }, [
478
+ enabled,
479
+ hasLoader,
480
+ levelsKey,
481
+ store,
482
+ ensureLevel
483
+ ]);
484
+ React$1.useEffect(() => {
485
+ if (!hasSearch) return void 0;
486
+ if (!enabled || !trimmed) {
487
+ abortKey(SEARCH_KEY);
488
+ setSearch((prev) => prev === null ? prev : null);
489
+ return;
490
+ }
491
+ const timer = setTimeout(() => runSearch(trimmed), searchDebounce);
492
+ return () => {
493
+ clearTimeout(timer);
494
+ abortKey(SEARCH_KEY);
495
+ };
496
+ }, [
497
+ hasSearch,
498
+ enabled,
499
+ trimmed,
500
+ searchDebounce,
501
+ abortKey,
502
+ runSearch
503
+ ]);
504
+ React$1.useEffect(() => {
505
+ if (!enabled || failed.current.size === 0) return;
506
+ for (const value of failed.current) attempted.current.delete(value);
507
+ failed.current.clear();
508
+ }, [enabled]);
509
+ React$1.useEffect(() => {
510
+ if (!hasResolve) return;
511
+ for (const value of values) {
512
+ if (!value) continue;
513
+ if (attempted.current.has(value)) continue;
514
+ if (base.byValue.has(value) || known.current.has(value)) continue;
515
+ attempted.current.add(value);
516
+ runResolve(value);
517
+ }
518
+ }, [
519
+ enabled,
520
+ hasResolve,
521
+ valuesKey,
522
+ base,
523
+ runResolve,
524
+ loadKey
525
+ ]);
526
+ const searchResults = React$1.useMemo(() => {
527
+ if (!hasSearch || !trimmed) return null;
528
+ if (!search || search.query !== trimmed) return NO_RESULTS;
529
+ return search.results;
530
+ }, [
531
+ hasSearch,
532
+ trimmed,
533
+ search
534
+ ]);
535
+ const searchState = React$1.useMemo(() => {
536
+ if (!hasSearch || !trimmed) return null;
537
+ const settled = search?.query === trimmed;
538
+ return {
539
+ loading: !settled || !!search?.loading,
540
+ error: settled && !!search?.error,
541
+ hasMore: false
542
+ };
543
+ }, [
544
+ hasSearch,
545
+ trimmed,
546
+ search
547
+ ]);
548
+ return React$1.useMemo(() => ({
549
+ active: hasLoader,
550
+ store,
551
+ states: store.states,
552
+ searchResults,
553
+ searchState,
554
+ ensureLevel,
555
+ loadMore,
556
+ retryLevel,
557
+ retrySearch,
558
+ invalidateLevel,
559
+ prefetchNode
560
+ }), [
561
+ hasLoader,
562
+ store,
563
+ searchResults,
564
+ searchState,
565
+ ensureLevel,
566
+ loadMore,
567
+ retryLevel,
568
+ retrySearch,
569
+ invalidateLevel,
570
+ prefetchNode
571
+ ]);
572
+ }
573
+ /** One level's load state, `null` when never fetched. Omit for the root. */
574
+ function useCascaderLoadState(parent) {
575
+ const { loadStates } = useCascaderState();
576
+ return loadStates.get(parent ?? "\0root") ?? null;
577
+ }
578
+ export { useCascaderLoadState, useCascaderLoader };
@@ -0,0 +1,12 @@
1
+ import { CascaderNode } from "./cascader-types.mjs";
2
+ import "react";
3
+ export interface CascaderItemState<T = unknown> {
4
+ branch: boolean;
5
+ selected: boolean;
6
+ /** The node's own flag or a disabled ancestor's, as `isCascaderDisabled`. */
7
+ disabled: boolean;
8
+ depth: number;
9
+ count: number;
10
+ /** Ancestor chain, root first. Populated for deep and global search rows. */
11
+ path: CascaderNode<T>[];
12
+ }