momoi-explorer 0.8.0 → 0.8.2

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 +407 -355
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,355 +1,407 @@
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
+ A headless file explorer library. Framework-agnostic core + React bindings + default UI in a 3-layer architecture.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install momoi-explorer
9
+ ```
10
+
11
+ ## Architecture
12
+
13
+ Three entry points with a layered architecture:
14
+
15
+ | Entry Point | Purpose | Requires React |
16
+ |---|---|---|
17
+ | `momoi-explorer` | Core engine (framework-agnostic) | No |
18
+ | `momoi-explorer/react` | React bindings (hooks + context) | Yes |
19
+ | `momoi-explorer/ui` | Default UI components | Yes |
20
+
21
+ ## Quick Start
22
+
23
+ ### 1. Implement a FileSystemAdapter
24
+
25
+ Everything starts with implementing `FileSystemAdapter`. Only `readDir` is required; other methods are optional (implementing them enables the corresponding features).
26
+
27
+ ```ts
28
+ import type { FileSystemAdapter } from 'momoi-explorer'
29
+
30
+ const adapter: FileSystemAdapter = {
31
+ // Required: return directory contents
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
+ // Optional: rename
41
+ async rename(oldPath, newPath) {
42
+ await fs.rename(oldPath, newPath)
43
+ },
44
+ // Optional: delete
45
+ async delete(paths) {
46
+ for (const p of paths) await fs.rm(p, { recursive: true })
47
+ },
48
+ // Optional: create file
49
+ async createFile(parentPath, name) {
50
+ await fs.writeFile(path.join(parentPath, name), '')
51
+ },
52
+ // Optional: create directory
53
+ async createDir(parentPath, name) {
54
+ await fs.mkdir(path.join(parentPath, name))
55
+ },
56
+ // Optional: file watching (debounce & coalescing handled by core)
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. Use the Default UI (easiest)
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. Build Custom UI with React Hooks
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. Core Only (framework-agnostic)
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
+ // Subscribe to state changes
127
+ tree.subscribe((state) => {
128
+ console.log('nodes:', state.rootNodes)
129
+ console.log('flatList:', state.flatList)
130
+ })
131
+
132
+ // Load the tree
133
+ await tree.loadRoot()
134
+
135
+ // Operations
136
+ await tree.expand('/home/user/project/src')
137
+ tree.select('/home/user/project/src/index.ts')
138
+ tree.setSearchQuery('config')
139
+
140
+ // Cleanup
141
+ tree.destroy()
142
+ ```
143
+
144
+ ## API Reference
145
+
146
+ ### Core (`momoi-explorer`)
147
+
148
+ #### `createFileTree(options): FileTreeController`
149
+
150
+ Main entry point for the headless file tree.
151
+
152
+ **FileTreeOptions:**
153
+ | Property | Type | Description |
154
+ |---|---|---|
155
+ | `adapter` | `FileSystemAdapter` | File system adapter (required) |
156
+ | `rootPath` | `string` | Absolute path to the root directory |
157
+ | `sort` | `(a, b) => number` | Custom sort function |
158
+ | `filter` | `(entry) => boolean` | Custom filter function |
159
+ | `watchOptions` | `WatchOptions` | File watching options |
160
+ | `onEvent` | `(event: TreeEvent) => void` | Event callback |
161
+
162
+ **FileTreeController Methods:**
163
+
164
+ | Method | Description |
165
+ |---|---|
166
+ | `getState()` | Get current TreeState |
167
+ | `subscribe(listener)` | Subscribe to state changes. Returns unsubscribe function |
168
+ | `loadRoot()` | Load and initialize root (must be called first) |
169
+ | `expand(path)` | Expand a directory |
170
+ | `collapse(path)` | Collapse a directory |
171
+ | `toggleExpand(path)` | Toggle expand/collapse |
172
+ | `expandTo(path)` | Expand all ancestors up to the given path |
173
+ | `select(path, mode?)` | Select a node (mode: 'replace' / 'toggle' / 'range') |
174
+ | `selectAll()` | Select all nodes |
175
+ | `clearSelection()` | Clear selection |
176
+ | `startRename(path)` | Enter rename mode |
177
+ | `commitRename(newName)` | Commit rename |
178
+ | `cancelRename()` | Cancel rename |
179
+ | `startCreate(parentPath, isDirectory, insertAfterPath?)` | Enter inline creation mode (insertAfterPath: position for the input row) |
180
+ | `commitCreate(name)` | Commit creation |
181
+ | `cancelCreate()` | Cancel creation |
182
+ | `createFile(parentPath, name)` | Create a file |
183
+ | `createDir(parentPath, name)` | Create a directory |
184
+ | `deleteSelected()` | Delete selected items |
185
+ | `refresh(path?)` | Refresh the tree (preserves expanded state) |
186
+ | `setSearchQuery(query)` | Set fuzzy search query (null to clear) |
187
+ | `collectAllFiles()` | Recursively collect all files (for QuickOpen) |
188
+ | `setFilter(fn)` | Dynamically change the filter function |
189
+ | `setSort(fn)` | Dynamically change the sort function |
190
+ | `destroy()` | Destroy the controller (stops watching, clears subscriptions) |
191
+
192
+ #### Utility Functions
193
+
194
+ | Function | Description |
195
+ |---|---|
196
+ | `flattenTree(nodes, expandedPaths, matchingPaths?)` | Convert tree to flat list |
197
+ | `computeSelection(current, anchor, target, mode, flatList)` | Compute selection state |
198
+ | `fuzzyMatch(query, target)` | Fuzzy match (match + score) |
199
+ | `fuzzyFind(files, query, maxResults?)` | Fuzzy search sorted by score |
200
+ | `findMatchingPaths(nodes, query)` | Return Set of matching paths |
201
+ | `coalesceEvents(raw)` | Coalesce raw watch events |
202
+ | `createEventProcessor(callback, options?)` | Event processor with debounce |
203
+ | `defaultSort(a, b)` | Default sort (directories first, name ascending) |
204
+ | `defaultFilter(entry)` | Default filter (show all) |
205
+ | `ExplorerCommands` | Explorer command ID constants (DELETE, RENAME, etc.) |
206
+ | `defaultExplorerKeybindings` | Default keybinding definitions (for momoi-keybind) |
207
+
208
+ ### React (`momoi-explorer/react`)
209
+
210
+ | Export | Kind | Description |
211
+ |---|---|---|
212
+ | `TreeProvider` | Component | File tree context provider. Calls `createFileTree` + `loadRoot` internally |
213
+ | `useFileTree()` | Hook | Returns full tree state and controller |
214
+ | `useTreeNode(path)` | Hook | Returns expand/select/rename state for a node (null if not found) |
215
+ | `useContextMenu()` | Hook | Context menu visibility control (show/hide + position) |
216
+ | `useExplorerKeybindings(inputService)` | Hook | momoi-keybind integration. Registers explorer command handlers |
217
+ | `useExplorerFocus(inputService)` | Hook | Syncs focus state with momoi-keybind context |
218
+ | `useTreeContext()` | Hook | Raw TreeContext value (usually use useFileTree instead) |
219
+
220
+ ### UI (`momoi-explorer/ui`)
221
+
222
+ | Export | Description |
223
+ |---|---|
224
+ | `FileExplorer` | All-in-one component (includes TreeProvider, virtual scrolling, context menu) |
225
+ | `TreeNodeRow` | Single tree row (icon, indent, selection, rename) |
226
+ | `ContextMenu` | Right-click menu (closes on outside click/Esc) |
227
+ | `InlineRename` | Inline rename input (Enter to confirm, Esc to cancel) |
228
+ | `TreeFilterBar` | Fuzzy search filter bar |
229
+ | `QuickOpen` | VSCode-style quick open dialog (Ctrl+P equivalent) |
230
+
231
+ **Styles:**
232
+
233
+ ```ts
234
+ import 'momoi-explorer/ui/style.css'
235
+ ```
236
+
237
+ VSCode-style dark theme. Customizable via CSS variables and class names (`.momoi-explorer-*`).
238
+
239
+ ### FileExplorer Props
240
+
241
+ ```tsx
242
+ <FileExplorer
243
+ adapter={adapter} // FileSystemAdapter (required)
244
+ rootPath="/path/to/dir" // Root path (required)
245
+ sort={(a, b) => ...} // Custom sort
246
+ filter={(entry) => ...} // Custom filter
247
+ watchOptions={{ ... }} // File watching options
248
+ onEvent={(e) => ...} // Tree event callback
249
+ onOpen={(path) => ...} // File double-click handler
250
+ renderIcon={(node, expanded) => ...} // Custom icon renderer
251
+ renderBadge={(node) => ...} // Custom badge renderer (e.g. git status)
252
+ contextMenuItems={(nodes) => [...]} // Context menu items
253
+ showFilterBar // Show filter bar
254
+ onControllerReady={(ctrl) => ...} // Get controller reference
255
+ inputService={inputService} // momoi-keybind InputService instance (optional)
256
+ onKeyDown={(e) => ...} // Key event handler when not using momoi-keybind
257
+ className="my-explorer" // CSS class
258
+ style={{ height: 400 }} // Inline styles
259
+ />
260
+ ```
261
+
262
+ ### QuickOpen Usage
263
+
264
+ ```tsx
265
+ import { FileExplorer, QuickOpen } from 'momoi-explorer/ui'
266
+
267
+ function App() {
268
+ const [ctrl, setCtrl] = useState<FileTreeController | null>(null)
269
+ const [quickOpen, setQuickOpen] = useState(false)
270
+
271
+ return (
272
+ <>
273
+ <FileExplorer
274
+ adapter={adapter}
275
+ rootPath={rootPath}
276
+ onControllerReady={setCtrl}
277
+ />
278
+ {ctrl && (
279
+ <QuickOpen
280
+ controller={ctrl}
281
+ isOpen={quickOpen}
282
+ onClose={() => setQuickOpen(false)}
283
+ onSelect={(entry) => openFile(entry.path)}
284
+ />
285
+ )}
286
+ </>
287
+ )
288
+ }
289
+ ```
290
+
291
+ ## Key Types
292
+
293
+ ```ts
294
+ interface FileEntry {
295
+ name: string // File name
296
+ path: string // Absolute path
297
+ isDirectory: boolean // Is directory
298
+ meta?: Record<string, unknown> // Extension metadata
299
+ }
300
+
301
+ interface TreeNode extends FileEntry {
302
+ depth: number
303
+ children?: TreeNode[]
304
+ childrenLoaded: boolean
305
+ }
306
+
307
+ interface FlatNode {
308
+ node: TreeNode
309
+ depth: number
310
+ }
311
+
312
+ interface TreeState {
313
+ rootPath: string
314
+ rootNodes: TreeNode[]
315
+ expandedPaths: Set<string>
316
+ selectedPaths: Set<string>
317
+ anchorPath: string | null
318
+ renamingPath: string | null
319
+ creatingState: CreatingState | null
320
+ searchQuery: string | null
321
+ flatList: FlatNode[]
322
+ }
323
+
324
+ type TreeEvent =
325
+ | { type: 'expand'; path: string }
326
+ | { type: 'collapse'; path: string }
327
+ | { type: 'select'; paths: string[] }
328
+ | { type: 'open'; path: string }
329
+ | { type: 'rename'; oldPath: string; newPath: string }
330
+ | { type: 'delete'; paths: string[] }
331
+ | { type: 'create'; parentPath: string; name: string; isDirectory: boolean }
332
+ | { type: 'refresh'; path?: string }
333
+ | { type: 'external-change'; changes: WatchEvent[] }
334
+ ```
335
+
336
+ ## File Watching
337
+
338
+ Implementing `adapter.watch` automatically enables file watching. Just emit raw events; the core handles:
339
+
340
+ - **Debounce** (75ms, VSCode-compatible)
341
+ - **Event coalescing**: rename → delete+create, delete+create (same path) → modify, child events removed on parent delete
342
+ - **Throttling**: Chunk splitting for large batches (500 events / 200ms interval)
343
+
344
+ ```ts
345
+ const tree = createFileTree({
346
+ adapter,
347
+ rootPath: '/project',
348
+ watchOptions: {
349
+ debounceMs: 100, // Default: 75
350
+ coalesce: true, // Default: true
351
+ throttle: {
352
+ maxChunkSize: 1000, // Default: 500
353
+ delayMs: 300, // Default: 200
354
+ },
355
+ },
356
+ })
357
+ ```
358
+
359
+ ## Keybinding Integration (momoi-keybind)
360
+
361
+ When `momoi-keybind` is installed, pass an `inputService` prop to enable keybindings. `momoi-keybind` is an optional peer dependency.
362
+
363
+ ```tsx
364
+ import { InputService } from 'momoi-keybind'
365
+ import { defaultExplorerKeybindings } from 'momoi-explorer'
366
+ import { FileExplorer } from 'momoi-explorer/ui'
367
+
368
+ const inputService = new InputService({
369
+ defaultKeybindings: defaultExplorerKeybindings,
370
+ })
371
+ inputService.start()
372
+
373
+ <FileExplorer
374
+ adapter={adapter}
375
+ rootPath={rootPath}
376
+ inputService={inputService}
377
+ />
378
+ ```
379
+
380
+ **Default Keybindings:**
381
+
382
+ | Key | Command |
383
+ |---|---|
384
+ | `Delete` | Delete selected items |
385
+ | `F2` | Rename |
386
+ | `Ctrl+N` | New file |
387
+ | `Ctrl+Shift+N` | New folder |
388
+ | `Ctrl+R` | Refresh |
389
+ | `Ctrl+Shift+E` | Collapse all folders |
390
+ | `Ctrl+A` | Select all |
391
+ | `Ctrl+Shift+C` | Copy path |
392
+
393
+ Override, add, or disable keybindings on the user side:
394
+
395
+ ```ts
396
+ const inputService = new InputService({
397
+ defaultKeybindings: defaultExplorerKeybindings,
398
+ userKeybindings: [
399
+ { key: 'F2', command: 'myApp.quickPreview', when: 'explorerFocus' }, // Override
400
+ { key: '', command: '-explorer.delete' }, // Disable
401
+ ],
402
+ })
403
+ ```
404
+
405
+ ## License
406
+
407
+ 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.2",
4
4
  "description": "A headless file explorer component with React bindings and default UI",
5
5
  "author": "Waik0",
6
6
  "license": "MIT",