@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.
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +2 -3
- package/dist/vendor/README.md +2 -1
- package/dist/vendor/reui.io/cascader/cascader-async.d.mts +20 -0
- package/dist/vendor/reui.io/cascader/cascader-async.mjs +578 -0
- package/dist/vendor/reui.io/cascader/cascader-context.d.mts +12 -0
- package/dist/vendor/reui.io/cascader/cascader-context.mjs +56 -0
- package/dist/vendor/reui.io/cascader/cascader-footer.d.mts +60 -0
- package/dist/vendor/reui.io/cascader/cascader-footer.mjs +445 -0
- package/dist/vendor/reui.io/cascader/cascader-i18n.mjs +64 -0
- package/dist/vendor/reui.io/cascader/cascader-item.d.mts +92 -0
- package/dist/vendor/reui.io/cascader/cascader-item.mjs +533 -0
- package/dist/vendor/reui.io/cascader/cascader-lib.d.mts +3 -0
- package/dist/vendor/reui.io/cascader/cascader-lib.mjs +688 -0
- package/dist/vendor/reui.io/cascader/cascader-nav.d.mts +56 -0
- package/dist/vendor/reui.io/cascader/cascader-nav.mjs +310 -0
- package/dist/vendor/reui.io/cascader/cascader-types.d.mts +140 -0
- package/dist/vendor/reui.io/cascader/cascader-virtual.d.mts +18 -0
- package/dist/vendor/reui.io/cascader/cascader-virtual.mjs +159 -0
- package/dist/vendor/reui.io/cascader/cascader.d.mts +299 -0
- package/dist/vendor/reui.io/cascader/cascader.mjs +1470 -0
- package/dist/vendor/reui.io/timeline.d.mts +21 -0
- package/dist/vendor/reui.io/timeline.mjs +127 -0
- package/dist/widgets/export.d.mts +28 -0
- package/dist/widgets/index.d.mts +55 -11
- package/dist/widgets/index.mjs +45 -2
- package/package.json +2 -2
- package/dist/vendor/tailored/file-tree.d.mts +0 -21
- 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 {
|
|
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,
|
|
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 {
|
|
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,
|
|
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 };
|
package/dist/vendor/README.md
CHANGED
|
@@ -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
|
+
}
|