momoi-explorer 0.8.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/ui.js ADDED
@@ -0,0 +1,1347 @@
1
+ // src/ui/FileExplorer.tsx
2
+ import { useCallback as useCallback5, useEffect as useEffect5, useMemo as useMemo3 } from "react";
3
+ import { Virtuoso } from "react-virtuoso";
4
+
5
+ // src/react/TreeProvider.tsx
6
+ import { useEffect, useMemo, useRef, useState } from "react";
7
+
8
+ // src/core/event-processor.ts
9
+ var DEFAULT_DEBOUNCE_MS = 75;
10
+ var DEFAULT_THROTTLE_CHUNK_SIZE = 500;
11
+ var DEFAULT_THROTTLE_DELAY_MS = 200;
12
+ function coalesceEvents(raw) {
13
+ const expanded = [];
14
+ for (const event of raw) {
15
+ if (event.type === "rename" && event.newPath) {
16
+ expanded.push({ type: "delete", path: event.path, isDirectory: event.isDirectory });
17
+ expanded.push({ type: "create", path: event.newPath, isDirectory: event.isDirectory });
18
+ } else {
19
+ expanded.push({ type: event.type, path: event.path, isDirectory: event.isDirectory });
20
+ }
21
+ }
22
+ const byPath = /* @__PURE__ */ new Map();
23
+ for (const event of expanded) {
24
+ const existing = byPath.get(event.path);
25
+ if (!existing) {
26
+ byPath.set(event.path, event);
27
+ continue;
28
+ }
29
+ if (existing.type === "delete" && event.type === "create") {
30
+ byPath.set(event.path, { type: "modify", path: event.path, isDirectory: event.isDirectory });
31
+ continue;
32
+ }
33
+ if (existing.type === "create" && event.type === "modify") {
34
+ continue;
35
+ }
36
+ if (existing.type === "create" && event.type === "delete") {
37
+ byPath.delete(event.path);
38
+ continue;
39
+ }
40
+ byPath.set(event.path, event);
41
+ }
42
+ const result = Array.from(byPath.values());
43
+ const deletedDirs = /* @__PURE__ */ new Set();
44
+ for (const event of result) {
45
+ if (event.type === "delete" && event.isDirectory) {
46
+ deletedDirs.add(event.path);
47
+ }
48
+ }
49
+ if (deletedDirs.size === 0) return result;
50
+ return result.filter((event) => {
51
+ if (event.type !== "delete") return true;
52
+ for (const dir of deletedDirs) {
53
+ if (event.path !== dir && event.path.startsWith(dir + "/")) {
54
+ return false;
55
+ }
56
+ }
57
+ return true;
58
+ });
59
+ }
60
+ function createEventProcessor(callback, options = {}) {
61
+ const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
62
+ const shouldCoalesce = options.coalesce ?? true;
63
+ const throttleChunkSize = options.throttle?.maxChunkSize ?? DEFAULT_THROTTLE_CHUNK_SIZE;
64
+ const throttleDelayMs = options.throttle?.delayMs ?? DEFAULT_THROTTLE_DELAY_MS;
65
+ let buffer = [];
66
+ let debounceTimer = null;
67
+ let throttleTimer = null;
68
+ let destroyed = false;
69
+ function processBuffer() {
70
+ if (destroyed || buffer.length === 0) return;
71
+ const raw = buffer;
72
+ buffer = [];
73
+ const events = shouldCoalesce ? coalesceEvents(raw) : raw.map((e) => ({
74
+ type: e.type === "rename" ? "modify" : e.type,
75
+ path: e.type === "rename" && e.newPath ? e.newPath : e.path,
76
+ isDirectory: e.isDirectory
77
+ }));
78
+ if (events.length === 0) return;
79
+ if (events.length <= throttleChunkSize) {
80
+ callback(events);
81
+ return;
82
+ }
83
+ let offset = 0;
84
+ function emitChunk() {
85
+ if (destroyed || offset >= events.length) return;
86
+ const chunk = events.slice(offset, offset + throttleChunkSize);
87
+ offset += throttleChunkSize;
88
+ callback(chunk);
89
+ if (offset < events.length) {
90
+ throttleTimer = setTimeout(emitChunk, throttleDelayMs);
91
+ }
92
+ }
93
+ emitChunk();
94
+ }
95
+ return {
96
+ push(events) {
97
+ if (destroyed) return;
98
+ buffer.push(...events);
99
+ if (debounceTimer !== null) {
100
+ clearTimeout(debounceTimer);
101
+ }
102
+ debounceTimer = setTimeout(processBuffer, debounceMs);
103
+ },
104
+ flush() {
105
+ if (debounceTimer !== null) {
106
+ clearTimeout(debounceTimer);
107
+ debounceTimer = null;
108
+ }
109
+ processBuffer();
110
+ },
111
+ destroy() {
112
+ destroyed = true;
113
+ if (debounceTimer !== null) clearTimeout(debounceTimer);
114
+ if (throttleTimer !== null) clearTimeout(throttleTimer);
115
+ buffer = [];
116
+ }
117
+ };
118
+ }
119
+
120
+ // src/core/flatten.ts
121
+ function flattenTree(nodes, expandedPaths, matchingPaths) {
122
+ const result = [];
123
+ function walk(children, depth) {
124
+ for (const node of children) {
125
+ if (matchingPaths && !matchingPaths.has(node.path)) continue;
126
+ result.push({ node, depth });
127
+ if (node.isDirectory && node.children) {
128
+ if (matchingPaths || expandedPaths.has(node.path)) {
129
+ walk(node.children, depth + 1);
130
+ }
131
+ }
132
+ }
133
+ }
134
+ walk(nodes, 0);
135
+ return result;
136
+ }
137
+
138
+ // src/core/selection.ts
139
+ function computeSelection(currentSelected, anchorPath, targetPath, mode, flatList) {
140
+ switch (mode) {
141
+ case "replace":
142
+ return {
143
+ selectedPaths: /* @__PURE__ */ new Set([targetPath]),
144
+ anchorPath: targetPath
145
+ };
146
+ case "toggle": {
147
+ const next = new Set(currentSelected);
148
+ if (next.has(targetPath)) {
149
+ next.delete(targetPath);
150
+ } else {
151
+ next.add(targetPath);
152
+ }
153
+ return {
154
+ selectedPaths: next,
155
+ anchorPath: targetPath
156
+ };
157
+ }
158
+ case "range": {
159
+ if (!anchorPath) {
160
+ return {
161
+ selectedPaths: /* @__PURE__ */ new Set([targetPath]),
162
+ anchorPath: targetPath
163
+ };
164
+ }
165
+ const paths = flatList.map((f) => f.node.path);
166
+ const anchorIdx = paths.indexOf(anchorPath);
167
+ const targetIdx = paths.indexOf(targetPath);
168
+ if (anchorIdx === -1 || targetIdx === -1) {
169
+ return {
170
+ selectedPaths: /* @__PURE__ */ new Set([targetPath]),
171
+ anchorPath: targetPath
172
+ };
173
+ }
174
+ const start = Math.min(anchorIdx, targetIdx);
175
+ const end = Math.max(anchorIdx, targetIdx);
176
+ const rangePaths = new Set(paths.slice(start, end + 1));
177
+ return {
178
+ selectedPaths: rangePaths,
179
+ anchorPath
180
+ // range選択ではanchorは変えない
181
+ };
182
+ }
183
+ }
184
+ }
185
+
186
+ // src/core/sort.ts
187
+ function defaultSort(a, b) {
188
+ if (a.isDirectory !== b.isDirectory) {
189
+ return a.isDirectory ? -1 : 1;
190
+ }
191
+ return a.name.localeCompare(b.name, void 0, { sensitivity: "base" });
192
+ }
193
+
194
+ // src/core/filter.ts
195
+ function defaultFilter(_entry) {
196
+ return true;
197
+ }
198
+
199
+ // src/core/search.ts
200
+ function fuzzyMatch(query, target) {
201
+ const q = query.toLowerCase();
202
+ const t = target.toLowerCase();
203
+ if (q.length === 0) return { match: true, score: 0 };
204
+ if (q.length > t.length) return { match: false, score: 0 };
205
+ if (t === q) return { match: true, score: 100 };
206
+ if (t.startsWith(q)) return { match: true, score: 90 };
207
+ if (t.includes(q)) return { match: true, score: 80 };
208
+ let qi = 0;
209
+ let score = 0;
210
+ let lastMatchIndex = -2;
211
+ for (let ti = 0; ti < t.length && qi < q.length; ti++) {
212
+ if (t[ti] === q[qi]) {
213
+ qi++;
214
+ if (ti === lastMatchIndex + 1) {
215
+ score += 5;
216
+ }
217
+ if (ti === 0 || "/\\-_.".includes(t[ti - 1])) {
218
+ score += 10;
219
+ }
220
+ score += 1;
221
+ lastMatchIndex = ti;
222
+ }
223
+ }
224
+ if (qi < q.length) return { match: false, score: 0 };
225
+ return { match: true, score };
226
+ }
227
+ function findMatchingPaths(nodes, query) {
228
+ const matching = /* @__PURE__ */ new Set();
229
+ function walk(node, ancestors) {
230
+ const nameMatch = fuzzyMatch(query, node.name).match;
231
+ let childMatch = false;
232
+ if (node.children) {
233
+ for (const child of node.children) {
234
+ if (walk(child, [...ancestors, node.path])) {
235
+ childMatch = true;
236
+ }
237
+ }
238
+ }
239
+ if (nameMatch || childMatch) {
240
+ matching.add(node.path);
241
+ for (const a of ancestors) {
242
+ matching.add(a);
243
+ }
244
+ return true;
245
+ }
246
+ return false;
247
+ }
248
+ for (const node of nodes) {
249
+ walk(node, []);
250
+ }
251
+ return matching;
252
+ }
253
+ function fuzzyFind(files, query, maxResults = 50) {
254
+ if (!query) return [];
255
+ const scored = [];
256
+ for (const entry of files) {
257
+ const nameResult = fuzzyMatch(query, entry.name);
258
+ const pathResult = fuzzyMatch(query, entry.path);
259
+ const bestScore = Math.max(nameResult.score, pathResult.score * 0.5);
260
+ if (nameResult.match || pathResult.match) {
261
+ scored.push({ entry, score: bestScore });
262
+ }
263
+ }
264
+ scored.sort((a, b) => b.score - a.score);
265
+ return scored.slice(0, maxResults).map((s) => s.entry);
266
+ }
267
+
268
+ // src/core/tree.ts
269
+ function toTreeNode(entry, depth) {
270
+ return {
271
+ ...entry,
272
+ depth,
273
+ children: entry.isDirectory ? void 0 : void 0,
274
+ childrenLoaded: false
275
+ };
276
+ }
277
+ function findNode(nodes, path) {
278
+ for (const node of nodes) {
279
+ if (node.path === path) return node;
280
+ if (node.children) {
281
+ const found = findNode(node.children, path);
282
+ if (found) return found;
283
+ }
284
+ }
285
+ return void 0;
286
+ }
287
+ function dirname(path) {
288
+ const sep = path.includes("\\") ? "\\" : "/";
289
+ const idx = path.lastIndexOf(sep);
290
+ return idx === -1 ? "" : path.slice(0, idx);
291
+ }
292
+ function createFileTree(options) {
293
+ const { adapter, rootPath, onEvent } = options;
294
+ let sortFn = options.sort ?? defaultSort;
295
+ let filterFn = options.filter ?? defaultFilter;
296
+ let state = {
297
+ rootPath,
298
+ rootNodes: [],
299
+ expandedPaths: /* @__PURE__ */ new Set(),
300
+ selectedPaths: /* @__PURE__ */ new Set(),
301
+ anchorPath: null,
302
+ renamingPath: null,
303
+ creatingState: null,
304
+ searchQuery: null,
305
+ flatList: []
306
+ };
307
+ const listeners = /* @__PURE__ */ new Set();
308
+ let expandingPaths = /* @__PURE__ */ new Set();
309
+ function emit(event) {
310
+ onEvent?.(event);
311
+ }
312
+ function notify() {
313
+ const matchingPaths = state.searchQuery ? findMatchingPaths(state.rootNodes, state.searchQuery) : null;
314
+ state = { ...state, flatList: flattenTree(state.rootNodes, state.expandedPaths, matchingPaths) };
315
+ for (const listener of listeners) {
316
+ listener(state);
317
+ }
318
+ }
319
+ async function loadChildren(node) {
320
+ const entries = await adapter.readDir(node.path);
321
+ const filtered = entries.filter(filterFn);
322
+ filtered.sort(sortFn);
323
+ const oldChildMap = node.children ? new Map(node.children.map((c) => [c.path, c])) : /* @__PURE__ */ new Map();
324
+ node.children = filtered.map((e) => {
325
+ const existing = oldChildMap.get(e.path);
326
+ if (existing && existing.childrenLoaded) {
327
+ return { ...toTreeNode(e, node.depth + 1), children: existing.children, childrenLoaded: true };
328
+ }
329
+ return toTreeNode(e, node.depth + 1);
330
+ });
331
+ node.childrenLoaded = true;
332
+ }
333
+ async function refreshParent(parentPath) {
334
+ const parentNode = findNode(state.rootNodes, parentPath);
335
+ if (parentNode) {
336
+ await loadChildren(parentNode);
337
+ state.expandedPaths = new Set(state.expandedPaths);
338
+ state.expandedPaths.add(parentPath);
339
+ } else if (parentPath === rootPath) {
340
+ const entries = await adapter.readDir(rootPath);
341
+ const filtered = entries.filter(filterFn);
342
+ filtered.sort(sortFn);
343
+ const oldNodeMap = new Map(state.rootNodes.map((n) => [n.path, n]));
344
+ state.rootNodes = filtered.map((e) => {
345
+ const existing = oldNodeMap.get(e.path);
346
+ if (existing && existing.childrenLoaded) {
347
+ return { ...toTreeNode(e, 0), children: existing.children, childrenLoaded: true };
348
+ }
349
+ return toTreeNode(e, 0);
350
+ });
351
+ }
352
+ }
353
+ function sortNodes(nodes) {
354
+ nodes.sort(sortFn);
355
+ for (const node of nodes) {
356
+ if (node.children) {
357
+ sortNodes(node.children);
358
+ }
359
+ }
360
+ }
361
+ function filterNodes(nodes) {
362
+ return nodes.filter((node) => {
363
+ if (!filterFn(node)) return false;
364
+ if (node.children) {
365
+ node.children = filterNodes(node.children);
366
+ }
367
+ return true;
368
+ });
369
+ }
370
+ let unwatchFn = null;
371
+ let eventProcessor = null;
372
+ function handleWatchEvents(events) {
373
+ emit({ type: "external-change", changes: events });
374
+ const dirsToRefresh = /* @__PURE__ */ new Set();
375
+ for (const event of events) {
376
+ const parent = dirname(event.path);
377
+ if (parent === rootPath || state.expandedPaths.has(parent)) {
378
+ dirsToRefresh.add(parent);
379
+ }
380
+ if (event.isDirectory && state.expandedPaths.has(event.path)) {
381
+ dirsToRefresh.add(event.path);
382
+ }
383
+ }
384
+ for (const dir of dirsToRefresh) {
385
+ refreshParent(dir).then(() => notify()).catch(() => {
386
+ });
387
+ }
388
+ }
389
+ function startWatching() {
390
+ if (!adapter.watch) return;
391
+ eventProcessor = createEventProcessor(handleWatchEvents, options.watchOptions);
392
+ unwatchFn = adapter.watch(rootPath, (events) => {
393
+ eventProcessor.push(events);
394
+ });
395
+ }
396
+ function stopWatching() {
397
+ if (unwatchFn) {
398
+ unwatchFn();
399
+ unwatchFn = null;
400
+ }
401
+ if (eventProcessor) {
402
+ eventProcessor.destroy();
403
+ eventProcessor = null;
404
+ }
405
+ }
406
+ const controller = {
407
+ getState() {
408
+ return state;
409
+ },
410
+ subscribe(listener) {
411
+ listeners.add(listener);
412
+ return () => listeners.delete(listener);
413
+ },
414
+ async loadRoot() {
415
+ const entries = await adapter.readDir(rootPath);
416
+ const filtered = entries.filter(filterFn);
417
+ filtered.sort(sortFn);
418
+ state.rootNodes = filtered.map((e) => toTreeNode(e, 0));
419
+ notify();
420
+ startWatching();
421
+ },
422
+ async expand(path) {
423
+ if (expandingPaths.has(path)) return;
424
+ expandingPaths.add(path);
425
+ try {
426
+ const node = findNode(state.rootNodes, path);
427
+ if (!node || !node.isDirectory) return;
428
+ if (!node.childrenLoaded) {
429
+ await loadChildren(node);
430
+ }
431
+ state.expandedPaths = new Set(state.expandedPaths);
432
+ state.expandedPaths.add(path);
433
+ notify();
434
+ emit({ type: "expand", path });
435
+ } finally {
436
+ expandingPaths.delete(path);
437
+ }
438
+ },
439
+ collapse(path) {
440
+ const sep = path.includes("\\") ? "\\" : "/";
441
+ const prefix = path + sep;
442
+ state.expandedPaths = new Set(state.expandedPaths);
443
+ state.expandedPaths.delete(path);
444
+ for (const p of state.expandedPaths) {
445
+ if (p.startsWith(prefix)) {
446
+ state.expandedPaths.delete(p);
447
+ }
448
+ }
449
+ notify();
450
+ emit({ type: "collapse", path });
451
+ },
452
+ async toggleExpand(path) {
453
+ if (expandingPaths.has(path)) return;
454
+ if (state.expandedPaths.has(path)) {
455
+ controller.collapse(path);
456
+ } else {
457
+ await controller.expand(path);
458
+ }
459
+ },
460
+ async expandTo(path) {
461
+ const parts = [];
462
+ let current = path;
463
+ while (current !== rootPath && current !== "") {
464
+ const parent = dirname(current);
465
+ if (parent === current) break;
466
+ parts.unshift(parent);
467
+ current = parent;
468
+ }
469
+ for (const ancestorPath of parts) {
470
+ if (ancestorPath === rootPath) continue;
471
+ if (!state.expandedPaths.has(ancestorPath)) {
472
+ await controller.expand(ancestorPath);
473
+ }
474
+ }
475
+ },
476
+ select(path, mode = "replace") {
477
+ const result = computeSelection(
478
+ state.selectedPaths,
479
+ state.anchorPath,
480
+ path,
481
+ mode,
482
+ state.flatList
483
+ );
484
+ state.selectedPaths = result.selectedPaths;
485
+ state.anchorPath = result.anchorPath;
486
+ notify();
487
+ emit({ type: "select", paths: Array.from(result.selectedPaths) });
488
+ },
489
+ selectAll() {
490
+ state.selectedPaths = new Set(state.flatList.map((f) => f.node.path));
491
+ notify();
492
+ emit({ type: "select", paths: Array.from(state.selectedPaths) });
493
+ },
494
+ clearSelection() {
495
+ state.selectedPaths = /* @__PURE__ */ new Set();
496
+ state.anchorPath = null;
497
+ notify();
498
+ emit({ type: "select", paths: [] });
499
+ },
500
+ startRename(path) {
501
+ state.renamingPath = path;
502
+ notify();
503
+ },
504
+ async commitRename(newName) {
505
+ if (!state.renamingPath || !adapter.rename) return;
506
+ const oldPath = state.renamingPath;
507
+ const parent = dirname(oldPath);
508
+ const sep = oldPath.includes("\\") ? "\\" : "/";
509
+ const newPath = parent + sep + newName;
510
+ await adapter.rename(oldPath, newPath);
511
+ state.renamingPath = null;
512
+ if (parent === rootPath) {
513
+ await controller.loadRoot();
514
+ } else {
515
+ const parentNode = findNode(state.rootNodes, parent);
516
+ if (parentNode) {
517
+ await loadChildren(parentNode);
518
+ }
519
+ }
520
+ notify();
521
+ emit({ type: "rename", oldPath, newPath });
522
+ },
523
+ cancelRename() {
524
+ state.renamingPath = null;
525
+ notify();
526
+ },
527
+ async startCreate(parentPath, isDirectory, insertAfterPath) {
528
+ if (parentPath !== rootPath && !state.expandedPaths.has(parentPath)) {
529
+ await controller.expand(parentPath);
530
+ }
531
+ state.creatingState = { parentPath, isDirectory, insertAfterPath };
532
+ notify();
533
+ },
534
+ async commitCreate(name) {
535
+ if (!state.creatingState) return;
536
+ const { parentPath, isDirectory } = state.creatingState;
537
+ state.creatingState = null;
538
+ if (isDirectory) {
539
+ await controller.createDir(parentPath, name);
540
+ } else {
541
+ await controller.createFile(parentPath, name);
542
+ }
543
+ },
544
+ cancelCreate() {
545
+ state.creatingState = null;
546
+ notify();
547
+ },
548
+ async createFile(parentPath, name) {
549
+ if (!adapter.createFile) return;
550
+ await adapter.createFile(parentPath, name);
551
+ await refreshParent(parentPath);
552
+ notify();
553
+ emit({ type: "create", parentPath, name, isDirectory: false });
554
+ },
555
+ async createDir(parentPath, name) {
556
+ if (!adapter.createDir) return;
557
+ await adapter.createDir(parentPath, name);
558
+ await refreshParent(parentPath);
559
+ notify();
560
+ emit({ type: "create", parentPath, name, isDirectory: true });
561
+ },
562
+ async deleteSelected() {
563
+ if (!adapter.delete || state.selectedPaths.size === 0) return;
564
+ const paths = Array.from(state.selectedPaths);
565
+ await adapter.delete(paths);
566
+ state.selectedPaths = /* @__PURE__ */ new Set();
567
+ state.anchorPath = null;
568
+ const parentDirs = new Set(paths.map(dirname));
569
+ for (const dir of parentDirs) {
570
+ await refreshParent(dir);
571
+ }
572
+ notify();
573
+ emit({ type: "delete", paths });
574
+ },
575
+ async refresh(path) {
576
+ if (!path || path === rootPath) {
577
+ await refreshParent(rootPath);
578
+ } else {
579
+ const node = findNode(state.rootNodes, path);
580
+ if (node && node.isDirectory) {
581
+ await loadChildren(node);
582
+ }
583
+ }
584
+ notify();
585
+ emit({ type: "refresh", path });
586
+ },
587
+ setSearchQuery(query) {
588
+ state.searchQuery = query && query.trim() ? query.trim() : null;
589
+ notify();
590
+ },
591
+ async collectAllFiles() {
592
+ const result = [];
593
+ async function walk(dirPath) {
594
+ const entries = await adapter.readDir(dirPath);
595
+ for (const entry of entries) {
596
+ if (!filterFn(entry)) continue;
597
+ result.push(entry);
598
+ if (entry.isDirectory) {
599
+ await walk(entry.path);
600
+ }
601
+ }
602
+ }
603
+ await walk(rootPath);
604
+ return result;
605
+ },
606
+ setFilter(fn) {
607
+ filterFn = fn ?? defaultFilter;
608
+ state.rootNodes = filterNodes(state.rootNodes);
609
+ notify();
610
+ },
611
+ setSort(fn) {
612
+ sortFn = fn ?? defaultSort;
613
+ sortNodes(state.rootNodes);
614
+ notify();
615
+ },
616
+ destroy() {
617
+ stopWatching();
618
+ listeners.clear();
619
+ }
620
+ };
621
+ return controller;
622
+ }
623
+
624
+ // src/react/context.ts
625
+ import { createContext, useContext } from "react";
626
+ var TreeContext = createContext(null);
627
+ function useTreeContext() {
628
+ const ctx = useContext(TreeContext);
629
+ if (!ctx) {
630
+ throw new Error("useTreeContext must be used within a <TreeProvider>");
631
+ }
632
+ return ctx;
633
+ }
634
+
635
+ // src/react/TreeProvider.tsx
636
+ import { jsx } from "react/jsx-runtime";
637
+ function TreeProvider({
638
+ adapter,
639
+ rootPath,
640
+ sort,
641
+ filter,
642
+ watchOptions,
643
+ onEvent,
644
+ children
645
+ }) {
646
+ const onEventRef = useRef(onEvent);
647
+ onEventRef.current = onEvent;
648
+ const controller = useMemo(() => {
649
+ return createFileTree({
650
+ adapter,
651
+ rootPath,
652
+ sort,
653
+ filter,
654
+ watchOptions,
655
+ onEvent: (event) => onEventRef.current?.(event)
656
+ });
657
+ }, [adapter, rootPath]);
658
+ const [state, setState] = useState(() => controller.getState());
659
+ useEffect(() => {
660
+ const unsub = controller.subscribe(setState);
661
+ controller.loadRoot();
662
+ return () => {
663
+ unsub();
664
+ controller.destroy();
665
+ };
666
+ }, [controller]);
667
+ const value = useMemo(() => ({ controller, state }), [controller, state]);
668
+ return /* @__PURE__ */ jsx(TreeContext.Provider, { value, children });
669
+ }
670
+
671
+ // src/react/useFileTree.ts
672
+ function useFileTree() {
673
+ const { controller, state } = useTreeContext();
674
+ return { ...state, controller };
675
+ }
676
+
677
+ // src/react/useContextMenu.ts
678
+ import { useCallback, useState as useState2 } from "react";
679
+ function useContextMenu() {
680
+ const [menuState, setMenuState] = useState2({
681
+ isVisible: false,
682
+ x: 0,
683
+ y: 0,
684
+ targetPath: null
685
+ });
686
+ const show = useCallback((e, targetPath) => {
687
+ e.preventDefault();
688
+ e.stopPropagation();
689
+ setMenuState({
690
+ isVisible: true,
691
+ x: e.clientX,
692
+ y: e.clientY,
693
+ targetPath
694
+ });
695
+ }, []);
696
+ const hide = useCallback(() => {
697
+ setMenuState((prev) => ({ ...prev, isVisible: false, targetPath: null }));
698
+ }, []);
699
+ return { ...menuState, show, hide };
700
+ }
701
+
702
+ // src/react/useExplorerKeybindings.ts
703
+ import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2 } from "react";
704
+
705
+ // src/core/keybindings.ts
706
+ var ExplorerCommands = {
707
+ DELETE: "explorer.delete",
708
+ RENAME: "explorer.rename",
709
+ NEW_FILE: "explorer.newFile",
710
+ NEW_FOLDER: "explorer.newFolder",
711
+ REFRESH: "explorer.refresh",
712
+ COLLAPSE_ALL: "explorer.collapseAll",
713
+ SELECT_ALL: "explorer.selectAll",
714
+ COPY_PATH: "explorer.copyPath"
715
+ };
716
+ var defaultExplorerKeybindings = [
717
+ { key: "Delete", command: ExplorerCommands.DELETE, when: "explorerFocus" },
718
+ { key: "F2", command: ExplorerCommands.RENAME, when: "explorerFocus" },
719
+ { key: "Ctrl+N", command: ExplorerCommands.NEW_FILE, when: "explorerFocus" },
720
+ { key: "Ctrl+Shift+N", command: ExplorerCommands.NEW_FOLDER, when: "explorerFocus" },
721
+ { key: "Ctrl+R", command: ExplorerCommands.REFRESH, when: "explorerFocus" },
722
+ { key: "Ctrl+Shift+E", command: ExplorerCommands.COLLAPSE_ALL, when: "explorerFocus" },
723
+ { key: "Ctrl+A", command: ExplorerCommands.SELECT_ALL, when: "explorerFocus" },
724
+ { key: "Ctrl+Shift+C", command: ExplorerCommands.COPY_PATH, when: "explorerFocus" }
725
+ ];
726
+
727
+ // src/react/useExplorerKeybindings.ts
728
+ function useExplorerKeybindings(inputService, options) {
729
+ const { controller, state } = useTreeContext();
730
+ const stateRef = useRef2(state);
731
+ stateRef.current = state;
732
+ const optionsRef = useRef2(options);
733
+ optionsRef.current = options;
734
+ const getCreateTarget = useCallback2(() => {
735
+ const s = stateRef.current;
736
+ if (s.selectedPaths.size === 0) return { parentPath: s.rootPath };
737
+ const firstSelected = s.flatList.find((f) => s.selectedPaths.has(f.node.path));
738
+ if (!firstSelected) return { parentPath: s.rootPath };
739
+ if (firstSelected.node.isDirectory) return { parentPath: firstSelected.node.path };
740
+ const sep = firstSelected.node.path.includes("\\") ? "\\" : "/";
741
+ const idx = firstSelected.node.path.lastIndexOf(sep);
742
+ const parentPath = idx === -1 ? s.rootPath : firstSelected.node.path.slice(0, idx);
743
+ return { parentPath, insertAfterPath: firstSelected.node.path };
744
+ }, []);
745
+ useEffect2(() => {
746
+ if (!inputService) return;
747
+ const disposers = [];
748
+ const handlers = {
749
+ [ExplorerCommands.DELETE]: () => controller.deleteSelected(),
750
+ [ExplorerCommands.RENAME]: () => {
751
+ const s = stateRef.current;
752
+ if (s.selectedPaths.size === 1) {
753
+ controller.startRename(Array.from(s.selectedPaths)[0]);
754
+ }
755
+ },
756
+ [ExplorerCommands.NEW_FILE]: () => {
757
+ const t = getCreateTarget();
758
+ controller.startCreate(t.parentPath, false, t.insertAfterPath);
759
+ },
760
+ [ExplorerCommands.NEW_FOLDER]: () => {
761
+ const t = getCreateTarget();
762
+ controller.startCreate(t.parentPath, true, t.insertAfterPath);
763
+ },
764
+ [ExplorerCommands.REFRESH]: () => controller.refresh(),
765
+ [ExplorerCommands.COLLAPSE_ALL]: () => {
766
+ const s = stateRef.current;
767
+ for (const path of s.expandedPaths) {
768
+ controller.collapse(path);
769
+ }
770
+ },
771
+ [ExplorerCommands.SELECT_ALL]: () => controller.selectAll(),
772
+ [ExplorerCommands.COPY_PATH]: () => {
773
+ const s = stateRef.current;
774
+ const paths = Array.from(s.selectedPaths);
775
+ optionsRef.current?.onCopyPath?.(paths);
776
+ }
777
+ };
778
+ for (const [command, handler] of Object.entries(handlers)) {
779
+ disposers.push(inputService.registerCommand(command, handler));
780
+ }
781
+ return () => {
782
+ for (const dispose of disposers) {
783
+ dispose();
784
+ }
785
+ };
786
+ }, [inputService, controller, getCreateTarget]);
787
+ }
788
+
789
+ // src/react/useExplorerFocus.ts
790
+ import { useCallback as useCallback3, useRef as useRef3 } from "react";
791
+ function useExplorerFocus(inputService, contextKey = "explorerFocus") {
792
+ const focused = useRef3(false);
793
+ const onFocus = useCallback3(() => {
794
+ if (!focused.current) {
795
+ focused.current = true;
796
+ inputService?.setContext(contextKey, true);
797
+ }
798
+ }, [inputService, contextKey]);
799
+ const onBlur = useCallback3(() => {
800
+ if (focused.current) {
801
+ focused.current = false;
802
+ inputService?.deleteContext(contextKey);
803
+ }
804
+ }, [inputService, contextKey]);
805
+ return { onFocus, onBlur, tabIndex: 0 };
806
+ }
807
+
808
+ // src/ui/TreeNodeRow.tsx
809
+ import { memo, useMemo as useMemo2 } from "react";
810
+ import { getIcon } from "material-file-icons";
811
+
812
+ // src/ui/InlineRename.tsx
813
+ import { useEffect as useEffect3, useRef as useRef4, useState as useState3 } from "react";
814
+ import { jsx as jsx2 } from "react/jsx-runtime";
815
+ function InlineRename({ currentName, onCommit, onCancel }) {
816
+ const inputRef = useRef4(null);
817
+ const [value, setValue] = useState3(currentName);
818
+ useEffect3(() => {
819
+ const input = inputRef.current;
820
+ if (!input) return;
821
+ input.focus();
822
+ const dotIndex = currentName.lastIndexOf(".");
823
+ if (dotIndex > 0) {
824
+ input.setSelectionRange(0, dotIndex);
825
+ } else {
826
+ input.select();
827
+ }
828
+ }, [currentName]);
829
+ function handleKeyDown(e) {
830
+ e.stopPropagation();
831
+ if (e.key === "Enter") {
832
+ const trimmed = value.trim();
833
+ if (trimmed && trimmed !== currentName) {
834
+ onCommit(trimmed);
835
+ } else {
836
+ onCancel();
837
+ }
838
+ } else if (e.key === "Escape") {
839
+ onCancel();
840
+ }
841
+ }
842
+ function handleBlur() {
843
+ const trimmed = value.trim();
844
+ if (trimmed && trimmed !== currentName) {
845
+ onCommit(trimmed);
846
+ } else {
847
+ onCancel();
848
+ }
849
+ }
850
+ return /* @__PURE__ */ jsx2(
851
+ "input",
852
+ {
853
+ ref: inputRef,
854
+ className: "momoi-explorer-rename-input",
855
+ value,
856
+ onChange: (e) => setValue(e.target.value),
857
+ onKeyDown: handleKeyDown,
858
+ onBlur: handleBlur
859
+ }
860
+ );
861
+ }
862
+
863
+ // src/ui/TreeNodeRow.tsx
864
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
865
+ function ChevronIcon() {
866
+ return /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 16 16", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx3("path", { d: "M6 4l4 4-4 4", stroke: "currentColor", strokeWidth: "1.5", fill: "none" }) });
867
+ }
868
+ function FolderIcon({ isExpanded }) {
869
+ return /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 16 16", xmlns: "http://www.w3.org/2000/svg", children: isExpanded ? /* @__PURE__ */ jsx3("path", { d: "M1.5 3h5l1 1.5H14.5v9h-13z", fill: "#dcb67a", opacity: "0.9" }) : /* @__PURE__ */ jsx3("path", { d: "M1.5 2.5h5l1 1.5H14.5v10h-13z", fill: "#dcb67a" }) });
870
+ }
871
+ function MaterialFileIcon({ filename }) {
872
+ const svg = useMemo2(() => getIcon(filename).svg, [filename]);
873
+ return /* @__PURE__ */ jsx3("span", { className: "momoi-explorer-icon-inner", dangerouslySetInnerHTML: { __html: svg } });
874
+ }
875
+ var TreeNodeRow = memo(function TreeNodeRow2({
876
+ node,
877
+ depth,
878
+ isExpanded,
879
+ isSelected,
880
+ isRenaming,
881
+ onClick,
882
+ onDoubleClick,
883
+ onContextMenu,
884
+ onToggleExpand,
885
+ onCommitRename,
886
+ onCancelRename,
887
+ renderIcon,
888
+ renderBadge
889
+ }) {
890
+ return /* @__PURE__ */ jsxs(
891
+ "div",
892
+ {
893
+ className: "momoi-explorer-row",
894
+ "data-selected": isSelected,
895
+ "data-path": node.path,
896
+ onClick,
897
+ onDoubleClick,
898
+ onContextMenu,
899
+ children: [
900
+ /* @__PURE__ */ jsx3("span", { className: "momoi-explorer-indent", children: Array.from({ length: depth }, (_, i) => /* @__PURE__ */ jsx3("span", { className: "momoi-explorer-indent-guide" }, i)) }),
901
+ /* @__PURE__ */ jsx3(
902
+ "span",
903
+ {
904
+ className: "momoi-explorer-chevron",
905
+ "data-expanded": isExpanded,
906
+ "data-is-dir": node.isDirectory,
907
+ onClick: (e) => {
908
+ e.stopPropagation();
909
+ onToggleExpand();
910
+ },
911
+ children: /* @__PURE__ */ jsx3(ChevronIcon, {})
912
+ }
913
+ ),
914
+ /* @__PURE__ */ jsx3("span", { className: "momoi-explorer-icon", children: renderIcon ? renderIcon(node, isExpanded) : node.isDirectory ? /* @__PURE__ */ jsx3(FolderIcon, { isExpanded }) : /* @__PURE__ */ jsx3(MaterialFileIcon, { filename: node.name }) }),
915
+ isRenaming ? /* @__PURE__ */ jsx3(
916
+ InlineRename,
917
+ {
918
+ currentName: node.name,
919
+ onCommit: onCommitRename,
920
+ onCancel: onCancelRename
921
+ }
922
+ ) : /* @__PURE__ */ jsx3("span", { className: "momoi-explorer-name", children: node.name }),
923
+ renderBadge && /* @__PURE__ */ jsx3("span", { className: "momoi-explorer-badge", children: renderBadge(node) })
924
+ ]
925
+ }
926
+ );
927
+ });
928
+
929
+ // src/ui/ContextMenu.tsx
930
+ import { useEffect as useEffect4, useRef as useRef5 } from "react";
931
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
932
+ function ContextMenu({ items, x, y, onClose }) {
933
+ const menuRef = useRef5(null);
934
+ useEffect4(() => {
935
+ function handleClick(e) {
936
+ if (menuRef.current && !menuRef.current.contains(e.target)) {
937
+ onClose();
938
+ }
939
+ }
940
+ function handleKey(e) {
941
+ if (e.key === "Escape") onClose();
942
+ }
943
+ document.addEventListener("mousedown", handleClick);
944
+ document.addEventListener("keydown", handleKey);
945
+ return () => {
946
+ document.removeEventListener("mousedown", handleClick);
947
+ document.removeEventListener("keydown", handleKey);
948
+ };
949
+ }, [onClose]);
950
+ useEffect4(() => {
951
+ const menu = menuRef.current;
952
+ if (!menu) return;
953
+ const rect = menu.getBoundingClientRect();
954
+ if (rect.right > window.innerWidth) {
955
+ menu.style.left = `${window.innerWidth - rect.width - 4}px`;
956
+ }
957
+ if (rect.bottom > window.innerHeight) {
958
+ menu.style.top = `${window.innerHeight - rect.height - 4}px`;
959
+ }
960
+ }, [x, y]);
961
+ return /* @__PURE__ */ jsx4(
962
+ "div",
963
+ {
964
+ ref: menuRef,
965
+ className: "momoi-explorer-context-menu",
966
+ style: { left: x, top: y },
967
+ children: items.map((item) => {
968
+ if (item.separator) {
969
+ return /* @__PURE__ */ jsx4("div", { className: "momoi-explorer-context-menu-separator" }, item.id);
970
+ }
971
+ return /* @__PURE__ */ jsxs2(
972
+ "div",
973
+ {
974
+ className: "momoi-explorer-context-menu-item",
975
+ "data-disabled": item.disabled ?? false,
976
+ onClick: () => {
977
+ if (!item.disabled) {
978
+ item.action([]);
979
+ onClose();
980
+ }
981
+ },
982
+ children: [
983
+ /* @__PURE__ */ jsx4("span", { children: item.label }),
984
+ item.shortcut && /* @__PURE__ */ jsx4("span", { className: "momoi-explorer-context-menu-shortcut", children: item.shortcut })
985
+ ]
986
+ },
987
+ item.id
988
+ );
989
+ })
990
+ }
991
+ );
992
+ }
993
+
994
+ // src/ui/TreeFilterBar.tsx
995
+ import { useCallback as useCallback4, useRef as useRef6 } from "react";
996
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
997
+ function TreeFilterBar({
998
+ placeholder = "Filter files..."
999
+ }) {
1000
+ const { controller, state } = useTreeContext();
1001
+ const inputRef = useRef6(null);
1002
+ const handleChange = useCallback4((e) => {
1003
+ controller.setSearchQuery(e.target.value || null);
1004
+ }, [controller]);
1005
+ const handleKeyDown = useCallback4((e) => {
1006
+ if (e.key === "Escape") {
1007
+ controller.setSearchQuery(null);
1008
+ if (inputRef.current) inputRef.current.value = "";
1009
+ }
1010
+ }, [controller]);
1011
+ return /* @__PURE__ */ jsxs3("div", { className: "momoi-explorer-filter-bar", children: [
1012
+ /* @__PURE__ */ jsx5(
1013
+ "input",
1014
+ {
1015
+ ref: inputRef,
1016
+ className: "momoi-explorer-filter-input",
1017
+ type: "text",
1018
+ placeholder,
1019
+ defaultValue: state.searchQuery ?? "",
1020
+ onChange: handleChange,
1021
+ onKeyDown: handleKeyDown
1022
+ }
1023
+ ),
1024
+ state.searchQuery && /* @__PURE__ */ jsx5(
1025
+ "span",
1026
+ {
1027
+ className: "momoi-explorer-filter-clear",
1028
+ onClick: () => {
1029
+ controller.setSearchQuery(null);
1030
+ if (inputRef.current) inputRef.current.value = "";
1031
+ },
1032
+ children: "\xD7"
1033
+ }
1034
+ )
1035
+ ] });
1036
+ }
1037
+
1038
+ // src/ui/FileExplorer.tsx
1039
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1040
+ function FileExplorerInner({
1041
+ onOpen,
1042
+ renderIcon,
1043
+ renderBadge,
1044
+ contextMenuItems,
1045
+ showFilterBar,
1046
+ onControllerReady,
1047
+ inputService,
1048
+ onKeyDown
1049
+ }) {
1050
+ const { flatList, expandedPaths, selectedPaths, renamingPath, creatingState, rootPath, controller } = useFileTree();
1051
+ useEffect5(() => {
1052
+ onControllerReady?.(controller);
1053
+ }, [controller, onControllerReady]);
1054
+ useExplorerKeybindings(inputService ?? null);
1055
+ const focusProps = useExplorerFocus(inputService ?? null);
1056
+ const ctxMenu = useContextMenu();
1057
+ const rowItems = useMemo3(() => {
1058
+ const items = flatList.map((f) => ({
1059
+ type: "node",
1060
+ node: f.node,
1061
+ depth: f.depth
1062
+ }));
1063
+ if (!creatingState) return items;
1064
+ const { parentPath, isDirectory, insertAfterPath } = creatingState;
1065
+ if (insertAfterPath) {
1066
+ const idx = items.findIndex((item) => item.type === "node" && item.node.path === insertAfterPath);
1067
+ if (idx !== -1) {
1068
+ items.splice(idx + 1, 0, { type: "create", parentPath, isDirectory, depth: items[idx].depth });
1069
+ }
1070
+ } else if (parentPath === rootPath) {
1071
+ items.push({ type: "create", parentPath, isDirectory, depth: 0 });
1072
+ } else {
1073
+ const parentIdx = items.findIndex((item) => item.type === "node" && item.node.path === parentPath);
1074
+ if (parentIdx !== -1) {
1075
+ const parentDepth = items[parentIdx].depth;
1076
+ items.splice(parentIdx + 1, 0, { type: "create", parentPath, isDirectory, depth: parentDepth + 1 });
1077
+ }
1078
+ }
1079
+ return items;
1080
+ }, [flatList, creatingState, rootPath]);
1081
+ const handleClick = useCallback5((path, isDirectory, e) => {
1082
+ if (e.shiftKey) {
1083
+ controller.select(path, "range");
1084
+ } else if (e.ctrlKey || e.metaKey) {
1085
+ controller.select(path, "toggle");
1086
+ } else {
1087
+ controller.select(path, "replace");
1088
+ if (isDirectory) {
1089
+ controller.toggleExpand(path);
1090
+ }
1091
+ }
1092
+ }, [controller]);
1093
+ const handleDoubleClick = useCallback5((path, isDirectory) => {
1094
+ if (!isDirectory) {
1095
+ onOpen?.(path);
1096
+ }
1097
+ }, [onOpen]);
1098
+ const handleContextMenu = useCallback5((e, path) => {
1099
+ if (!selectedPaths.has(path)) {
1100
+ controller.select(path, "replace");
1101
+ }
1102
+ ctxMenu.show(e, path);
1103
+ }, [controller, selectedPaths, ctxMenu]);
1104
+ const handleBackgroundContextMenu = useCallback5((e) => {
1105
+ e.preventDefault();
1106
+ controller.clearSelection();
1107
+ ctxMenu.show(e, "");
1108
+ }, [controller, ctxMenu]);
1109
+ const menuItems = useMemo3(() => {
1110
+ if (!ctxMenu.isVisible || !contextMenuItems) return [];
1111
+ if (ctxMenu.targetPath === "") return contextMenuItems([]);
1112
+ const targetNodes = flatList.filter((f) => selectedPaths.has(f.node.path)).map((f) => f.node);
1113
+ return contextMenuItems(targetNodes);
1114
+ }, [ctxMenu.isVisible, ctxMenu.targetPath, contextMenuItems, flatList, selectedPaths]);
1115
+ return /* @__PURE__ */ jsxs4(
1116
+ "div",
1117
+ {
1118
+ style: { height: "100%", outline: "none" },
1119
+ onContextMenu: handleBackgroundContextMenu,
1120
+ onKeyDown,
1121
+ onFocus: focusProps.onFocus,
1122
+ onBlur: focusProps.onBlur,
1123
+ tabIndex: focusProps.tabIndex,
1124
+ children: [
1125
+ showFilterBar && /* @__PURE__ */ jsx6(TreeFilterBar, {}),
1126
+ /* @__PURE__ */ jsx6(
1127
+ Virtuoso,
1128
+ {
1129
+ totalCount: rowItems.length,
1130
+ fixedItemHeight: 22,
1131
+ increaseViewportBy: 200,
1132
+ computeItemKey: (index) => {
1133
+ const item = rowItems[index];
1134
+ return item.type === "create" ? `__create__${item.parentPath}` : item.node.path;
1135
+ },
1136
+ itemContent: (index) => {
1137
+ const item = rowItems[index];
1138
+ if (item.type === "create") {
1139
+ return /* @__PURE__ */ jsxs4("div", { className: "momoi-explorer-row", children: [
1140
+ /* @__PURE__ */ jsx6("span", { className: "momoi-explorer-indent", children: Array.from({ length: item.depth }, (_, i) => /* @__PURE__ */ jsx6("span", { className: "momoi-explorer-indent-guide" }, i)) }),
1141
+ /* @__PURE__ */ jsx6("span", { className: "momoi-explorer-chevron", "data-is-dir": "false", children: /* @__PURE__ */ jsx6("svg", { viewBox: "0 0 16 16", children: /* @__PURE__ */ jsx6("path", { d: "M6 4l4 4-4 4", stroke: "currentColor", strokeWidth: "1.5", fill: "none" }) }) }),
1142
+ /* @__PURE__ */ jsx6("span", { className: "momoi-explorer-icon", children: item.isDirectory ? "\u{1F4C1}" : "\u{1F4C4}" }),
1143
+ /* @__PURE__ */ jsx6(
1144
+ InlineRename,
1145
+ {
1146
+ currentName: "",
1147
+ onCommit: (name) => controller.commitCreate(name),
1148
+ onCancel: () => controller.cancelCreate()
1149
+ }
1150
+ )
1151
+ ] });
1152
+ }
1153
+ const { node, depth } = item;
1154
+ return /* @__PURE__ */ jsx6(
1155
+ TreeNodeRow,
1156
+ {
1157
+ node,
1158
+ depth,
1159
+ isExpanded: expandedPaths.has(node.path),
1160
+ isSelected: selectedPaths.has(node.path),
1161
+ isRenaming: renamingPath === node.path,
1162
+ onClick: (e) => handleClick(node.path, node.isDirectory, e),
1163
+ onDoubleClick: () => handleDoubleClick(node.path, node.isDirectory),
1164
+ onContextMenu: (e) => handleContextMenu(e, node.path),
1165
+ onToggleExpand: () => controller.toggleExpand(node.path),
1166
+ onCommitRename: (name) => controller.commitRename(name),
1167
+ onCancelRename: () => controller.cancelRename(),
1168
+ renderIcon,
1169
+ renderBadge
1170
+ }
1171
+ );
1172
+ }
1173
+ }
1174
+ ),
1175
+ ctxMenu.isVisible && menuItems.length > 0 && /* @__PURE__ */ jsx6(
1176
+ ContextMenu,
1177
+ {
1178
+ items: menuItems,
1179
+ x: ctxMenu.x,
1180
+ y: ctxMenu.y,
1181
+ onClose: ctxMenu.hide
1182
+ }
1183
+ )
1184
+ ]
1185
+ }
1186
+ );
1187
+ }
1188
+ function FileExplorer({
1189
+ adapter,
1190
+ rootPath,
1191
+ sort,
1192
+ filter,
1193
+ watchOptions,
1194
+ onEvent,
1195
+ onOpen,
1196
+ renderIcon,
1197
+ renderBadge,
1198
+ contextMenuItems,
1199
+ showFilterBar,
1200
+ onControllerReady,
1201
+ inputService,
1202
+ onKeyDown,
1203
+ className,
1204
+ style
1205
+ }) {
1206
+ return /* @__PURE__ */ jsx6("div", { className: `momoi-explorer ${className ?? ""}`, style, children: /* @__PURE__ */ jsx6(
1207
+ TreeProvider,
1208
+ {
1209
+ adapter,
1210
+ rootPath,
1211
+ sort,
1212
+ filter,
1213
+ watchOptions,
1214
+ onEvent,
1215
+ children: /* @__PURE__ */ jsx6(
1216
+ FileExplorerInner,
1217
+ {
1218
+ onOpen,
1219
+ renderIcon,
1220
+ renderBadge,
1221
+ contextMenuItems,
1222
+ showFilterBar,
1223
+ onControllerReady,
1224
+ inputService,
1225
+ onKeyDown
1226
+ }
1227
+ )
1228
+ }
1229
+ ) });
1230
+ }
1231
+
1232
+ // src/ui/QuickOpen.tsx
1233
+ import { useCallback as useCallback6, useEffect as useEffect6, useMemo as useMemo4, useRef as useRef7, useState as useState4 } from "react";
1234
+ import { getIcon as getIcon2 } from "material-file-icons";
1235
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1236
+ function QuickOpenIcon({ filename }) {
1237
+ const svg = useMemo4(() => getIcon2(filename).svg, [filename]);
1238
+ return /* @__PURE__ */ jsx7("span", { className: "momoi-explorer-quickopen-icon", dangerouslySetInnerHTML: { __html: svg } });
1239
+ }
1240
+ function QuickOpen({
1241
+ controller,
1242
+ isOpen,
1243
+ onClose,
1244
+ onSelect,
1245
+ placeholder = "Search files by name...",
1246
+ maxResults = 50
1247
+ }) {
1248
+ const inputRef = useRef7(null);
1249
+ const [query, setQuery] = useState4("");
1250
+ const [allFiles, setAllFiles] = useState4([]);
1251
+ const [results, setResults] = useState4([]);
1252
+ const [selectedIndex, setSelectedIndex] = useState4(0);
1253
+ useEffect6(() => {
1254
+ if (!isOpen) return;
1255
+ controller.collectAllFiles().then(setAllFiles);
1256
+ }, [isOpen, controller]);
1257
+ useEffect6(() => {
1258
+ if (isOpen) {
1259
+ setQuery("");
1260
+ setResults([]);
1261
+ setSelectedIndex(0);
1262
+ setTimeout(() => inputRef.current?.focus(), 0);
1263
+ }
1264
+ }, [isOpen]);
1265
+ useEffect6(() => {
1266
+ if (!query) {
1267
+ setResults([]);
1268
+ setSelectedIndex(0);
1269
+ return;
1270
+ }
1271
+ const found = fuzzyFind(allFiles, query, maxResults);
1272
+ setResults(found);
1273
+ setSelectedIndex(0);
1274
+ }, [query, allFiles, maxResults]);
1275
+ const handleKeyDown = useCallback6((e) => {
1276
+ switch (e.key) {
1277
+ case "Escape":
1278
+ onClose();
1279
+ break;
1280
+ case "ArrowDown":
1281
+ e.preventDefault();
1282
+ setSelectedIndex((i) => Math.min(i + 1, results.length - 1));
1283
+ break;
1284
+ case "ArrowUp":
1285
+ e.preventDefault();
1286
+ setSelectedIndex((i) => Math.max(i - 1, 0));
1287
+ break;
1288
+ case "Enter":
1289
+ e.preventDefault();
1290
+ if (results[selectedIndex]) {
1291
+ onSelect(results[selectedIndex]);
1292
+ onClose();
1293
+ }
1294
+ break;
1295
+ }
1296
+ }, [onClose, onSelect, results, selectedIndex]);
1297
+ if (!isOpen) return null;
1298
+ return /* @__PURE__ */ jsx7("div", { className: "momoi-explorer-quickopen-overlay", onClick: onClose, children: /* @__PURE__ */ jsxs5(
1299
+ "div",
1300
+ {
1301
+ className: "momoi-explorer-quickopen",
1302
+ onClick: (e) => e.stopPropagation(),
1303
+ children: [
1304
+ /* @__PURE__ */ jsx7(
1305
+ "input",
1306
+ {
1307
+ ref: inputRef,
1308
+ className: "momoi-explorer-quickopen-input",
1309
+ type: "text",
1310
+ placeholder,
1311
+ value: query,
1312
+ onChange: (e) => setQuery(e.target.value),
1313
+ onKeyDown: handleKeyDown
1314
+ }
1315
+ ),
1316
+ results.length > 0 && /* @__PURE__ */ jsx7("div", { className: "momoi-explorer-quickopen-results", children: results.map((entry, i) => /* @__PURE__ */ jsxs5(
1317
+ "div",
1318
+ {
1319
+ className: "momoi-explorer-quickopen-item",
1320
+ "data-selected": i === selectedIndex,
1321
+ onMouseEnter: () => setSelectedIndex(i),
1322
+ onClick: () => {
1323
+ onSelect(entry);
1324
+ onClose();
1325
+ },
1326
+ children: [
1327
+ /* @__PURE__ */ jsx7(QuickOpenIcon, { filename: entry.name }),
1328
+ /* @__PURE__ */ jsx7("span", { className: "momoi-explorer-quickopen-name", children: entry.name }),
1329
+ /* @__PURE__ */ jsx7("span", { className: "momoi-explorer-quickopen-path", children: entry.path })
1330
+ ]
1331
+ },
1332
+ entry.path
1333
+ )) }),
1334
+ query && results.length === 0 && /* @__PURE__ */ jsx7("div", { className: "momoi-explorer-quickopen-empty", children: "No matching files" })
1335
+ ]
1336
+ }
1337
+ ) });
1338
+ }
1339
+ export {
1340
+ ContextMenu,
1341
+ FileExplorer,
1342
+ InlineRename,
1343
+ QuickOpen,
1344
+ TreeFilterBar,
1345
+ TreeNodeRow
1346
+ };
1347
+ //# sourceMappingURL=ui.js.map