@particle-academy/react-fancy 4.12.1 → 4.13.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 +1 -0
- package/dist/index.cjs +924 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +281 -7
- package/dist/index.d.ts +281 -7
- package/dist/index.js +924 -5
- package/dist/index.js.map +1 -1
- package/docs/FileBrowser.md +231 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -7884,11 +7884,12 @@ function usePopover() {
|
|
|
7884
7884
|
}
|
|
7885
7885
|
return ctx;
|
|
7886
7886
|
}
|
|
7887
|
-
function PopoverTrigger({ children, className }) {
|
|
7887
|
+
function PopoverTrigger({ children, className, ...rest }) {
|
|
7888
7888
|
const { setOpen, open, anchorRef, hover, onHoverEnter, onHoverLeave } = usePopover();
|
|
7889
7889
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
7890
7890
|
"span",
|
|
7891
7891
|
{
|
|
7892
|
+
...rest,
|
|
7892
7893
|
ref: anchorRef,
|
|
7893
7894
|
"data-react-fancy-popover-trigger": "",
|
|
7894
7895
|
className: cn("inline-flex", className),
|
|
@@ -7902,7 +7903,7 @@ function PopoverTrigger({ children, className }) {
|
|
|
7902
7903
|
);
|
|
7903
7904
|
}
|
|
7904
7905
|
PopoverTrigger.displayName = "PopoverTrigger";
|
|
7905
|
-
function PopoverContent({ children, className }) {
|
|
7906
|
+
function PopoverContent({ children, className, style, ...rest }) {
|
|
7906
7907
|
const { open, setOpen, anchorRef, floatingRef, placement, offset, hover, onHoverEnter, onHoverLeave } = usePopover();
|
|
7907
7908
|
const outsideRef = react.useRef(null);
|
|
7908
7909
|
const position = useFloatingPosition(anchorRef, floatingRef, {
|
|
@@ -7918,6 +7919,7 @@ function PopoverContent({ children, className }) {
|
|
|
7918
7919
|
return /* @__PURE__ */ jsxRuntime.jsx(Portal, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
7919
7920
|
"div",
|
|
7920
7921
|
{
|
|
7922
|
+
...rest,
|
|
7921
7923
|
ref: (node) => {
|
|
7922
7924
|
outsideRef.current = node;
|
|
7923
7925
|
floatingRef.current = node;
|
|
@@ -7928,7 +7930,7 @@ function PopoverContent({ children, className }) {
|
|
|
7928
7930
|
positioned ? "fancy-scale-in" : "invisible",
|
|
7929
7931
|
className
|
|
7930
7932
|
),
|
|
7931
|
-
style: { left: position.x, top: position.y },
|
|
7933
|
+
style: { ...style, left: position.x, top: position.y },
|
|
7932
7934
|
onMouseEnter: hover ? onHoverEnter : void 0,
|
|
7933
7935
|
onMouseLeave: hover ? onHoverLeave : void 0,
|
|
7934
7936
|
children
|
|
@@ -13142,6 +13144,923 @@ TreeNavRoot.displayName = "TreeNav";
|
|
|
13142
13144
|
var TreeNav = Object.assign(TreeNavRoot, {
|
|
13143
13145
|
Node: TreeNode
|
|
13144
13146
|
});
|
|
13147
|
+
var FileBrowserContext = react.createContext(null);
|
|
13148
|
+
function useFileBrowser() {
|
|
13149
|
+
const ctx = react.useContext(FileBrowserContext);
|
|
13150
|
+
if (!ctx) {
|
|
13151
|
+
throw new Error("useFileBrowser must be used within a <FileBrowser> component");
|
|
13152
|
+
}
|
|
13153
|
+
return ctx;
|
|
13154
|
+
}
|
|
13155
|
+
|
|
13156
|
+
// src/components/FileBrowser/FileBrowser.utils.ts
|
|
13157
|
+
function normalizePath(input) {
|
|
13158
|
+
const raw = input.trim().replace(/\\/g, "/");
|
|
13159
|
+
if (raw === "") return "/";
|
|
13160
|
+
const absolute = raw.startsWith("/");
|
|
13161
|
+
const out = [];
|
|
13162
|
+
for (const segment of raw.split("/")) {
|
|
13163
|
+
if (segment === "" || segment === ".") continue;
|
|
13164
|
+
if (segment === "..") {
|
|
13165
|
+
out.pop();
|
|
13166
|
+
continue;
|
|
13167
|
+
}
|
|
13168
|
+
out.push(segment);
|
|
13169
|
+
}
|
|
13170
|
+
if (out.length === 0) return "/";
|
|
13171
|
+
return (absolute ? "/" : "") + out.join("/");
|
|
13172
|
+
}
|
|
13173
|
+
function parentPath(path) {
|
|
13174
|
+
const i = path.lastIndexOf("/");
|
|
13175
|
+
if (i === -1) return "/";
|
|
13176
|
+
if (i === 0) return "/";
|
|
13177
|
+
return path.slice(0, i);
|
|
13178
|
+
}
|
|
13179
|
+
function pathSegments(path) {
|
|
13180
|
+
const absolute = path.startsWith("/");
|
|
13181
|
+
const parts = path.split("/").filter(Boolean);
|
|
13182
|
+
const segments = [];
|
|
13183
|
+
let acc = "";
|
|
13184
|
+
parts.forEach((part, i) => {
|
|
13185
|
+
acc = i === 0 && !absolute ? part : `${acc}/${part}`;
|
|
13186
|
+
segments.push({ label: part, path: acc });
|
|
13187
|
+
});
|
|
13188
|
+
return segments;
|
|
13189
|
+
}
|
|
13190
|
+
function compareNames(a, b) {
|
|
13191
|
+
const cmp = a.localeCompare(b, "en", { numeric: true, sensitivity: "base" });
|
|
13192
|
+
if (cmp !== 0) return cmp;
|
|
13193
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
13194
|
+
}
|
|
13195
|
+
function compareFileEntries(a, b, sort) {
|
|
13196
|
+
if (a.kind !== b.kind) return a.kind === "dir" ? -1 : 1;
|
|
13197
|
+
const direction = sort.direction === "desc" ? -1 : 1;
|
|
13198
|
+
let cmp;
|
|
13199
|
+
switch (sort.by) {
|
|
13200
|
+
case "size":
|
|
13201
|
+
cmp = (a.size ?? -1) - (b.size ?? -1);
|
|
13202
|
+
break;
|
|
13203
|
+
case "mtime": {
|
|
13204
|
+
const am = a.mtime ?? "";
|
|
13205
|
+
const bm = b.mtime ?? "";
|
|
13206
|
+
cmp = am < bm ? -1 : am > bm ? 1 : 0;
|
|
13207
|
+
break;
|
|
13208
|
+
}
|
|
13209
|
+
default:
|
|
13210
|
+
cmp = compareNames(a.name, b.name);
|
|
13211
|
+
}
|
|
13212
|
+
if (cmp !== 0) return direction * cmp;
|
|
13213
|
+
return compareNames(a.name, b.name);
|
|
13214
|
+
}
|
|
13215
|
+
function isEntryExpandable(entry, children, hasProvider) {
|
|
13216
|
+
if (entry.kind !== "dir" || entry.hasChildren === false) return false;
|
|
13217
|
+
if (children !== void 0) return children.length > 0;
|
|
13218
|
+
return hasProvider;
|
|
13219
|
+
}
|
|
13220
|
+
function entryMatchesFilter(entry, query, entriesFor, visited = /* @__PURE__ */ new Set()) {
|
|
13221
|
+
if (entry.name.toLowerCase().includes(query)) return true;
|
|
13222
|
+
if (entry.kind !== "dir" || visited.has(entry.path)) return false;
|
|
13223
|
+
visited.add(entry.path);
|
|
13224
|
+
const children = entriesFor(entry.path);
|
|
13225
|
+
return children?.some((child) => entryMatchesFilter(child, query, entriesFor, visited)) ?? false;
|
|
13226
|
+
}
|
|
13227
|
+
function formatFileSize(bytes) {
|
|
13228
|
+
if (!Number.isFinite(bytes) || bytes < 0) return "";
|
|
13229
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
13230
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
13231
|
+
let value = bytes;
|
|
13232
|
+
let unit = -1;
|
|
13233
|
+
do {
|
|
13234
|
+
value /= 1024;
|
|
13235
|
+
unit++;
|
|
13236
|
+
} while (value >= 1024 && unit < units.length - 1);
|
|
13237
|
+
const rounded = value >= 100 ? Math.round(value) : Math.round(value * 10) / 10;
|
|
13238
|
+
return `${rounded} ${units[unit]}`;
|
|
13239
|
+
}
|
|
13240
|
+
var SEGMENT_CLASS = "shrink-0 rounded text-[13px] text-zinc-500 transition-colors hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300";
|
|
13241
|
+
var ACTIVE_SEGMENT_CLASS = "shrink-0 truncate text-[13px] font-medium text-zinc-900 dark:text-white";
|
|
13242
|
+
function FileBrowserPathBar({
|
|
13243
|
+
editable = true,
|
|
13244
|
+
placeholder = "/path/to/folder",
|
|
13245
|
+
className
|
|
13246
|
+
}) {
|
|
13247
|
+
const { path, navigate } = useFileBrowser();
|
|
13248
|
+
const [editing, setEditing] = react.useState(false);
|
|
13249
|
+
const [draft, setDraft] = react.useState("");
|
|
13250
|
+
const startEdit = () => {
|
|
13251
|
+
setDraft(path);
|
|
13252
|
+
setEditing(true);
|
|
13253
|
+
};
|
|
13254
|
+
const commit = () => {
|
|
13255
|
+
navigate(normalizePath(draft));
|
|
13256
|
+
setEditing(false);
|
|
13257
|
+
};
|
|
13258
|
+
const segments = pathSegments(path);
|
|
13259
|
+
const atRoot = segments.length === 0;
|
|
13260
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
13261
|
+
"div",
|
|
13262
|
+
{
|
|
13263
|
+
"data-react-fancy-file-browser-path": "",
|
|
13264
|
+
className: cn(
|
|
13265
|
+
"flex items-center gap-1 border-b border-zinc-200 px-2 py-1.5 dark:border-zinc-700",
|
|
13266
|
+
className
|
|
13267
|
+
),
|
|
13268
|
+
children: [
|
|
13269
|
+
editing ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
13270
|
+
"input",
|
|
13271
|
+
{
|
|
13272
|
+
"data-react-fancy-file-browser-path-input": "",
|
|
13273
|
+
type: "text",
|
|
13274
|
+
value: draft,
|
|
13275
|
+
onChange: (e) => setDraft(e.target.value),
|
|
13276
|
+
onKeyDown: (e) => {
|
|
13277
|
+
if (e.key === "Enter") {
|
|
13278
|
+
e.preventDefault();
|
|
13279
|
+
commit();
|
|
13280
|
+
} else if (e.key === "Escape") {
|
|
13281
|
+
e.preventDefault();
|
|
13282
|
+
setEditing(false);
|
|
13283
|
+
}
|
|
13284
|
+
},
|
|
13285
|
+
onBlur: () => setEditing(false),
|
|
13286
|
+
autoFocus: true,
|
|
13287
|
+
spellCheck: false,
|
|
13288
|
+
placeholder,
|
|
13289
|
+
"aria-label": "Path",
|
|
13290
|
+
className: "min-w-0 flex-1 rounded-md border border-zinc-300 bg-transparent px-2 py-0.5 font-mono text-xs text-zinc-700 outline-none placeholder:text-zinc-400 focus:border-blue-400 dark:border-zinc-600 dark:text-zinc-300"
|
|
13291
|
+
}
|
|
13292
|
+
) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
13293
|
+
"nav",
|
|
13294
|
+
{
|
|
13295
|
+
"aria-label": "Path",
|
|
13296
|
+
onDoubleClick: editable ? startEdit : void 0,
|
|
13297
|
+
className: "flex min-w-0 flex-1 items-center gap-1 overflow-x-auto",
|
|
13298
|
+
children: [
|
|
13299
|
+
atRoot ? /* @__PURE__ */ jsxRuntime.jsx("span", { "data-react-fancy-file-browser-path-segment": "", "aria-current": "location", className: ACTIVE_SEGMENT_CLASS, children: "/" }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
13300
|
+
"button",
|
|
13301
|
+
{
|
|
13302
|
+
type: "button",
|
|
13303
|
+
"data-react-fancy-file-browser-path-segment": "",
|
|
13304
|
+
onClick: () => navigate("/"),
|
|
13305
|
+
className: SEGMENT_CLASS,
|
|
13306
|
+
children: "/"
|
|
13307
|
+
}
|
|
13308
|
+
),
|
|
13309
|
+
segments.map((segment, i) => {
|
|
13310
|
+
const last = i === segments.length - 1;
|
|
13311
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
|
|
13312
|
+
i > 0 && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { size: 12, "aria-hidden": "true", className: "shrink-0 text-zinc-400" }),
|
|
13313
|
+
last ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
13314
|
+
"span",
|
|
13315
|
+
{
|
|
13316
|
+
"data-react-fancy-file-browser-path-segment": "",
|
|
13317
|
+
"aria-current": "location",
|
|
13318
|
+
className: ACTIVE_SEGMENT_CLASS,
|
|
13319
|
+
children: segment.label
|
|
13320
|
+
}
|
|
13321
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
13322
|
+
"button",
|
|
13323
|
+
{
|
|
13324
|
+
type: "button",
|
|
13325
|
+
"data-react-fancy-file-browser-path-segment": "",
|
|
13326
|
+
onClick: () => navigate(segment.path),
|
|
13327
|
+
className: SEGMENT_CLASS,
|
|
13328
|
+
children: segment.label
|
|
13329
|
+
}
|
|
13330
|
+
)
|
|
13331
|
+
] }, segment.path);
|
|
13332
|
+
})
|
|
13333
|
+
]
|
|
13334
|
+
}
|
|
13335
|
+
),
|
|
13336
|
+
editable && !editing && /* @__PURE__ */ jsxRuntime.jsx(
|
|
13337
|
+
"button",
|
|
13338
|
+
{
|
|
13339
|
+
type: "button",
|
|
13340
|
+
"aria-label": "Edit path",
|
|
13341
|
+
onClick: startEdit,
|
|
13342
|
+
className: "shrink-0 rounded-md p-1 text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-300",
|
|
13343
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pencil, { size: 13 })
|
|
13344
|
+
}
|
|
13345
|
+
)
|
|
13346
|
+
]
|
|
13347
|
+
}
|
|
13348
|
+
);
|
|
13349
|
+
}
|
|
13350
|
+
FileBrowserPathBar.displayName = "FileBrowserPathBar";
|
|
13351
|
+
var SORT_FIELDS = [
|
|
13352
|
+
{ field: "name", label: "Name" },
|
|
13353
|
+
{ field: "size", label: "Size" },
|
|
13354
|
+
{ field: "mtime", label: "Modified" }
|
|
13355
|
+
];
|
|
13356
|
+
function FileBrowserToolbar({ filterPlaceholder = "Filter", className }) {
|
|
13357
|
+
const { filter, setFilter, sort, setSort } = useFileBrowser();
|
|
13358
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
13359
|
+
"div",
|
|
13360
|
+
{
|
|
13361
|
+
"data-react-fancy-file-browser-toolbar": "",
|
|
13362
|
+
className: cn(
|
|
13363
|
+
"flex items-center gap-2 border-b border-zinc-200 px-2 py-1.5 dark:border-zinc-700",
|
|
13364
|
+
className
|
|
13365
|
+
),
|
|
13366
|
+
children: [
|
|
13367
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
13368
|
+
"div",
|
|
13369
|
+
{
|
|
13370
|
+
"data-react-fancy-file-browser-filter": "",
|
|
13371
|
+
className: "flex min-w-0 flex-1 items-center gap-1.5 rounded-md border border-zinc-200 px-2 dark:border-zinc-700",
|
|
13372
|
+
children: [
|
|
13373
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Search, { size: 13, "aria-hidden": "true", className: "shrink-0 text-zinc-400" }),
|
|
13374
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
13375
|
+
"input",
|
|
13376
|
+
{
|
|
13377
|
+
type: "text",
|
|
13378
|
+
value: filter,
|
|
13379
|
+
onChange: (e) => setFilter(e.target.value),
|
|
13380
|
+
placeholder: filterPlaceholder,
|
|
13381
|
+
"aria-label": "Filter by name",
|
|
13382
|
+
spellCheck: false,
|
|
13383
|
+
className: "min-w-0 flex-1 bg-transparent py-1 text-xs text-zinc-700 outline-none placeholder:text-zinc-400 dark:text-zinc-300"
|
|
13384
|
+
}
|
|
13385
|
+
),
|
|
13386
|
+
filter !== "" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
13387
|
+
"button",
|
|
13388
|
+
{
|
|
13389
|
+
type: "button",
|
|
13390
|
+
"aria-label": "Clear filter",
|
|
13391
|
+
onClick: () => setFilter(""),
|
|
13392
|
+
className: "shrink-0 text-zinc-400 transition-colors hover:text-zinc-600 dark:hover:text-zinc-300",
|
|
13393
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 12 })
|
|
13394
|
+
}
|
|
13395
|
+
)
|
|
13396
|
+
]
|
|
13397
|
+
}
|
|
13398
|
+
),
|
|
13399
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
13400
|
+
"div",
|
|
13401
|
+
{
|
|
13402
|
+
"data-react-fancy-file-browser-sort": "",
|
|
13403
|
+
role: "group",
|
|
13404
|
+
"aria-label": "Sort",
|
|
13405
|
+
className: "flex shrink-0 items-center gap-0.5",
|
|
13406
|
+
children: SORT_FIELDS.map(({ field, label }) => {
|
|
13407
|
+
const active = sort.by === field;
|
|
13408
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
13409
|
+
"button",
|
|
13410
|
+
{
|
|
13411
|
+
type: "button",
|
|
13412
|
+
"data-sort-field": field,
|
|
13413
|
+
"aria-pressed": active,
|
|
13414
|
+
onClick: () => setSort(
|
|
13415
|
+
active ? { by: field, direction: sort.direction === "asc" ? "desc" : "asc" } : { by: field, direction: "asc" }
|
|
13416
|
+
),
|
|
13417
|
+
className: cn(
|
|
13418
|
+
"flex items-center gap-0.5 rounded-md px-1.5 py-0.5 text-xs transition-colors",
|
|
13419
|
+
active ? "bg-zinc-100 font-medium text-zinc-900 dark:bg-zinc-800 dark:text-white" : "text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300"
|
|
13420
|
+
),
|
|
13421
|
+
children: [
|
|
13422
|
+
label,
|
|
13423
|
+
active && (sort.direction === "asc" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowUp, { size: 11, "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowDown, { size: 11, "aria-hidden": "true" }))
|
|
13424
|
+
]
|
|
13425
|
+
},
|
|
13426
|
+
field
|
|
13427
|
+
);
|
|
13428
|
+
})
|
|
13429
|
+
}
|
|
13430
|
+
)
|
|
13431
|
+
]
|
|
13432
|
+
}
|
|
13433
|
+
);
|
|
13434
|
+
}
|
|
13435
|
+
FileBrowserToolbar.displayName = "FileBrowserToolbar";
|
|
13436
|
+
var EXT_COLORS2 = {
|
|
13437
|
+
ts: "#3178c6",
|
|
13438
|
+
tsx: "#3178c6",
|
|
13439
|
+
js: "#f7df1e",
|
|
13440
|
+
jsx: "#f7df1e",
|
|
13441
|
+
php: "#777bb4",
|
|
13442
|
+
html: "#e34c26",
|
|
13443
|
+
htm: "#e34c26",
|
|
13444
|
+
css: "#264de4",
|
|
13445
|
+
json: "#a1a1aa",
|
|
13446
|
+
md: "#71717a",
|
|
13447
|
+
yaml: "#cb171e",
|
|
13448
|
+
yml: "#cb171e"
|
|
13449
|
+
};
|
|
13450
|
+
function FileIcon2({ ext }) {
|
|
13451
|
+
const color = ext && EXT_COLORS2[ext.toLowerCase()] || "#71717a";
|
|
13452
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", className: "shrink-0", children: [
|
|
13453
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 1h5.5L13 4.5V14a1 1 0 01-1 1H4a1 1 0 01-1-1V2a1 1 0 011-1z", stroke: color, strokeWidth: "1.2" }),
|
|
13454
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 1v4h4", stroke: color, strokeWidth: "1.2" })
|
|
13455
|
+
] });
|
|
13456
|
+
}
|
|
13457
|
+
function FolderIcon2({ open }) {
|
|
13458
|
+
if (open) {
|
|
13459
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", className: "shrink-0", children: [
|
|
13460
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M1.5 3.5a1 1 0 011-1h3l1.5 1.5H13a1 1 0 011 1V5H2.5V3.5z", fill: "#fbbf24" }),
|
|
13461
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M1 6h13l-1.5 7.5H2.5L1 6z", fill: "#fbbf24", opacity: "0.7" })
|
|
13462
|
+
] });
|
|
13463
|
+
}
|
|
13464
|
+
return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", className: "shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M1.5 3a1 1 0 011-1h3l1.5 1.5H13a1 1 0 011 1v8a1 1 0 01-1 1H2.5a1 1 0 01-1-1V3z", fill: "#fbbf24" }) });
|
|
13465
|
+
}
|
|
13466
|
+
function FileBrowserStatusRow({ depth, kind, message, onRetry }) {
|
|
13467
|
+
const { indentSize } = useFileBrowser();
|
|
13468
|
+
const paddingLeft = depth * indentSize + 4 + 18;
|
|
13469
|
+
if (kind === "error") {
|
|
13470
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
13471
|
+
"div",
|
|
13472
|
+
{
|
|
13473
|
+
role: "none",
|
|
13474
|
+
"data-react-fancy-file-browser-status": "error",
|
|
13475
|
+
className: "flex items-center gap-1.5 py-0.5 pr-2 text-xs text-red-600 dark:text-red-400",
|
|
13476
|
+
style: { paddingLeft },
|
|
13477
|
+
children: [
|
|
13478
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.CircleAlert, { size: 12, className: "shrink-0" }),
|
|
13479
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate", children: message || "Failed to load" }),
|
|
13480
|
+
onRetry && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
13481
|
+
"button",
|
|
13482
|
+
{
|
|
13483
|
+
type: "button",
|
|
13484
|
+
onClick: onRetry,
|
|
13485
|
+
className: "flex shrink-0 items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200",
|
|
13486
|
+
children: [
|
|
13487
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCw, { size: 11 }),
|
|
13488
|
+
"Retry"
|
|
13489
|
+
]
|
|
13490
|
+
}
|
|
13491
|
+
)
|
|
13492
|
+
]
|
|
13493
|
+
}
|
|
13494
|
+
);
|
|
13495
|
+
}
|
|
13496
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
13497
|
+
"div",
|
|
13498
|
+
{
|
|
13499
|
+
role: "none",
|
|
13500
|
+
"data-react-fancy-file-browser-status": kind,
|
|
13501
|
+
className: "flex items-center gap-1.5 py-0.5 text-xs text-zinc-400 italic dark:text-zinc-500",
|
|
13502
|
+
style: { paddingLeft },
|
|
13503
|
+
children: [
|
|
13504
|
+
kind === "loading" && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 12, className: "shrink-0 animate-spin", "aria-hidden": "true" }),
|
|
13505
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: kind === "loading" ? "Loading\u2026" : kind === "empty" ? "Empty" : kind === "no-matches" ? "No matches" : "No entries" })
|
|
13506
|
+
]
|
|
13507
|
+
}
|
|
13508
|
+
);
|
|
13509
|
+
}
|
|
13510
|
+
FileBrowserStatusRow.displayName = "FileBrowserStatusRow";
|
|
13511
|
+
function FileBrowserNode({ entry, depth }) {
|
|
13512
|
+
const {
|
|
13513
|
+
entriesFor,
|
|
13514
|
+
visibleChildrenFor,
|
|
13515
|
+
statusFor,
|
|
13516
|
+
errorFor,
|
|
13517
|
+
loadPath,
|
|
13518
|
+
hasProvider,
|
|
13519
|
+
expandedPaths,
|
|
13520
|
+
toggleExpanded,
|
|
13521
|
+
navigate,
|
|
13522
|
+
isSelected,
|
|
13523
|
+
isSelectable,
|
|
13524
|
+
selectEntry,
|
|
13525
|
+
tabFocusPath,
|
|
13526
|
+
setFocusedPath,
|
|
13527
|
+
focusRow,
|
|
13528
|
+
registerRow,
|
|
13529
|
+
indentSize,
|
|
13530
|
+
showIcons
|
|
13531
|
+
} = useFileBrowser();
|
|
13532
|
+
const rawChildren = entriesFor(entry.path);
|
|
13533
|
+
const childrenKnown = rawChildren !== void 0;
|
|
13534
|
+
const expandable = isEntryExpandable(entry, rawChildren, hasProvider);
|
|
13535
|
+
const expanded = expandable && expandedPaths.includes(entry.path);
|
|
13536
|
+
const status = statusFor(entry.path);
|
|
13537
|
+
const selectable = isSelectable(entry);
|
|
13538
|
+
const selected = isSelected(entry.path);
|
|
13539
|
+
const loading = expanded && !childrenKnown && status === "loading";
|
|
13540
|
+
const paddingLeft = depth * indentSize + 4;
|
|
13541
|
+
const ext = entry.name.includes(".") ? entry.name.split(".").pop() : void 0;
|
|
13542
|
+
const handleClick = () => {
|
|
13543
|
+
if (entry.disabled) return;
|
|
13544
|
+
setFocusedPath(entry.path);
|
|
13545
|
+
focusRow(entry.path);
|
|
13546
|
+
if (selectable) selectEntry(entry);
|
|
13547
|
+
if (expandable) toggleExpanded(entry.path);
|
|
13548
|
+
};
|
|
13549
|
+
const handleDoubleClick = () => {
|
|
13550
|
+
if (entry.disabled || entry.kind !== "dir") return;
|
|
13551
|
+
navigate(entry.path);
|
|
13552
|
+
};
|
|
13553
|
+
const handleChevronClick = (e) => {
|
|
13554
|
+
e.stopPropagation();
|
|
13555
|
+
if (!entry.disabled && expandable) toggleExpanded(entry.path);
|
|
13556
|
+
};
|
|
13557
|
+
const visibleChildren = expanded && childrenKnown ? visibleChildrenFor(entry.path) : [];
|
|
13558
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
13559
|
+
"div",
|
|
13560
|
+
{
|
|
13561
|
+
role: "treeitem",
|
|
13562
|
+
"data-react-fancy-file-browser-node": "",
|
|
13563
|
+
"data-path": entry.path,
|
|
13564
|
+
"data-kind": entry.kind,
|
|
13565
|
+
"aria-level": depth + 1,
|
|
13566
|
+
"aria-expanded": expandable ? expanded : void 0,
|
|
13567
|
+
"aria-selected": selectable ? selected : void 0,
|
|
13568
|
+
"aria-disabled": entry.disabled || void 0,
|
|
13569
|
+
"aria-busy": loading || void 0,
|
|
13570
|
+
tabIndex: tabFocusPath === entry.path ? 0 : -1,
|
|
13571
|
+
ref: (el) => registerRow(entry.path, el),
|
|
13572
|
+
onFocus: (e) => {
|
|
13573
|
+
if (e.target === e.currentTarget) setFocusedPath(entry.path);
|
|
13574
|
+
},
|
|
13575
|
+
className: "group outline-none",
|
|
13576
|
+
children: [
|
|
13577
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
13578
|
+
"div",
|
|
13579
|
+
{
|
|
13580
|
+
"data-react-fancy-file-browser-row": "",
|
|
13581
|
+
onClick: handleClick,
|
|
13582
|
+
onDoubleClick: handleDoubleClick,
|
|
13583
|
+
className: cn(
|
|
13584
|
+
"flex w-full cursor-pointer items-center gap-1 rounded-md py-0.5 pr-2 text-left text-[13px] transition-colors select-none",
|
|
13585
|
+
selected ? "bg-blue-500/15 text-blue-600 dark:text-blue-400" : "text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800",
|
|
13586
|
+
entry.disabled && "pointer-events-none opacity-40",
|
|
13587
|
+
"group-focus-visible:ring-2 group-focus-visible:ring-blue-500/40 group-focus-visible:ring-inset"
|
|
13588
|
+
),
|
|
13589
|
+
style: { paddingLeft },
|
|
13590
|
+
children: [
|
|
13591
|
+
expandable ? loading ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 14, className: "shrink-0 animate-spin text-zinc-400", "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
13592
|
+
"button",
|
|
13593
|
+
{
|
|
13594
|
+
type: "button",
|
|
13595
|
+
tabIndex: -1,
|
|
13596
|
+
"aria-hidden": "true",
|
|
13597
|
+
onClick: handleChevronClick,
|
|
13598
|
+
className: "flex shrink-0 items-center justify-center text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300",
|
|
13599
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
13600
|
+
lucideReact.ChevronRight,
|
|
13601
|
+
{
|
|
13602
|
+
size: 14,
|
|
13603
|
+
className: cn("transition-transform duration-150", expanded && "rotate-90")
|
|
13604
|
+
}
|
|
13605
|
+
)
|
|
13606
|
+
}
|
|
13607
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-3.5 shrink-0" }),
|
|
13608
|
+
showIcons && (entry.kind === "dir" ? /* @__PURE__ */ jsxRuntime.jsx(FolderIcon2, { open: expanded }) : /* @__PURE__ */ jsxRuntime.jsx(FileIcon2, { ext })),
|
|
13609
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate", children: entry.name }),
|
|
13610
|
+
entry.kind === "file" && entry.size !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-[11px] text-zinc-400 tabular-nums dark:text-zinc-500", children: formatFileSize(entry.size) })
|
|
13611
|
+
]
|
|
13612
|
+
}
|
|
13613
|
+
),
|
|
13614
|
+
expanded && /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "group", "data-react-fancy-file-browser-node-children": "", children: [
|
|
13615
|
+
!childrenKnown && (status === "loading" || status === "idle") && /* @__PURE__ */ jsxRuntime.jsx(FileBrowserStatusRow, { depth: depth + 1, kind: "loading" }),
|
|
13616
|
+
!childrenKnown && status === "error" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
13617
|
+
FileBrowserStatusRow,
|
|
13618
|
+
{
|
|
13619
|
+
depth: depth + 1,
|
|
13620
|
+
kind: "error",
|
|
13621
|
+
message: errorFor(entry.path),
|
|
13622
|
+
onRetry: () => loadPath(entry.path, { reload: true })
|
|
13623
|
+
}
|
|
13624
|
+
),
|
|
13625
|
+
childrenKnown && (visibleChildren.length > 0 ? visibleChildren.map((child) => /* @__PURE__ */ jsxRuntime.jsx(FileBrowserNode, { entry: child, depth: depth + 1 }, child.path)) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
13626
|
+
FileBrowserStatusRow,
|
|
13627
|
+
{
|
|
13628
|
+
depth: depth + 1,
|
|
13629
|
+
kind: rawChildren.length === 0 ? "empty" : "no-matches"
|
|
13630
|
+
}
|
|
13631
|
+
))
|
|
13632
|
+
] })
|
|
13633
|
+
]
|
|
13634
|
+
}
|
|
13635
|
+
);
|
|
13636
|
+
}
|
|
13637
|
+
FileBrowserNode.displayName = "FileBrowserNode";
|
|
13638
|
+
function FileBrowserTree({ ariaLabel = "Files", className }) {
|
|
13639
|
+
const ctx = useFileBrowser();
|
|
13640
|
+
const {
|
|
13641
|
+
path,
|
|
13642
|
+
entriesFor,
|
|
13643
|
+
visibleChildrenFor,
|
|
13644
|
+
statusFor,
|
|
13645
|
+
errorFor,
|
|
13646
|
+
loadPath,
|
|
13647
|
+
hasProvider,
|
|
13648
|
+
visibleRows,
|
|
13649
|
+
tabFocusPath,
|
|
13650
|
+
setFocusedPath,
|
|
13651
|
+
focusRow,
|
|
13652
|
+
toggleExpanded,
|
|
13653
|
+
isSelectable,
|
|
13654
|
+
selectEntry,
|
|
13655
|
+
navigate,
|
|
13656
|
+
multiple
|
|
13657
|
+
} = ctx;
|
|
13658
|
+
const rootEntries = entriesFor(path);
|
|
13659
|
+
const rootStatus = statusFor(path);
|
|
13660
|
+
const rootChildren = rootEntries !== void 0 ? visibleChildrenFor(path) : [];
|
|
13661
|
+
const moveTo = (row) => {
|
|
13662
|
+
setFocusedPath(row.entry.path);
|
|
13663
|
+
focusRow(row.entry.path);
|
|
13664
|
+
};
|
|
13665
|
+
const activate = (row) => {
|
|
13666
|
+
if (row.entry.disabled) return;
|
|
13667
|
+
if (isSelectable(row.entry)) selectEntry(row.entry);
|
|
13668
|
+
if (row.expandable) toggleExpanded(row.entry.path);
|
|
13669
|
+
else if (row.entry.kind === "dir" && !isSelectable(row.entry)) navigate(row.entry.path);
|
|
13670
|
+
};
|
|
13671
|
+
const handleKeyDown = (e) => {
|
|
13672
|
+
const rows = visibleRows;
|
|
13673
|
+
if (rows.length === 0) return;
|
|
13674
|
+
const index = rows.findIndex((row) => row.entry.path === tabFocusPath);
|
|
13675
|
+
const current = index >= 0 ? rows[index] : rows[0];
|
|
13676
|
+
switch (e.key) {
|
|
13677
|
+
case "ArrowDown":
|
|
13678
|
+
e.preventDefault();
|
|
13679
|
+
if (index < rows.length - 1) moveTo(rows[index + 1]);
|
|
13680
|
+
break;
|
|
13681
|
+
case "ArrowUp":
|
|
13682
|
+
e.preventDefault();
|
|
13683
|
+
if (index > 0) moveTo(rows[index - 1]);
|
|
13684
|
+
break;
|
|
13685
|
+
case "Home":
|
|
13686
|
+
e.preventDefault();
|
|
13687
|
+
moveTo(rows[0]);
|
|
13688
|
+
break;
|
|
13689
|
+
case "End":
|
|
13690
|
+
e.preventDefault();
|
|
13691
|
+
moveTo(rows[rows.length - 1]);
|
|
13692
|
+
break;
|
|
13693
|
+
case "ArrowRight":
|
|
13694
|
+
e.preventDefault();
|
|
13695
|
+
if (current.entry.disabled) break;
|
|
13696
|
+
if (current.expandable && !current.expanded) {
|
|
13697
|
+
toggleExpanded(current.entry.path);
|
|
13698
|
+
} else if (current.expanded) {
|
|
13699
|
+
const firstChild = rows[index + 1];
|
|
13700
|
+
if (firstChild && firstChild.parentPath === current.entry.path) moveTo(firstChild);
|
|
13701
|
+
}
|
|
13702
|
+
break;
|
|
13703
|
+
case "ArrowLeft":
|
|
13704
|
+
e.preventDefault();
|
|
13705
|
+
if (current.expanded && !current.entry.disabled) {
|
|
13706
|
+
toggleExpanded(current.entry.path);
|
|
13707
|
+
} else if (current.parentPath) {
|
|
13708
|
+
const parent = rows.find((row) => row.entry.path === current.parentPath);
|
|
13709
|
+
if (parent) moveTo(parent);
|
|
13710
|
+
}
|
|
13711
|
+
break;
|
|
13712
|
+
case "Enter":
|
|
13713
|
+
e.preventDefault();
|
|
13714
|
+
activate(current);
|
|
13715
|
+
break;
|
|
13716
|
+
case " ":
|
|
13717
|
+
e.preventDefault();
|
|
13718
|
+
if (!current.entry.disabled) selectEntry(current.entry);
|
|
13719
|
+
break;
|
|
13720
|
+
}
|
|
13721
|
+
};
|
|
13722
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
13723
|
+
"div",
|
|
13724
|
+
{
|
|
13725
|
+
role: "tree",
|
|
13726
|
+
"aria-label": ariaLabel,
|
|
13727
|
+
"aria-multiselectable": multiple || void 0,
|
|
13728
|
+
"data-react-fancy-file-browser-tree": "",
|
|
13729
|
+
className: cn("min-h-0 flex-1 overflow-auto p-1", className),
|
|
13730
|
+
onKeyDown: handleKeyDown,
|
|
13731
|
+
children: [
|
|
13732
|
+
rootEntries === void 0 && hasProvider && (rootStatus === "loading" || rootStatus === "idle") && /* @__PURE__ */ jsxRuntime.jsx(FileBrowserStatusRow, { depth: 0, kind: "loading" }),
|
|
13733
|
+
rootEntries === void 0 && rootStatus === "error" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
13734
|
+
FileBrowserStatusRow,
|
|
13735
|
+
{
|
|
13736
|
+
depth: 0,
|
|
13737
|
+
kind: "error",
|
|
13738
|
+
message: errorFor(path),
|
|
13739
|
+
onRetry: () => loadPath(path, { reload: true })
|
|
13740
|
+
}
|
|
13741
|
+
),
|
|
13742
|
+
rootEntries === void 0 && rootStatus === "idle" && !hasProvider && /* @__PURE__ */ jsxRuntime.jsx(FileBrowserStatusRow, { depth: 0, kind: "unknown" }),
|
|
13743
|
+
rootEntries !== void 0 && (rootChildren.length > 0 ? rootChildren.map((entry) => /* @__PURE__ */ jsxRuntime.jsx(FileBrowserNode, { entry, depth: 0 }, entry.path)) : /* @__PURE__ */ jsxRuntime.jsx(FileBrowserStatusRow, { depth: 0, kind: rootEntries.length === 0 ? "empty" : "no-matches" }))
|
|
13744
|
+
]
|
|
13745
|
+
}
|
|
13746
|
+
);
|
|
13747
|
+
}
|
|
13748
|
+
FileBrowserTree.displayName = "FileBrowserTree";
|
|
13749
|
+
var DEFAULT_SORT2 = { by: "name", direction: "asc" };
|
|
13750
|
+
function snapshotNodeToEntry(node) {
|
|
13751
|
+
const { children: _children, ...entry } = node;
|
|
13752
|
+
return entry;
|
|
13753
|
+
}
|
|
13754
|
+
function buildSnapshotMap(snapshot) {
|
|
13755
|
+
const map = /* @__PURE__ */ new Map();
|
|
13756
|
+
if (!snapshot || snapshot.length === 0) return map;
|
|
13757
|
+
for (const root of snapshot) {
|
|
13758
|
+
const parent = parentPath(root.path);
|
|
13759
|
+
const list = map.get(parent);
|
|
13760
|
+
if (list) list.push(snapshotNodeToEntry(root));
|
|
13761
|
+
else map.set(parent, [snapshotNodeToEntry(root)]);
|
|
13762
|
+
}
|
|
13763
|
+
const walk2 = (nodes) => {
|
|
13764
|
+
for (const node of nodes) {
|
|
13765
|
+
if (node.children) {
|
|
13766
|
+
map.set(node.path, node.children.map(snapshotNodeToEntry));
|
|
13767
|
+
walk2(node.children);
|
|
13768
|
+
}
|
|
13769
|
+
}
|
|
13770
|
+
};
|
|
13771
|
+
walk2(snapshot);
|
|
13772
|
+
return map;
|
|
13773
|
+
}
|
|
13774
|
+
function FileBrowserRoot({
|
|
13775
|
+
provider,
|
|
13776
|
+
snapshot,
|
|
13777
|
+
select = "file",
|
|
13778
|
+
multiple = false,
|
|
13779
|
+
value,
|
|
13780
|
+
defaultValue,
|
|
13781
|
+
onChange,
|
|
13782
|
+
path,
|
|
13783
|
+
defaultPath = "/",
|
|
13784
|
+
onPathChange,
|
|
13785
|
+
expandedPaths,
|
|
13786
|
+
defaultExpandedPaths,
|
|
13787
|
+
onExpandedChange,
|
|
13788
|
+
sort,
|
|
13789
|
+
defaultSort,
|
|
13790
|
+
onSortChange,
|
|
13791
|
+
filter,
|
|
13792
|
+
defaultFilter: defaultFilter2,
|
|
13793
|
+
onFilterChange,
|
|
13794
|
+
onError,
|
|
13795
|
+
indentSize = 16,
|
|
13796
|
+
showIcons = true,
|
|
13797
|
+
className,
|
|
13798
|
+
children
|
|
13799
|
+
}) {
|
|
13800
|
+
const [currentPath, setCurrentPath] = useControllableState(path, defaultPath, onPathChange);
|
|
13801
|
+
const [expanded, setExpanded] = useControllableState(
|
|
13802
|
+
expandedPaths,
|
|
13803
|
+
defaultExpandedPaths ?? [],
|
|
13804
|
+
onExpandedChange
|
|
13805
|
+
);
|
|
13806
|
+
const [sortState, setSortState] = useControllableState(sort, defaultSort ?? DEFAULT_SORT2, onSortChange);
|
|
13807
|
+
const [filterState, setFilterState] = useControllableState(filter, defaultFilter2 ?? "", onFilterChange);
|
|
13808
|
+
const [internalValue, setInternalValue] = react.useState(
|
|
13809
|
+
() => defaultValue ?? (multiple ? [] : null)
|
|
13810
|
+
);
|
|
13811
|
+
const selection = value !== void 0 ? value : internalValue;
|
|
13812
|
+
const selectedPaths = react.useMemo(
|
|
13813
|
+
() => selection == null ? [] : Array.isArray(selection) ? selection : [selection],
|
|
13814
|
+
[selection]
|
|
13815
|
+
);
|
|
13816
|
+
const snapshotMap = react.useMemo(() => buildSnapshotMap(snapshot), [snapshot]);
|
|
13817
|
+
const [loadedChildren, setLoadedChildren] = react.useState({});
|
|
13818
|
+
const [loadStatus, setLoadStatus] = react.useState({});
|
|
13819
|
+
const [loadErrors, setLoadErrors] = react.useState({});
|
|
13820
|
+
const seqCounter = react.useRef(0);
|
|
13821
|
+
const requestSeq = react.useRef({});
|
|
13822
|
+
const stateRef = react.useRef({ snapshotMap, loadedChildren, loadStatus });
|
|
13823
|
+
stateRef.current = { snapshotMap, loadedChildren, loadStatus };
|
|
13824
|
+
const providerRef = react.useRef(provider);
|
|
13825
|
+
providerRef.current = provider;
|
|
13826
|
+
const onErrorRef = react.useRef(onError);
|
|
13827
|
+
onErrorRef.current = onError;
|
|
13828
|
+
const onChangeRef = react.useRef(onChange);
|
|
13829
|
+
onChangeRef.current = onChange;
|
|
13830
|
+
const loadPath = react.useCallback((targetPath, options) => {
|
|
13831
|
+
const prov = providerRef.current;
|
|
13832
|
+
if (!prov) return;
|
|
13833
|
+
const { snapshotMap: snap, loadedChildren: loaded, loadStatus: statuses } = stateRef.current;
|
|
13834
|
+
const status = statuses[targetPath] ?? "idle";
|
|
13835
|
+
if (status === "loading") return;
|
|
13836
|
+
const known = snap.get(targetPath) ?? loaded[targetPath];
|
|
13837
|
+
if (!options?.reload && (known !== void 0 || status === "error")) return;
|
|
13838
|
+
const requestId = ++seqCounter.current;
|
|
13839
|
+
requestSeq.current[targetPath] = requestId;
|
|
13840
|
+
setLoadStatus((prev) => ({ ...prev, [targetPath]: "loading" }));
|
|
13841
|
+
prov.loadChildren(targetPath).then(
|
|
13842
|
+
(entries) => {
|
|
13843
|
+
if (requestSeq.current[targetPath] !== requestId) return;
|
|
13844
|
+
setLoadedChildren((prev) => ({ ...prev, [targetPath]: entries }));
|
|
13845
|
+
setLoadStatus((prev) => ({ ...prev, [targetPath]: "loaded" }));
|
|
13846
|
+
setLoadErrors((prev) => {
|
|
13847
|
+
if (!(targetPath in prev)) return prev;
|
|
13848
|
+
const next = { ...prev };
|
|
13849
|
+
delete next[targetPath];
|
|
13850
|
+
return next;
|
|
13851
|
+
});
|
|
13852
|
+
},
|
|
13853
|
+
(error) => {
|
|
13854
|
+
if (requestSeq.current[targetPath] !== requestId) return;
|
|
13855
|
+
setLoadStatus((prev) => ({ ...prev, [targetPath]: "error" }));
|
|
13856
|
+
setLoadErrors((prev) => ({
|
|
13857
|
+
...prev,
|
|
13858
|
+
[targetPath]: error instanceof Error ? error.message : String(error)
|
|
13859
|
+
}));
|
|
13860
|
+
onErrorRef.current?.(targetPath, error);
|
|
13861
|
+
}
|
|
13862
|
+
);
|
|
13863
|
+
}, []);
|
|
13864
|
+
const entriesFor = react.useCallback(
|
|
13865
|
+
(p) => snapshotMap.get(p) ?? loadedChildren[p],
|
|
13866
|
+
[snapshotMap, loadedChildren]
|
|
13867
|
+
);
|
|
13868
|
+
const statusFor = react.useCallback(
|
|
13869
|
+
(p) => {
|
|
13870
|
+
if (snapshotMap.has(p)) return "loaded";
|
|
13871
|
+
const status = loadStatus[p];
|
|
13872
|
+
if (status) return status;
|
|
13873
|
+
return loadedChildren[p] !== void 0 ? "loaded" : "idle";
|
|
13874
|
+
},
|
|
13875
|
+
[snapshotMap, loadStatus, loadedChildren]
|
|
13876
|
+
);
|
|
13877
|
+
const errorFor = react.useCallback((p) => loadErrors[p], [loadErrors]);
|
|
13878
|
+
react.useEffect(() => {
|
|
13879
|
+
if (!provider) return;
|
|
13880
|
+
loadPath(currentPath);
|
|
13881
|
+
for (const p of expanded) loadPath(p);
|
|
13882
|
+
}, [provider, currentPath, expanded, snapshotMap, loadPath]);
|
|
13883
|
+
const filterLower = filterState.trim().toLowerCase();
|
|
13884
|
+
const visibleChildrenFor = react.useCallback(
|
|
13885
|
+
(p) => {
|
|
13886
|
+
const entries = entriesFor(p);
|
|
13887
|
+
if (!entries) return [];
|
|
13888
|
+
const filtered = filterLower ? entries.filter((entry) => entryMatchesFilter(entry, filterLower, entriesFor)) : entries.slice();
|
|
13889
|
+
return filtered.sort((a, b) => compareFileEntries(a, b, sortState));
|
|
13890
|
+
},
|
|
13891
|
+
[entriesFor, filterLower, sortState]
|
|
13892
|
+
);
|
|
13893
|
+
const visibleRows = react.useMemo(() => {
|
|
13894
|
+
const rows = [];
|
|
13895
|
+
const visited = /* @__PURE__ */ new Set([currentPath]);
|
|
13896
|
+
const walk2 = (p, depth, parent) => {
|
|
13897
|
+
for (const entry of visibleChildrenFor(p)) {
|
|
13898
|
+
const expandable = isEntryExpandable(entry, entriesFor(entry.path), !!provider);
|
|
13899
|
+
const isOpen = expandable && expanded.includes(entry.path);
|
|
13900
|
+
rows.push({ entry, depth, expandable, expanded: isOpen, parentPath: parent });
|
|
13901
|
+
if (isOpen && !visited.has(entry.path)) {
|
|
13902
|
+
visited.add(entry.path);
|
|
13903
|
+
walk2(entry.path, depth + 1, entry.path);
|
|
13904
|
+
}
|
|
13905
|
+
}
|
|
13906
|
+
};
|
|
13907
|
+
walk2(currentPath, 0, null);
|
|
13908
|
+
return rows;
|
|
13909
|
+
}, [visibleChildrenFor, entriesFor, provider, expanded, currentPath]);
|
|
13910
|
+
const entryIndex = react.useMemo(() => {
|
|
13911
|
+
const map = /* @__PURE__ */ new Map();
|
|
13912
|
+
for (const list of snapshotMap.values()) {
|
|
13913
|
+
for (const entry of list) map.set(entry.path, entry);
|
|
13914
|
+
}
|
|
13915
|
+
for (const list of Object.values(loadedChildren)) {
|
|
13916
|
+
for (const entry of list) {
|
|
13917
|
+
if (!map.has(entry.path)) map.set(entry.path, entry);
|
|
13918
|
+
}
|
|
13919
|
+
}
|
|
13920
|
+
return map;
|
|
13921
|
+
}, [snapshotMap, loadedChildren]);
|
|
13922
|
+
const isSelectable = react.useCallback(
|
|
13923
|
+
(entry) => !entry.disabled && (select === "both" || (select === "file" ? entry.kind === "file" : entry.kind === "dir")),
|
|
13924
|
+
[select]
|
|
13925
|
+
);
|
|
13926
|
+
const isSelected = react.useCallback((p) => selectedPaths.includes(p), [selectedPaths]);
|
|
13927
|
+
const commitSelection = react.useCallback(
|
|
13928
|
+
(paths) => {
|
|
13929
|
+
const nextValue = multiple ? paths : paths[0] ?? null;
|
|
13930
|
+
if (value === void 0) setInternalValue(nextValue);
|
|
13931
|
+
onChangeRef.current?.(
|
|
13932
|
+
nextValue,
|
|
13933
|
+
paths.map((p) => entryIndex.get(p)).filter((entry) => entry !== void 0)
|
|
13934
|
+
);
|
|
13935
|
+
},
|
|
13936
|
+
[multiple, value, entryIndex]
|
|
13937
|
+
);
|
|
13938
|
+
const selectEntry = react.useCallback(
|
|
13939
|
+
(entry) => {
|
|
13940
|
+
if (!isSelectable(entry)) return;
|
|
13941
|
+
if (multiple) {
|
|
13942
|
+
commitSelection(
|
|
13943
|
+
selectedPaths.includes(entry.path) ? selectedPaths.filter((p) => p !== entry.path) : [...selectedPaths, entry.path]
|
|
13944
|
+
);
|
|
13945
|
+
} else {
|
|
13946
|
+
commitSelection([entry.path]);
|
|
13947
|
+
}
|
|
13948
|
+
},
|
|
13949
|
+
[isSelectable, multiple, selectedPaths, commitSelection]
|
|
13950
|
+
);
|
|
13951
|
+
const toggleExpanded = react.useCallback(
|
|
13952
|
+
(p) => {
|
|
13953
|
+
setExpanded((prev) => prev.includes(p) ? prev.filter((x) => x !== p) : [...prev, p]);
|
|
13954
|
+
},
|
|
13955
|
+
[setExpanded]
|
|
13956
|
+
);
|
|
13957
|
+
const [focusedPath, setFocusedPath] = react.useState(null);
|
|
13958
|
+
const navigate = react.useCallback(
|
|
13959
|
+
(p) => {
|
|
13960
|
+
setCurrentPath(p);
|
|
13961
|
+
setFilterState("");
|
|
13962
|
+
setFocusedPath(null);
|
|
13963
|
+
},
|
|
13964
|
+
[setCurrentPath, setFilterState]
|
|
13965
|
+
);
|
|
13966
|
+
const rowRefs = react.useRef(/* @__PURE__ */ new Map());
|
|
13967
|
+
const registerRow = react.useCallback((p, el) => {
|
|
13968
|
+
if (el) rowRefs.current.set(p, el);
|
|
13969
|
+
else rowRefs.current.delete(p);
|
|
13970
|
+
}, []);
|
|
13971
|
+
const focusRow = react.useCallback((p) => {
|
|
13972
|
+
rowRefs.current.get(p)?.focus();
|
|
13973
|
+
}, []);
|
|
13974
|
+
const tabFocusPath = react.useMemo(() => {
|
|
13975
|
+
if (focusedPath && visibleRows.some((row) => row.entry.path === focusedPath)) {
|
|
13976
|
+
return focusedPath;
|
|
13977
|
+
}
|
|
13978
|
+
return visibleRows[0]?.entry.path ?? null;
|
|
13979
|
+
}, [focusedPath, visibleRows]);
|
|
13980
|
+
const ctx = react.useMemo(
|
|
13981
|
+
() => ({
|
|
13982
|
+
entriesFor,
|
|
13983
|
+
visibleChildrenFor,
|
|
13984
|
+
statusFor,
|
|
13985
|
+
errorFor,
|
|
13986
|
+
loadPath,
|
|
13987
|
+
hasProvider: !!provider,
|
|
13988
|
+
path: currentPath,
|
|
13989
|
+
navigate,
|
|
13990
|
+
expandedPaths: expanded,
|
|
13991
|
+
toggleExpanded,
|
|
13992
|
+
select,
|
|
13993
|
+
multiple,
|
|
13994
|
+
selectedPaths,
|
|
13995
|
+
isSelected,
|
|
13996
|
+
isSelectable,
|
|
13997
|
+
selectEntry,
|
|
13998
|
+
sort: sortState,
|
|
13999
|
+
setSort: setSortState,
|
|
14000
|
+
filter: filterState,
|
|
14001
|
+
setFilter: setFilterState,
|
|
14002
|
+
visibleRows,
|
|
14003
|
+
focusedPath,
|
|
14004
|
+
setFocusedPath,
|
|
14005
|
+
tabFocusPath,
|
|
14006
|
+
focusRow,
|
|
14007
|
+
registerRow,
|
|
14008
|
+
indentSize,
|
|
14009
|
+
showIcons
|
|
14010
|
+
}),
|
|
14011
|
+
[
|
|
14012
|
+
entriesFor,
|
|
14013
|
+
visibleChildrenFor,
|
|
14014
|
+
statusFor,
|
|
14015
|
+
errorFor,
|
|
14016
|
+
loadPath,
|
|
14017
|
+
provider,
|
|
14018
|
+
currentPath,
|
|
14019
|
+
navigate,
|
|
14020
|
+
expanded,
|
|
14021
|
+
toggleExpanded,
|
|
14022
|
+
select,
|
|
14023
|
+
multiple,
|
|
14024
|
+
selectedPaths,
|
|
14025
|
+
isSelected,
|
|
14026
|
+
isSelectable,
|
|
14027
|
+
selectEntry,
|
|
14028
|
+
sortState,
|
|
14029
|
+
setSortState,
|
|
14030
|
+
filterState,
|
|
14031
|
+
setFilterState,
|
|
14032
|
+
visibleRows,
|
|
14033
|
+
focusedPath,
|
|
14034
|
+
tabFocusPath,
|
|
14035
|
+
focusRow,
|
|
14036
|
+
registerRow,
|
|
14037
|
+
indentSize,
|
|
14038
|
+
showIcons
|
|
14039
|
+
]
|
|
14040
|
+
);
|
|
14041
|
+
return /* @__PURE__ */ jsxRuntime.jsx(FileBrowserContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
14042
|
+
"div",
|
|
14043
|
+
{
|
|
14044
|
+
"data-react-fancy-file-browser": "",
|
|
14045
|
+
className: cn(
|
|
14046
|
+
"flex flex-col overflow-hidden rounded-lg border border-zinc-200 bg-white text-sm dark:border-zinc-700 dark:bg-zinc-900",
|
|
14047
|
+
className
|
|
14048
|
+
),
|
|
14049
|
+
children: children ?? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
14050
|
+
/* @__PURE__ */ jsxRuntime.jsx(FileBrowserPathBar, {}),
|
|
14051
|
+
/* @__PURE__ */ jsxRuntime.jsx(FileBrowserToolbar, {}),
|
|
14052
|
+
/* @__PURE__ */ jsxRuntime.jsx(FileBrowserTree, {})
|
|
14053
|
+
] })
|
|
14054
|
+
}
|
|
14055
|
+
) });
|
|
14056
|
+
}
|
|
14057
|
+
FileBrowserRoot.displayName = "FileBrowser";
|
|
14058
|
+
var FileBrowser = Object.assign(FileBrowserRoot, {
|
|
14059
|
+
PathBar: FileBrowserPathBar,
|
|
14060
|
+
Toolbar: FileBrowserToolbar,
|
|
14061
|
+
Tree: FileBrowserTree,
|
|
14062
|
+
Node: FileBrowserNode
|
|
14063
|
+
});
|
|
13145
14064
|
|
|
13146
14065
|
// src/utils/media-type.ts
|
|
13147
14066
|
var IMAGE_EXTS = /* @__PURE__ */ new Set([
|
|
@@ -14995,6 +15914,7 @@ exports.EmojiSelect = EmojiSelect;
|
|
|
14995
15914
|
exports.FauxClient = FauxClient;
|
|
14996
15915
|
exports.Field = Field;
|
|
14997
15916
|
exports.FieldModeContext = FieldModeContext;
|
|
15917
|
+
exports.FileBrowser = FileBrowser;
|
|
14998
15918
|
exports.FileUpload = FileUpload;
|
|
14999
15919
|
exports.Form = Form;
|
|
15000
15920
|
exports.FormProvider = FormProvider;
|
|
@@ -15075,6 +15995,7 @@ exports.useDropdown = useDropdown;
|
|
|
15075
15995
|
exports.useEditor = useEditor;
|
|
15076
15996
|
exports.useEscapeKey = useEscapeKey;
|
|
15077
15997
|
exports.useFieldMode = useFieldMode;
|
|
15998
|
+
exports.useFileBrowser = useFileBrowser;
|
|
15078
15999
|
exports.useFileUpload = useFileUpload;
|
|
15079
16000
|
exports.useFloatingPosition = useFloatingPosition;
|
|
15080
16001
|
exports.useFocusTrap = useFocusTrap;
|