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