momoi-explorer 0.8.0 → 0.8.3

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.
Files changed (2) hide show
  1. package/README.md +409 -355
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,355 +1,409 @@
1
- # momoi-explorer
2
-
3
- ヘッドレスファイルエクスプローラーライブラリ。フレームワーク非依存のコア + React バインディング + デフォルト UI の3層構成。
4
-
5
- ## インストール
6
-
7
- ```bash
8
- npm install momoi-explorer
9
- ```
10
-
11
- ## アーキテクチャ
12
-
13
- 3つのエントリポイントを持つ段階的なアーキテクチャ:
14
-
15
- | エントリポイント | 用途 | React必須 |
16
- |---|---|---|
17
- | `momoi-explorer` | コアエンジン(フレームワーク非依存) | No |
18
- | `momoi-explorer/react` | React バインディング(hooks + context) | Yes |
19
- | `momoi-explorer/ui` | デフォルト UI コンポーネント | Yes |
20
-
21
- ## クイックスタート
22
-
23
- ### 1. FileSystemAdapter を実装する
24
-
25
- 全ての始まりは `FileSystemAdapter` の実装。`readDir` のみ必須で、他のメソッドはオプション(実装すると対応機能が有効になる)。
26
-
27
- ```ts
28
- import type { FileSystemAdapter } from 'momoi-explorer'
29
-
30
- const adapter: FileSystemAdapter = {
31
- // 必須: ディレクトリの中身を返す
32
- async readDir(dirPath) {
33
- const entries = await fs.readdir(dirPath, { withFileTypes: true })
34
- return entries.map(e => ({
35
- name: e.name,
36
- path: path.join(dirPath, e.name),
37
- isDirectory: e.isDirectory(),
38
- }))
39
- },
40
- // オプション: リネーム
41
- async rename(oldPath, newPath) {
42
- await fs.rename(oldPath, newPath)
43
- },
44
- // オプション: 削除
45
- async delete(paths) {
46
- for (const p of paths) await fs.rm(p, { recursive: true })
47
- },
48
- // オプション: ファイル作成
49
- async createFile(parentPath, name) {
50
- await fs.writeFile(path.join(parentPath, name), '')
51
- },
52
- // オプション: フォルダ作成
53
- async createDir(parentPath, name) {
54
- await fs.mkdir(path.join(parentPath, name))
55
- },
56
- // オプション: ファイル監視(デバウンス・合体はコアが行う)
57
- watch(dirPath, callback) {
58
- const watcher = fs.watch(dirPath, { recursive: true }, (event, filename) => {
59
- callback([{ type: event === 'rename' ? 'create' : 'modify', path: filename, isDirectory: false }])
60
- })
61
- return () => watcher.close()
62
- },
63
- }
64
- ```
65
-
66
- ### 2a. デフォルト UI を使う(最も簡単)
67
-
68
- ```tsx
69
- import { FileExplorer } from 'momoi-explorer/ui'
70
- import 'momoi-explorer/ui/style.css'
71
-
72
- function App() {
73
- return (
74
- <FileExplorer
75
- adapter={adapter}
76
- rootPath="/home/user/project"
77
- onOpen={(path) => openFile(path)}
78
- onEvent={(e) => console.log('tree event:', e)}
79
- showFilterBar
80
- />
81
- )
82
- }
83
- ```
84
-
85
- ### 2b. React hooks でカスタム UI を構築する
86
-
87
- ```tsx
88
- import { TreeProvider, useFileTree, useTreeNode } from 'momoi-explorer/react'
89
-
90
- function App() {
91
- return (
92
- <TreeProvider adapter={adapter} rootPath="/home/user/project">
93
- <MyCustomTree />
94
- </TreeProvider>
95
- )
96
- }
97
-
98
- function MyCustomTree() {
99
- const { flatList, controller } = useFileTree()
100
-
101
- return (
102
- <div>
103
- {flatList.map(({ node, depth }) => (
104
- <div key={node.path} style={{ paddingLeft: depth * 16 }}>
105
- <span onClick={() => controller.toggleExpand(node.path)}>
106
- {node.name}
107
- </span>
108
- </div>
109
- ))}
110
- </div>
111
- )
112
- }
113
- ```
114
-
115
- ### 2c. コアのみ使用(フレームワーク非依存)
116
-
117
- ```ts
118
- import { createFileTree } from 'momoi-explorer'
119
-
120
- const tree = createFileTree({
121
- adapter,
122
- rootPath: '/home/user/project',
123
- onEvent: (e) => console.log(e),
124
- })
125
-
126
- // 状態購読
127
- tree.subscribe((state) => {
128
- console.log('nodes:', state.rootNodes)
129
- console.log('flatList:', state.flatList)
130
- })
131
-
132
- // ツリーを読み込み
133
- await tree.loadRoot()
134
-
135
- // 操作
136
- await tree.expand('/home/user/project/src')
137
- tree.select('/home/user/project/src/index.ts')
138
- tree.setSearchQuery('config')
139
-
140
- // 後始末
141
- tree.destroy()
142
- ```
143
-
144
- ## API リファレンス
145
-
146
- ### コア層 (`momoi-explorer`)
147
-
148
- #### `createFileTree(options): FileTreeController`
149
-
150
- ヘッドレスファイルツリーのメインエントリポイント。
151
-
152
- **FileTreeOptions:**
153
- | プロパティ | 型 | 説明 |
154
- |---|---|---|
155
- | `adapter` | `FileSystemAdapter` | ファイルシステムアダプタ(必須) |
156
- | `rootPath` | `string` | ルートディレクトリの絶対パス |
157
- | `sort` | `(a, b) => number` | カスタムソート関数 |
158
- | `filter` | `(entry) => boolean` | カスタムフィルタ関数 |
159
- | `watchOptions` | `WatchOptions` | ファイル監視設定 |
160
- | `onEvent` | `(event: TreeEvent) => void` | イベントコールバック |
161
-
162
- **FileTreeController のメソッド:**
163
-
164
- | メソッド | 説明 |
165
- |---|---|
166
- | `getState()` | 現在の TreeState を取得 |
167
- | `subscribe(listener)` | 状態変更を購読。unsubscribe関数を返す |
168
- | `loadRoot()` | ルートを読み込み・初期化(最初に必ず呼ぶ) |
169
- | `expand(path)` | ディレクトリを展開 |
170
- | `collapse(path)` | ディレクトリを折りたたみ |
171
- | `toggleExpand(path)` | 展開/折りたたみをトグル |
172
- | `expandTo(path)` | 指定パスまで祖先をすべて展開 |
173
- | `select(path, mode?)` | ノードを選択(mode: 'replace' / 'toggle' / 'range') |
174
- | `selectAll()` | 全ノードを選択 |
175
- | `clearSelection()` | 選択解除 |
176
- | `startRename(path)` | リネームモード開始 |
177
- | `commitRename(newName)` | リネーム確定 |
178
- | `cancelRename()` | リネームキャンセル |
179
- | `startCreate(parentPath, isDirectory)` | インライン新規作成モード開始 |
180
- | `commitCreate(name)` | 新規作成確定 |
181
- | `cancelCreate()` | 新規作成キャンセル |
182
- | `createFile(parentPath, name)` | ファイル作成 |
183
- | `createDir(parentPath, name)` | フォルダ作成 |
184
- | `deleteSelected()` | 選択中のアイテムを削除 |
185
- | `refresh(path?)` | ツリーをリフレッシュ(展開状態は保持) |
186
- | `setSearchQuery(query)` | ファジー検索クエリ設定(nullで解除) |
187
- | `collectAllFiles()` | 全ファイルを再帰収集(QuickOpen用) |
188
- | `setFilter(fn)` | フィルタ関数を動的変更 |
189
- | `setSort(fn)` | ソート関数を動的変更 |
190
- | `destroy()` | コントローラ破棄(監視停止・購読解除) |
191
-
192
- #### ユーティリティ関数
193
-
194
- | 関数 | 説明 |
195
- |---|---|
196
- | `flattenTree(nodes, expandedPaths, matchingPaths?)` | ツリーをフラットリストに変換 |
197
- | `computeSelection(current, anchor, target, mode, flatList)` | 選択状態を計算 |
198
- | `fuzzyMatch(query, target)` | ファジーマッチ(match + score) |
199
- | `fuzzyFind(files, query, maxResults?)` | スコア順にファジー検索 |
200
- | `findMatchingPaths(nodes, query)` | マッチするパスのSetを返す |
201
- | `coalesceEvents(raw)` | 生イベントを合体処理 |
202
- | `createEventProcessor(callback, options?)` | デバウンス付きイベントプロセッサ |
203
- | `defaultSort(a, b)` | デフォルトソート(フォルダ優先・名前昇順) |
204
- | `defaultFilter(entry)` | デフォルトフィルタ(全表示) |
205
-
206
- ### React層 (`momoi-explorer/react`)
207
-
208
- | エクスポート | 種別 | 説明 |
209
- |---|---|---|
210
- | `TreeProvider` | コンポーネント | ファイルツリーのコンテキストプロバイダー。内部で `createFileTree` + `loadRoot` を行う |
211
- | `useFileTree()` | Hook | ツリー全体の状態とコントローラを返す |
212
- | `useTreeNode(path)` | Hook | 個別ノードの展開/選択/リネーム状態を返す(見つからない場合 null) |
213
- | `useContextMenu()` | Hook | 右クリックメニューの表示制御(show/hide + 座標管理) |
214
- | `useTreeContext()` | Hook | TreeContext の生の値を取得(通常は useFileTree を使う) |
215
-
216
- ### UI層 (`momoi-explorer/ui`)
217
-
218
- | エクスポート | 説明 |
219
- |---|---|
220
- | `FileExplorer` | オールインワンコンポーネント(TreeProvider内包、仮想スクロール、コンテキストメニュー対応) |
221
- | `TreeNodeRow` | ツリーの1行コンポーネント(アイコン、インデント、選択、リネーム対応) |
222
- | `ContextMenu` | 右クリックメニュー(外側クリック/Escで閉じる) |
223
- | `InlineRename` | インライン名前変更input(Enter確定、Escキャンセル) |
224
- | `TreeFilterBar` | ファジー検索フィルタバー |
225
- | `QuickOpen` | VSCode風クイックオープンダイアログ(Ctrl+P相当) |
226
-
227
- **スタイル:**
228
-
229
- ```ts
230
- import 'momoi-explorer/ui/style.css'
231
- ```
232
-
233
- VSCode風ダークテーマ。CSS変数やクラス名(`.momoi-explorer-*`)でカスタマイズ可能。
234
-
235
- ### FileExplorer の Props
236
-
237
- ```tsx
238
- <FileExplorer
239
- adapter={adapter} // FileSystemAdapter(必須)
240
- rootPath="/path/to/dir" // ルートパス(必須)
241
- sort={(a, b) => ...} // カスタムソート
242
- filter={(entry) => ...} // カスタムフィルタ
243
- watchOptions={{ ... }} // ファイル監視設定
244
- onEvent={(e) => ...} // ツリーイベントコールバック
245
- onOpen={(path) => ...} // ファイルダブルクリック時
246
- renderIcon={(node, expanded) => ...} // カスタムアイコン
247
- renderBadge={(node) => ...} // カスタムバッジ(git status等)
248
- contextMenuItems={(nodes) => [...]} // コンテキストメニュー項目
249
- showFilterBar // フィルタバー表示
250
- onControllerReady={(ctrl) => ...} // コントローラ参照の取得
251
- className="my-explorer" // CSSクラス
252
- style={{ height: 400 }} // インラインスタイル
253
- />
254
- ```
255
-
256
- ### QuickOpen の使い方
257
-
258
- ```tsx
259
- import { FileExplorer, QuickOpen } from 'momoi-explorer/ui'
260
-
261
- function App() {
262
- const [ctrl, setCtrl] = useState<FileTreeController | null>(null)
263
- const [quickOpen, setQuickOpen] = useState(false)
264
-
265
- return (
266
- <>
267
- <FileExplorer
268
- adapter={adapter}
269
- rootPath={rootPath}
270
- onControllerReady={setCtrl}
271
- />
272
- {ctrl && (
273
- <QuickOpen
274
- controller={ctrl}
275
- isOpen={quickOpen}
276
- onClose={() => setQuickOpen(false)}
277
- onSelect={(entry) => openFile(entry.path)}
278
- />
279
- )}
280
- </>
281
- )
282
- }
283
- ```
284
-
285
- ## 主要な型
286
-
287
- ```ts
288
- interface FileEntry {
289
- name: string // ファイル名
290
- path: string // 絶対パス
291
- isDirectory: boolean // ディレクトリか
292
- meta?: Record<string, unknown> // 拡張用メタデータ
293
- }
294
-
295
- interface TreeNode extends FileEntry {
296
- depth: number
297
- children?: TreeNode[]
298
- childrenLoaded: boolean
299
- }
300
-
301
- interface FlatNode {
302
- node: TreeNode
303
- depth: number
304
- }
305
-
306
- interface TreeState {
307
- rootPath: string
308
- rootNodes: TreeNode[]
309
- expandedPaths: Set<string>
310
- selectedPaths: Set<string>
311
- anchorPath: string | null
312
- renamingPath: string | null
313
- creatingState: CreatingState | null
314
- searchQuery: string | null
315
- flatList: FlatNode[]
316
- }
317
-
318
- type TreeEvent =
319
- | { type: 'expand'; path: string }
320
- | { type: 'collapse'; path: string }
321
- | { type: 'select'; paths: string[] }
322
- | { type: 'open'; path: string }
323
- | { type: 'rename'; oldPath: string; newPath: string }
324
- | { type: 'delete'; paths: string[] }
325
- | { type: 'create'; parentPath: string; name: string; isDirectory: boolean }
326
- | { type: 'refresh'; path?: string }
327
- | { type: 'external-change'; changes: WatchEvent[] }
328
- ```
329
-
330
- ## ファイル監視
331
-
332
- `adapter.watch` を実装すると自動でファイル監視が有効になる。生イベントをそのまま投げるだけでよく、以下の処理はコアが自動で行う:
333
-
334
- - **デバウンス** (75ms, VSCode準拠)
335
- - **イベント合体**: rename delete+create、delete+create(同一パス) modify、親フォルダ削除時に子イベント除去
336
- - **スロットリング**: 大量イベント時にチャンク分割(500件/200ms間隔)
337
-
338
- ```ts
339
- const tree = createFileTree({
340
- adapter,
341
- rootPath: '/project',
342
- watchOptions: {
343
- debounceMs: 100, // デフォルト: 75
344
- coalesce: true, // デフォルト: true
345
- throttle: {
346
- maxChunkSize: 1000, // デフォルト: 500
347
- delayMs: 300, // デフォルト: 200
348
- },
349
- },
350
- })
351
- ```
352
-
353
- ## ライセンス
354
-
355
- MIT
1
+ # momoi-explorer
2
+
3
+ > Built with [Claude Code](https://claude.com/claude-code) by Anthropic.
4
+
5
+ A headless file explorer library. Framework-agnostic core + React bindings + default UI in a 3-layer architecture.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install momoi-explorer
11
+ ```
12
+
13
+ ## Architecture
14
+
15
+ Three entry points with a layered architecture:
16
+
17
+ | Entry Point | Purpose | Requires React |
18
+ |---|---|---|
19
+ | `momoi-explorer` | Core engine (framework-agnostic) | No |
20
+ | `momoi-explorer/react` | React bindings (hooks + context) | Yes |
21
+ | `momoi-explorer/ui` | Default UI components | Yes |
22
+
23
+ ## Quick Start
24
+
25
+ ### 1. Implement a FileSystemAdapter
26
+
27
+ Everything starts with implementing `FileSystemAdapter`. Only `readDir` is required; other methods are optional (implementing them enables the corresponding features).
28
+
29
+ ```ts
30
+ import type { FileSystemAdapter } from 'momoi-explorer'
31
+
32
+ const adapter: FileSystemAdapter = {
33
+ // Required: return directory contents
34
+ async readDir(dirPath) {
35
+ const entries = await fs.readdir(dirPath, { withFileTypes: true })
36
+ return entries.map(e => ({
37
+ name: e.name,
38
+ path: path.join(dirPath, e.name),
39
+ isDirectory: e.isDirectory(),
40
+ }))
41
+ },
42
+ // Optional: rename
43
+ async rename(oldPath, newPath) {
44
+ await fs.rename(oldPath, newPath)
45
+ },
46
+ // Optional: delete
47
+ async delete(paths) {
48
+ for (const p of paths) await fs.rm(p, { recursive: true })
49
+ },
50
+ // Optional: create file
51
+ async createFile(parentPath, name) {
52
+ await fs.writeFile(path.join(parentPath, name), '')
53
+ },
54
+ // Optional: create directory
55
+ async createDir(parentPath, name) {
56
+ await fs.mkdir(path.join(parentPath, name))
57
+ },
58
+ // Optional: file watching (debounce & coalescing handled by core)
59
+ watch(dirPath, callback) {
60
+ const watcher = fs.watch(dirPath, { recursive: true }, (event, filename) => {
61
+ callback([{ type: event === 'rename' ? 'create' : 'modify', path: filename, isDirectory: false }])
62
+ })
63
+ return () => watcher.close()
64
+ },
65
+ }
66
+ ```
67
+
68
+ ### 2a. Use the Default UI (easiest)
69
+
70
+ ```tsx
71
+ import { FileExplorer } from 'momoi-explorer/ui'
72
+ import 'momoi-explorer/ui/style.css'
73
+
74
+ function App() {
75
+ return (
76
+ <FileExplorer
77
+ adapter={adapter}
78
+ rootPath="/home/user/project"
79
+ onOpen={(path) => openFile(path)}
80
+ onEvent={(e) => console.log('tree event:', e)}
81
+ showFilterBar
82
+ />
83
+ )
84
+ }
85
+ ```
86
+
87
+ ### 2b. Build Custom UI with React Hooks
88
+
89
+ ```tsx
90
+ import { TreeProvider, useFileTree, useTreeNode } from 'momoi-explorer/react'
91
+
92
+ function App() {
93
+ return (
94
+ <TreeProvider adapter={adapter} rootPath="/home/user/project">
95
+ <MyCustomTree />
96
+ </TreeProvider>
97
+ )
98
+ }
99
+
100
+ function MyCustomTree() {
101
+ const { flatList, controller } = useFileTree()
102
+
103
+ return (
104
+ <div>
105
+ {flatList.map(({ node, depth }) => (
106
+ <div key={node.path} style={{ paddingLeft: depth * 16 }}>
107
+ <span onClick={() => controller.toggleExpand(node.path)}>
108
+ {node.name}
109
+ </span>
110
+ </div>
111
+ ))}
112
+ </div>
113
+ )
114
+ }
115
+ ```
116
+
117
+ ### 2c. Core Only (framework-agnostic)
118
+
119
+ ```ts
120
+ import { createFileTree } from 'momoi-explorer'
121
+
122
+ const tree = createFileTree({
123
+ adapter,
124
+ rootPath: '/home/user/project',
125
+ onEvent: (e) => console.log(e),
126
+ })
127
+
128
+ // Subscribe to state changes
129
+ tree.subscribe((state) => {
130
+ console.log('nodes:', state.rootNodes)
131
+ console.log('flatList:', state.flatList)
132
+ })
133
+
134
+ // Load the tree
135
+ await tree.loadRoot()
136
+
137
+ // Operations
138
+ await tree.expand('/home/user/project/src')
139
+ tree.select('/home/user/project/src/index.ts')
140
+ tree.setSearchQuery('config')
141
+
142
+ // Cleanup
143
+ tree.destroy()
144
+ ```
145
+
146
+ ## API Reference
147
+
148
+ ### Core (`momoi-explorer`)
149
+
150
+ #### `createFileTree(options): FileTreeController`
151
+
152
+ Main entry point for the headless file tree.
153
+
154
+ **FileTreeOptions:**
155
+ | Property | Type | Description |
156
+ |---|---|---|
157
+ | `adapter` | `FileSystemAdapter` | File system adapter (required) |
158
+ | `rootPath` | `string` | Absolute path to the root directory |
159
+ | `sort` | `(a, b) => number` | Custom sort function |
160
+ | `filter` | `(entry) => boolean` | Custom filter function |
161
+ | `watchOptions` | `WatchOptions` | File watching options |
162
+ | `onEvent` | `(event: TreeEvent) => void` | Event callback |
163
+
164
+ **FileTreeController Methods:**
165
+
166
+ | Method | Description |
167
+ |---|---|
168
+ | `getState()` | Get current TreeState |
169
+ | `subscribe(listener)` | Subscribe to state changes. Returns unsubscribe function |
170
+ | `loadRoot()` | Load and initialize root (must be called first) |
171
+ | `expand(path)` | Expand a directory |
172
+ | `collapse(path)` | Collapse a directory |
173
+ | `toggleExpand(path)` | Toggle expand/collapse |
174
+ | `expandTo(path)` | Expand all ancestors up to the given path |
175
+ | `select(path, mode?)` | Select a node (mode: 'replace' / 'toggle' / 'range') |
176
+ | `selectAll()` | Select all nodes |
177
+ | `clearSelection()` | Clear selection |
178
+ | `startRename(path)` | Enter rename mode |
179
+ | `commitRename(newName)` | Commit rename |
180
+ | `cancelRename()` | Cancel rename |
181
+ | `startCreate(parentPath, isDirectory, insertAfterPath?)` | Enter inline creation mode (insertAfterPath: position for the input row) |
182
+ | `commitCreate(name)` | Commit creation |
183
+ | `cancelCreate()` | Cancel creation |
184
+ | `createFile(parentPath, name)` | Create a file |
185
+ | `createDir(parentPath, name)` | Create a directory |
186
+ | `deleteSelected()` | Delete selected items |
187
+ | `refresh(path?)` | Refresh the tree (preserves expanded state) |
188
+ | `setSearchQuery(query)` | Set fuzzy search query (null to clear) |
189
+ | `collectAllFiles()` | Recursively collect all files (for QuickOpen) |
190
+ | `setFilter(fn)` | Dynamically change the filter function |
191
+ | `setSort(fn)` | Dynamically change the sort function |
192
+ | `destroy()` | Destroy the controller (stops watching, clears subscriptions) |
193
+
194
+ #### Utility Functions
195
+
196
+ | Function | Description |
197
+ |---|---|
198
+ | `flattenTree(nodes, expandedPaths, matchingPaths?)` | Convert tree to flat list |
199
+ | `computeSelection(current, anchor, target, mode, flatList)` | Compute selection state |
200
+ | `fuzzyMatch(query, target)` | Fuzzy match (match + score) |
201
+ | `fuzzyFind(files, query, maxResults?)` | Fuzzy search sorted by score |
202
+ | `findMatchingPaths(nodes, query)` | Return Set of matching paths |
203
+ | `coalesceEvents(raw)` | Coalesce raw watch events |
204
+ | `createEventProcessor(callback, options?)` | Event processor with debounce |
205
+ | `defaultSort(a, b)` | Default sort (directories first, name ascending) |
206
+ | `defaultFilter(entry)` | Default filter (show all) |
207
+ | `ExplorerCommands` | Explorer command ID constants (DELETE, RENAME, etc.) |
208
+ | `defaultExplorerKeybindings` | Default keybinding definitions (for momoi-keybind) |
209
+
210
+ ### React (`momoi-explorer/react`)
211
+
212
+ | Export | Kind | Description |
213
+ |---|---|---|
214
+ | `TreeProvider` | Component | File tree context provider. Calls `createFileTree` + `loadRoot` internally |
215
+ | `useFileTree()` | Hook | Returns full tree state and controller |
216
+ | `useTreeNode(path)` | Hook | Returns expand/select/rename state for a node (null if not found) |
217
+ | `useContextMenu()` | Hook | Context menu visibility control (show/hide + position) |
218
+ | `useExplorerKeybindings(inputService)` | Hook | momoi-keybind integration. Registers explorer command handlers |
219
+ | `useExplorerFocus(inputService)` | Hook | Syncs focus state with momoi-keybind context |
220
+ | `useTreeContext()` | Hook | Raw TreeContext value (usually use useFileTree instead) |
221
+
222
+ ### UI (`momoi-explorer/ui`)
223
+
224
+ | Export | Description |
225
+ |---|---|
226
+ | `FileExplorer` | All-in-one component (includes TreeProvider, virtual scrolling, context menu) |
227
+ | `TreeNodeRow` | Single tree row (icon, indent, selection, rename) |
228
+ | `ContextMenu` | Right-click menu (closes on outside click/Esc) |
229
+ | `InlineRename` | Inline rename input (Enter to confirm, Esc to cancel) |
230
+ | `TreeFilterBar` | Fuzzy search filter bar |
231
+ | `QuickOpen` | VSCode-style quick open dialog (Ctrl+P equivalent) |
232
+
233
+ **Styles:**
234
+
235
+ ```ts
236
+ import 'momoi-explorer/ui/style.css'
237
+ ```
238
+
239
+ VSCode-style dark theme. Customizable via CSS variables and class names (`.momoi-explorer-*`).
240
+
241
+ ### FileExplorer Props
242
+
243
+ ```tsx
244
+ <FileExplorer
245
+ adapter={adapter} // FileSystemAdapter (required)
246
+ rootPath="/path/to/dir" // Root path (required)
247
+ sort={(a, b) => ...} // Custom sort
248
+ filter={(entry) => ...} // Custom filter
249
+ watchOptions={{ ... }} // File watching options
250
+ onEvent={(e) => ...} // Tree event callback
251
+ onOpen={(path) => ...} // File double-click handler
252
+ renderIcon={(node, expanded) => ...} // Custom icon renderer
253
+ renderBadge={(node) => ...} // Custom badge renderer (e.g. git status)
254
+ contextMenuItems={(nodes) => [...]} // Context menu items
255
+ showFilterBar // Show filter bar
256
+ onControllerReady={(ctrl) => ...} // Get controller reference
257
+ inputService={inputService} // momoi-keybind InputService instance (optional)
258
+ onKeyDown={(e) => ...} // Key event handler when not using momoi-keybind
259
+ className="my-explorer" // CSS class
260
+ style={{ height: 400 }} // Inline styles
261
+ />
262
+ ```
263
+
264
+ ### QuickOpen Usage
265
+
266
+ ```tsx
267
+ import { FileExplorer, QuickOpen } from 'momoi-explorer/ui'
268
+
269
+ function App() {
270
+ const [ctrl, setCtrl] = useState<FileTreeController | null>(null)
271
+ const [quickOpen, setQuickOpen] = useState(false)
272
+
273
+ return (
274
+ <>
275
+ <FileExplorer
276
+ adapter={adapter}
277
+ rootPath={rootPath}
278
+ onControllerReady={setCtrl}
279
+ />
280
+ {ctrl && (
281
+ <QuickOpen
282
+ controller={ctrl}
283
+ isOpen={quickOpen}
284
+ onClose={() => setQuickOpen(false)}
285
+ onSelect={(entry) => openFile(entry.path)}
286
+ />
287
+ )}
288
+ </>
289
+ )
290
+ }
291
+ ```
292
+
293
+ ## Key Types
294
+
295
+ ```ts
296
+ interface FileEntry {
297
+ name: string // File name
298
+ path: string // Absolute path
299
+ isDirectory: boolean // Is directory
300
+ meta?: Record<string, unknown> // Extension metadata
301
+ }
302
+
303
+ interface TreeNode extends FileEntry {
304
+ depth: number
305
+ children?: TreeNode[]
306
+ childrenLoaded: boolean
307
+ }
308
+
309
+ interface FlatNode {
310
+ node: TreeNode
311
+ depth: number
312
+ }
313
+
314
+ interface TreeState {
315
+ rootPath: string
316
+ rootNodes: TreeNode[]
317
+ expandedPaths: Set<string>
318
+ selectedPaths: Set<string>
319
+ anchorPath: string | null
320
+ renamingPath: string | null
321
+ creatingState: CreatingState | null
322
+ searchQuery: string | null
323
+ flatList: FlatNode[]
324
+ }
325
+
326
+ type TreeEvent =
327
+ | { type: 'expand'; path: string }
328
+ | { type: 'collapse'; path: string }
329
+ | { type: 'select'; paths: string[] }
330
+ | { type: 'open'; path: string }
331
+ | { type: 'rename'; oldPath: string; newPath: string }
332
+ | { type: 'delete'; paths: string[] }
333
+ | { type: 'create'; parentPath: string; name: string; isDirectory: boolean }
334
+ | { type: 'refresh'; path?: string }
335
+ | { type: 'external-change'; changes: WatchEvent[] }
336
+ ```
337
+
338
+ ## File Watching
339
+
340
+ Implementing `adapter.watch` automatically enables file watching. Just emit raw events; the core handles:
341
+
342
+ - **Debounce** (75ms, VSCode-compatible)
343
+ - **Event coalescing**: rename → delete+create, delete+create (same path) → modify, child events removed on parent delete
344
+ - **Throttling**: Chunk splitting for large batches (500 events / 200ms interval)
345
+
346
+ ```ts
347
+ const tree = createFileTree({
348
+ adapter,
349
+ rootPath: '/project',
350
+ watchOptions: {
351
+ debounceMs: 100, // Default: 75
352
+ coalesce: true, // Default: true
353
+ throttle: {
354
+ maxChunkSize: 1000, // Default: 500
355
+ delayMs: 300, // Default: 200
356
+ },
357
+ },
358
+ })
359
+ ```
360
+
361
+ ## Keybinding Integration (momoi-keybind)
362
+
363
+ When `momoi-keybind` is installed, pass an `inputService` prop to enable keybindings. `momoi-keybind` is an optional peer dependency.
364
+
365
+ ```tsx
366
+ import { InputService } from 'momoi-keybind'
367
+ import { defaultExplorerKeybindings } from 'momoi-explorer'
368
+ import { FileExplorer } from 'momoi-explorer/ui'
369
+
370
+ const inputService = new InputService({
371
+ defaultKeybindings: defaultExplorerKeybindings,
372
+ })
373
+ inputService.start()
374
+
375
+ <FileExplorer
376
+ adapter={adapter}
377
+ rootPath={rootPath}
378
+ inputService={inputService}
379
+ />
380
+ ```
381
+
382
+ **Default Keybindings:**
383
+
384
+ | Key | Command |
385
+ |---|---|
386
+ | `Delete` | Delete selected items |
387
+ | `F2` | Rename |
388
+ | `Ctrl+N` | New file |
389
+ | `Ctrl+Shift+N` | New folder |
390
+ | `Ctrl+R` | Refresh |
391
+ | `Ctrl+Shift+E` | Collapse all folders |
392
+ | `Ctrl+A` | Select all |
393
+ | `Ctrl+Shift+C` | Copy path |
394
+
395
+ Override, add, or disable keybindings on the user side:
396
+
397
+ ```ts
398
+ const inputService = new InputService({
399
+ defaultKeybindings: defaultExplorerKeybindings,
400
+ userKeybindings: [
401
+ { key: 'F2', command: 'myApp.quickPreview', when: 'explorerFocus' }, // Override
402
+ { key: '', command: '-explorer.delete' }, // Disable
403
+ ],
404
+ })
405
+ ```
406
+
407
+ ## License
408
+
409
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "momoi-explorer",
3
- "version": "0.8.0",
3
+ "version": "0.8.3",
4
4
  "description": "A headless file explorer component with React bindings and default UI",
5
5
  "author": "Waik0",
6
6
  "license": "MIT",