@deftui/react 0.2.5 → 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/README.md +2 -2
- package/dist/index.d.mts +7 -5
- package/dist/index.mjs +4 -4
- package/dist/styles.css +3 -0
- package/dist/vendor/README.md +10 -9
- package/dist/vendor/coss/calendar.d.mts +4 -0
- package/dist/vendor/coss/calendar.mjs +70 -0
- package/dist/vendor/coss/card.d.mts +23 -0
- package/dist/vendor/coss/card.mjs +160 -0
- package/dist/vendor/coss/dialog.d.mts +1 -1
- package/dist/vendor/coss/dialog.mjs +1 -1
- package/dist/vendor/coss/table.d.mts +15 -0
- package/dist/vendor/coss/table.mjs +73 -0
- 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/vendor/tailored/lightbox.d.mts +1 -1
- package/dist/vendor/tailored/lightbox.mjs +1 -1
- package/dist/widgets/datetime.d.mts +1 -1
- package/dist/widgets/datetime.mjs +6 -1
- package/dist/widgets/export.d.mts +28 -0
- package/dist/widgets/index.d.mts +69 -20
- package/dist/widgets/index.mjs +61 -11
- package/package.json +2 -2
- package/dist/vendor/shadcn/calendar.d.mts +0 -10
- package/dist/vendor/shadcn/calendar.mjs +0 -107
- package/dist/vendor/shadcn/card.d.mts +0 -11
- package/dist/vendor/shadcn/card.mjs +0 -55
- package/dist/vendor/shadcn/table.d.mts +0 -9
- package/dist/vendor/shadcn/table.mjs +0 -59
- package/dist/vendor/tailored/file-tree.d.mts +0 -21
- package/dist/vendor/tailored/file-tree.mjs +0 -146
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React$1 from "react";
|
|
3
|
+
// src/vendor/reui.io/cascader/cascader-context.tsx
|
|
4
|
+
const CascaderStateContext = React$1.createContext(void 0);
|
|
5
|
+
/**
|
|
6
|
+
* One provider serves every `T`, so the context holds an erased `unknown` value
|
|
7
|
+
* and this cast restores it. The primitive never inspects the payload.
|
|
8
|
+
*/
|
|
9
|
+
function useCascaderState() {
|
|
10
|
+
const context = React$1.useContext(CascaderStateContext);
|
|
11
|
+
if (!context) throw new Error("useCascaderState must be used within a Cascader");
|
|
12
|
+
return context;
|
|
13
|
+
}
|
|
14
|
+
const CascaderActionsContext = React$1.createContext(void 0);
|
|
15
|
+
function useCascaderActions() {
|
|
16
|
+
const context = React$1.useContext(CascaderActionsContext);
|
|
17
|
+
if (!context) throw new Error("useCascaderActions must be used within a Cascader");
|
|
18
|
+
return context;
|
|
19
|
+
}
|
|
20
|
+
const CascaderRenderContext = React$1.createContext({});
|
|
21
|
+
function useCascaderRender() {
|
|
22
|
+
return React$1.useContext(CascaderRenderContext);
|
|
23
|
+
}
|
|
24
|
+
const NO_HIGHLIGHT = {
|
|
25
|
+
index: -1,
|
|
26
|
+
value: null
|
|
27
|
+
};
|
|
28
|
+
function createCascaderHighlightStore() {
|
|
29
|
+
let snapshot = NO_HIGHLIGHT;
|
|
30
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
31
|
+
return {
|
|
32
|
+
subscribe(onStoreChange) {
|
|
33
|
+
listeners.add(onStoreChange);
|
|
34
|
+
return () => {
|
|
35
|
+
listeners.delete(onStoreChange);
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
getSnapshot() {
|
|
39
|
+
return snapshot;
|
|
40
|
+
},
|
|
41
|
+
set(next) {
|
|
42
|
+
if (next.index === snapshot.index && next.value === snapshot.value) return;
|
|
43
|
+
snapshot = next;
|
|
44
|
+
for (const listener of listeners) listener();
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Shared and permanently empty, so the hook degrades outside a `Cascader`. */
|
|
49
|
+
const FALLBACK_HIGHLIGHT_STORE = createCascaderHighlightStore();
|
|
50
|
+
const CascaderHighlightContext = React$1.createContext(FALLBACK_HIGHLIGHT_STORE);
|
|
51
|
+
/** Subscribes to the highlight. Re-renders ONLY the calling component. */
|
|
52
|
+
function useCascaderHighlight() {
|
|
53
|
+
const store = React$1.useContext(CascaderHighlightContext);
|
|
54
|
+
return React$1.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
55
|
+
}
|
|
56
|
+
export { CascaderActionsContext, CascaderHighlightContext, CascaderRenderContext, CascaderStateContext, createCascaderHighlightStore, useCascaderActions, useCascaderHighlight, useCascaderRender, useCascaderState };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React$1 from "react";
|
|
2
|
+
import { Popover } from "@base-ui/react";
|
|
3
|
+
export type CascaderFooterProps = React$1.ComponentProps<"div">;
|
|
4
|
+
/**
|
|
5
|
+
* Actions pinned below the list, a SIBLING of `CascaderList`. Children win
|
|
6
|
+
* over the root's `actions` prop; with neither it renders nothing.
|
|
7
|
+
*/
|
|
8
|
+
declare function CascaderFooter({ className, children, onKeyDown, ...props }: CascaderFooterProps): React$1.JSX.Element | null;
|
|
9
|
+
export interface CascaderActionProps extends Omit<React$1.ComponentProps<"button">, "onSelect"> {
|
|
10
|
+
icon?: React$1.ReactNode;
|
|
11
|
+
/** Fires on press, after `onClick`, and not at all when disabled. */
|
|
12
|
+
onSelect?: () => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One footer command, shaped like a row and deliberately NOT one. A real
|
|
16
|
+
* `<button>`, never a `Combobox.Item`: an item would join the arrow-key ring,
|
|
17
|
+
* appear in `filteredItems`, and vanish the moment a query matched nothing -
|
|
18
|
+
* exactly when "Create new attribute" is most useful. Disabled is
|
|
19
|
+
* `aria-disabled`, never the native attribute: that one is not a tab stop and
|
|
20
|
+
* the panel's Tab order reads `button:not([disabled])`, so a footer whose ONLY
|
|
21
|
+
* row is a disabled command had no stop after the search field (measured on
|
|
22
|
+
* `c-cascader-8`). Staying focusable costs the guards below, plus the
|
|
23
|
+
* greyed-out look: `CASCADER_ACTION_CLASS` keys that off `aria-disabled`
|
|
24
|
+
* instead of `:disabled`.
|
|
25
|
+
*/
|
|
26
|
+
declare function CascaderAction({ className, icon, children, onSelect, onClick, onKeyDown, disabled, ...props }: CascaderActionProps): React$1.JSX.Element;
|
|
27
|
+
export interface CascaderSubmenuProps {
|
|
28
|
+
open?: boolean;
|
|
29
|
+
defaultOpen?: boolean;
|
|
30
|
+
onOpenChange?: (open: boolean) => void;
|
|
31
|
+
children?: React$1.ReactNode;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A footer row plus the flyout it opens. Registers with the cascader root
|
|
35
|
+
* while open, which turns one Escape into two. Cleared in an EFFECT, so the
|
|
36
|
+
* flyout still reads as open during the event that closed it.
|
|
37
|
+
*/
|
|
38
|
+
declare function CascaderSubmenu({ open: openProp, defaultOpen, onOpenChange, children }: CascaderSubmenuProps): React$1.JSX.Element;
|
|
39
|
+
export interface CascaderSubmenuTriggerProps extends Omit<React$1.ComponentProps<"button">, "onSelect"> {
|
|
40
|
+
icon?: React$1.ReactNode;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The footer row that opens the flyout, and its anchor. `aria-haspopup="menu"`
|
|
44
|
+
* rather than the `dialog` Base UI would announce: what opens is a list of
|
|
45
|
+
* commands with roving focus. `disabled` is intercepted, not forwarded:
|
|
46
|
+
* `Popover.Trigger` runs it through `useButton`, which writes the NATIVE
|
|
47
|
+
* attribute for a native `<button>` and takes the row out of the panel's Tab
|
|
48
|
+
* ring, the defect `CascaderAction` documents, and this component exposes no
|
|
49
|
+
* `focusableWhenDisabled` to opt out. Published as `aria-disabled`, with the
|
|
50
|
+
* arrow keys, Enter and Space, and the click closed by hand (`useClick`
|
|
51
|
+
* ignores `defaultPrevented`).
|
|
52
|
+
*/
|
|
53
|
+
declare function CascaderSubmenuTrigger({ className, icon, children, onKeyDown, onClick, disabled, ...props }: CascaderSubmenuTriggerProps): React$1.JSX.Element;
|
|
54
|
+
export interface CascaderSubmenuContentProps extends Popover.Popup.Props, Pick<Popover.Positioner.Props, "side" | "align" | "sideOffset" | "alignOffset"> {}
|
|
55
|
+
/**
|
|
56
|
+
* The flyout itself. `Portal` with NO `container`: a nested portal resolves to
|
|
57
|
+
* the parent portal node, the mechanism the file header describes.
|
|
58
|
+
*/
|
|
59
|
+
declare function CascaderSubmenuContent({ className, children, onKeyDown, side, align, sideOffset, alignOffset, ...props }: CascaderSubmenuContentProps): React$1.JSX.Element;
|
|
60
|
+
export { CascaderAction, CascaderFooter, CascaderSubmenu, CascaderSubmenuContent, CascaderSubmenuTrigger };
|