@particle-academy/react-fancy 4.12.1 → 4.13.0

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