@aiquants/directory-tree 3.0.5 → 3.1.1
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/README.md +25 -8
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/styles/directory-tree.css +1 -1
- package/dist/styles/directory-tree.standalone.css +3 -0
- package/package.json +7 -5
- package/src/DirectoryTree.spec.md +473 -0
- package/src/DirectoryTree.tsx +978 -0
- package/src/DirectoryTreeGrid.tsx +261 -0
- package/src/TreeLine.tsx +300 -0
- package/src/cli.server.ts +38 -0
- package/src/entryRendering.tsx +131 -0
- package/src/gridUtils.ts +88 -0
- package/src/index.ts +31 -0
- package/src/logger.ts +162 -0
- package/src/styles/components.entry.css +10 -0
- package/src/styles/directory-tree.css +87 -0
- package/src/styles/standalone.entry.css +12 -0
- package/src/types.ts +619 -0
- package/src/useDirectoryTreeState.ts +133 -0
- package/src/useEntryInteraction.ts +74 -0
- package/dist/directory-tree.css +0 -1
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
# DirectoryTree Component Specification
|
|
2
|
+
|
|
3
|
+
## 1. Revision History
|
|
4
|
+
|
|
5
|
+
| Version | Date | Author | Reviewer | Approver | Summary of Changes |
|
|
6
|
+
| :--- | :--- | :--- | :--- | :--- | :--- |
|
|
7
|
+
| 3.0.1 | 2026-07-07 | Antigravity | | | Fix: `columns[0].className` is now correctly applied to the name column cells in the header and footer (TreeGrid mode), ensuring consistency with body rows and data columns. |
|
|
8
|
+
| 3.0.0 | 2026-07-07 | Claude Opus 4.8 | | | Add TreeGrid (columns) mode via the optional `grid` prop (`DirectoryTreeGrid` / `DirectoryTreeColumn`). The name/tree column is frozen on the left (indent + connector lines confined there); the remaining columns render as a vertically-aligned grid with an aligned column header, an optional footer that aggregates over **all** entries (collapsed subtrees included), and a horizontally scrollable numeric region (the name column stays frozen). Grid mode uses `role="treegrid"` / `row` / `gridcell` / `columnheader`. Enabled by a new opt-in `onWheelHorizontal` prop on `@aiquants/virtualscroll` (axis-aware wheel). `grid` and each `render` / `footer` must be stable references. Note: connector lines are rendered on a `<canvas>` (this document previously described them as a single SVG component — corrected here). |
|
|
9
|
+
| 2.2.0 | 2026-07-06 | Claude Opus 4.8 | | | Add `visual.itemHeight` (`number \| ((entry, index) => number)`, default 20) to make row height configurable. Variable-height rows are supported: the tree connector geometry and virtual-scroll range now use cumulative per-row offsets instead of a uniform pitch. Invalid values (`NaN` / `Infinity` / `0` / negative) fall back to 20px. |
|
|
10
|
+
| 1.6 | 2026-06-15 | Antigravity | | | Add visual styling props (entryClassName, entryStyle, nameClassName, etc.) to allow custom css and inline styles for entries and name labels, eliminating downstream arbitrary selector hacks. |
|
|
11
|
+
| 1.5 | 2025-11-10 | GitHub Copilot | | | Add showDirectoryIcons and showFileIcons props to control type icon visibility. |
|
|
12
|
+
| 1.4 | 2025-11-10 | GitHub Copilot | | | Add showExpandIcons prop to toggle directory expand icon visibility. |
|
|
13
|
+
| 1.3 | 2025-11-10 | GitHub Copilot | | | Add showTreeLines prop to allow toggling connector line rendering. |
|
|
14
|
+
| 1.2 | 2025-09-12 | GitHub Copilot | | | Update lineColor prop to be configurable instead of using internal theme detection. Theme control moved to calling component. |
|
|
15
|
+
| 1.1 | 2025-06-29 | GitHub Copilot | | | Add virtualization support using `useVirtualizer` hook. |
|
|
16
|
+
| 1.0 | 2025-06-28 | Copilot | | | Newly created |
|
|
17
|
+
|
|
18
|
+
## 2. Introduction
|
|
19
|
+
|
|
20
|
+
### 2.1. Document Purpose
|
|
21
|
+
|
|
22
|
+
This document provides the detailed design specifications for the `DirectoryTree` React component.
|
|
23
|
+
本書は `DirectoryTree` React コンポーネントの詳細設計仕様書です。
|
|
24
|
+
|
|
25
|
+
### 2.2. Background
|
|
26
|
+
|
|
27
|
+
In applications that handle file and directory structures, there's a need for a UI component that can display hierarchical data efficiently. The previous implementation lacked performance with large datasets and had a rigid styling system.
|
|
28
|
+
ファイルやディレクトリ構造を扱うアプリケーションでは、階層データを効率的に表示する UI コンポーネントが必要です。以前の実装は、大規模なデータセットに対するパフォーマンスが低く、柔軟性のないスタイリングシステムでした。
|
|
29
|
+
|
|
30
|
+
### 2.3. Objective
|
|
31
|
+
|
|
32
|
+
To provide a highly performant, customizable, and easy-to-use directory tree component. This component uses Tailwind CSS for flexible styling.
|
|
33
|
+
パフォーマンスが高く、カスタマイズ可能で、使いやすいディレクトリツリーコンポーネントを提供することを目的とします。このコンポーネントは、Tailwind CSS を使用して柔軟なスタイリングを実現します。
|
|
34
|
+
|
|
35
|
+
### 2.4. Scope
|
|
36
|
+
|
|
37
|
+
#### 2.4.1. In Scope
|
|
38
|
+
|
|
39
|
+
* Displaying a hierarchical directory structure based on the `DirectoryEntry` type.
|
|
40
|
+
`DirectoryEntry` 型に基づいた階層的なディレクトリ構造の表示。
|
|
41
|
+
* Expanding and collapsing individual directories.
|
|
42
|
+
個々のディレクトリの展開と折りたたみ。
|
|
43
|
+
* Recursively expanding and collapsing a directory and its children (e.g., via double-click).
|
|
44
|
+
ディレクトリとその子要素の再帰的な展開と折りたたみ (例: ダブルクリック)。
|
|
45
|
+
* File selection callback.
|
|
46
|
+
ファイル選択時のコールバック機能。
|
|
47
|
+
* Customizable styling via `className` and `style` props.
|
|
48
|
+
`className` および `style` props を介したカスタマイズ可能なスタイリング。
|
|
49
|
+
* Persisting the expansion state of each entity in `localStorage`.
|
|
50
|
+
`localStorage` に各エンティティの展開状態を保存して維持する。
|
|
51
|
+
* Explicitly handling `absolutePath` and `relativePath` within the `DirectoryTree`.
|
|
52
|
+
`DirectoryTree` 内部で `absolutePath` と `relativePath` を明示的に扱う。
|
|
53
|
+
* Improving user experience by using `startTransition` for state management of expansion.
|
|
54
|
+
展開状態の管理に `startTransition` を用いてUXを向上させる。
|
|
55
|
+
* Rendering tree connector lines on a `<canvas>` overlay to visually represent the hierarchy and the open/closed state of directories.
|
|
56
|
+
`<canvas>` オーバーレイにツリー接続線を描画し、ディレクトリの階層と開閉状態を視覚的に表現する。
|
|
57
|
+
* Virtualization of directory items for improved performance with large directories (via `@aiquants/virtualscroll`).
|
|
58
|
+
ディレクトリアイテムの仮想化により、大規模なディレクトリでのパフォーマンスを向上させる(`@aiquants/virtualscroll` を使用)。
|
|
59
|
+
* TreeGrid (columns) mode: a frozen name/tree column plus vertically-aligned data columns with an aligned header, an optional all-entries footer, and a horizontally scrollable numeric region — enabled via the optional `grid` prop.
|
|
60
|
+
TreeGrid(列)モード: 凍結した名前/ツリー列と縦整列したデータ列、整列ヘッダ、任意の全 entries フッタ、数値領域の横スクロールを、任意の `grid` prop で提供する。
|
|
61
|
+
|
|
62
|
+
#### 2.4.2. Out of Scope
|
|
63
|
+
|
|
64
|
+
* File operations (create, delete, rename).
|
|
65
|
+
ファイル操作 (作成、削除、名前変更)。
|
|
66
|
+
* Drag-and-drop functionality.
|
|
67
|
+
ドラッグアンドドロップ機能。
|
|
68
|
+
* Context menu integration.
|
|
69
|
+
コンテキストメニューの統合。
|
|
70
|
+
|
|
71
|
+
#### 2.4.3. Future Expansion (Optional)
|
|
72
|
+
|
|
73
|
+
* Integration with a file system API to dynamically load directory contents.
|
|
74
|
+
ファイルシステム API と統合し、ディレクトリの内容を動的に読み込む機能。
|
|
75
|
+
* Support for custom icons based on file type or directory state.
|
|
76
|
+
ファイルタイプやディレクトリの状態に応じたカスタムアイコンのサポート。
|
|
77
|
+
|
|
78
|
+
### 2.5. Target Audience
|
|
79
|
+
|
|
80
|
+
This document is intended for developers who will use or contribute to the `DirectoryTree` component, as well as UI/UX designers who need to understand its capabilities.
|
|
81
|
+
本書は `DirectoryTree` コンポーネントを使用または開発に貢献する開発者、およびその機能を理解する必要がある UI/UX デザイナーを対象としています。
|
|
82
|
+
|
|
83
|
+
### 2.6. References
|
|
84
|
+
|
|
85
|
+
* `DirectoryEntry` type definition: [DirectoryTree.tsx](./DirectoryTree.tsx)
|
|
86
|
+
* `useDirectoryTreeState` hook: [useDirectoryTreeState.ts](./useDirectoryTreeState.ts)
|
|
87
|
+
* `TreeLine` component: [TreeLine.tsx](./TreeLine.tsx)
|
|
88
|
+
|
|
89
|
+
### 2.7. Prerequisites and Constraints
|
|
90
|
+
|
|
91
|
+
* The component must be used within a React application.
|
|
92
|
+
コンポーネントは React アプリケーション内で使用する必要があります。
|
|
93
|
+
* Styling is managed via Tailwind CSS and `tailwind-variants`. Proper configuration of Tailwind CSS is required.
|
|
94
|
+
スタイリングは Tailwind CSS と `tailwind-variants` を介して管理されます。Tailwind CSS の適切な設定が必要です。
|
|
95
|
+
* Input data must conform to the `DirectoryEntry[]` type structure.
|
|
96
|
+
入力データは `DirectoryEntry[]` 型の構造に準拠する必要があります。
|
|
97
|
+
|
|
98
|
+
### 2.8. Architectural Policy and Technology Selection
|
|
99
|
+
|
|
100
|
+
* **State Management**: The expansion state of the tree is managed by the `useDirectoryTreeState` hook, separating state logic from the presentation component.
|
|
101
|
+
**状態管理**: ツリーの展開状態は `useDirectoryTreeState` フックによって管理され、状態ロジックをプレゼンテーションコンポーネントから分離します。
|
|
102
|
+
* **Styling**: `tailwind-variants` is used for creating a flexible and customizable styling system based on component state and props.
|
|
103
|
+
**スタイリング**: `tailwind-variants` を使用して、コンポーネントの状態と Props に基づいた柔軟でカスタマイズ可能なスタイリングシステムを構築します。
|
|
104
|
+
* **Componentization**: The component is broken down into smaller, manageable parts like `DirectoryEntryItem` and `TreeLine` to improve maintainability and reusability.
|
|
105
|
+
**コンポーネント化**: コンポーネントは `DirectoryEntryItem` や `TreeLine` のような小さく管理しやすいパーツに分割され、保守性と再利用性を向上させています。
|
|
106
|
+
|
|
107
|
+
## 3. Component API
|
|
108
|
+
|
|
109
|
+
### 3.1. Data Structures
|
|
110
|
+
|
|
111
|
+
#### 3.1.1. `DirectoryEntry`
|
|
112
|
+
|
|
113
|
+
The fundamental data structure for representing a file or directory.
|
|
114
|
+
ファイルまたはディレクトリを表す基本的なデータ構造。
|
|
115
|
+
|
|
116
|
+
| Property | Type | Description |
|
|
117
|
+
| :--- | :--- | :--- |
|
|
118
|
+
| `name` | `string` | The name of the file or directory. ファイルまたはディレクトリの名前。 |
|
|
119
|
+
| `absolutePath` | `string` | The absolute path of the entry. エントリの絶対パス。 |
|
|
120
|
+
| `relativePath` | `string` | The path of the entry relative to the root. ルートからの相対パス。 |
|
|
121
|
+
| `children` | `DirectoryEntry[] \| null` | An array of child entries if it's a directory, otherwise `null`. ディレクトリの場合は子エントリの配列、それ以外の場合は `null`。 |
|
|
122
|
+
|
|
123
|
+
### 3.2. Props
|
|
124
|
+
|
|
125
|
+
The `DirectoryTree` component accepts the following props:
|
|
126
|
+
`DirectoryTree` コンポーネントは、以下の props を受け入れます。
|
|
127
|
+
|
|
128
|
+
| Prop Name | Type | Required | Default | Description |
|
|
129
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
130
|
+
| `entries` | `DirectoryEntry[]` | Yes | - | The array of root directory entries to display. 表示するルートディレクトリのエントリの配列。 |
|
|
131
|
+
| `expansion` | `object` | Yes | - | Configuration for tree expansion state and behavior. ツリーの展開状態と動作の設定。 |
|
|
132
|
+
| `selection` | `object` | Yes | - | Configuration for item selection. アイテム選択の設定。 |
|
|
133
|
+
| `visual` | `object` | No | - | Visual customization options. 表示のカスタマイズオプション。 |
|
|
134
|
+
| `virtualScroll` | `DirectoryTreeVirtualScrollOptions` | No | - | Additional options for VirtualScroll integration. VirtualScroll の設定を上書きするオプション。 |
|
|
135
|
+
| `grid` | `DirectoryTreeGrid` | No | - | TreeGrid (columns) mode. When present, renders a frozen name column + aligned data columns with header/footer and a horizontally scrollable numeric region. See §3.2.4. TreeGrid(列)モード。指定時は凍結名前列+整列データ列を描画する(§3.2.4)。 |
|
|
136
|
+
|
|
137
|
+
#### 3.2.1. `expansion` Configuration
|
|
138
|
+
|
|
139
|
+
| Property | Type | Required | Default | Description |
|
|
140
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
141
|
+
| `toggle` | `(path: string) => void` | Yes | - | Function to toggle directory expansion state. ディレクトリの展開状態を切り替える関数。 |
|
|
142
|
+
| `isExpanded` | `(path: string) => boolean` | Yes | - | Function to check if a directory is expanded. ディレクトリが展開されているかを確認する関数。 |
|
|
143
|
+
| `expandMultiple` | `(paths: string[]) => void` | Yes | - | Function to expand multiple directories. 複数のディレクトリを展開する関数。 |
|
|
144
|
+
| `collapseMultiple` | `(paths: string[]) => void` | Yes | - | Function to collapse multiple directories. 複数のディレクトリを折りたたむ関数。 |
|
|
145
|
+
| `isPending` | `boolean` | No | `false` | Whether the tree is in a pending state. ツリーが保留状態かどうか。 |
|
|
146
|
+
| `alwaysExpanded` | `boolean` | No | `false` | If true, all directories are always expanded. true の場合、すべてのディレクトリが常に展開されます。 |
|
|
147
|
+
| `doubleClickAction` | `'recursive' \| 'toggle'` | No | `'recursive'` | Action on double-clicking a directory. ディレクトリをダブルクリックしたときのアクション。 |
|
|
148
|
+
|
|
149
|
+
#### 3.2.2. `selection` Configuration
|
|
150
|
+
|
|
151
|
+
| Property | Type | Required | Default | Description |
|
|
152
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
153
|
+
| `onEntryClick` | `(event: DirectoryTreeClickEvent) => void` | Yes | - | Callback triggered when an entry is clicked. エントリがクリックされたときのコールバック。 |
|
|
154
|
+
| `selectedPath` | `string \| null` | No | - | The currently selected path. 現在選択されているパス。 |
|
|
155
|
+
| `mode` | `'none' \| 'single' \| 'multiple'` | No | `'none'` | Selection mode for items. アイテムの選択モード。 |
|
|
156
|
+
| `selectedItems` | `Set<string>` | No | - | Set of paths for currently selected items. 現在選択されているアイテムのパスのセット。 |
|
|
157
|
+
| `onSelectionChange` | `(entry: DirectoryEntry, isSelected: boolean) => void` | No | - | Callback when item selection changes. アイテム選択状態変更時のコールバック。 |
|
|
158
|
+
|
|
159
|
+
#### 3.2.3. `visual` Configuration
|
|
160
|
+
|
|
161
|
+
| Property | Type | Required | Default | Description |
|
|
162
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
163
|
+
| `className` | `string` | No | - | Additional CSS classes to apply to the root container. コンテナ用のオプション CSS クラス名。 |
|
|
164
|
+
| `style` | `React.CSSProperties` | No | - | Inline styles to apply to the root container. コンテナ用のオプションのインラインスタイル。 |
|
|
165
|
+
| `lineColor` | `string` | No | `'#A0AEC0'` | The color of the tree lines. ツリーラインの色。 |
|
|
166
|
+
| `showTreeLines` | `boolean` | No | `true` | Whether to render connector tree lines. ツリーのコネクタラインを描画するかどうか。 |
|
|
167
|
+
| `showExpandIcons` | `boolean` | No | `true` | Whether to render directory expand icons. ディレクトリの展開アイコンを描画するかどうか。 |
|
|
168
|
+
| `showDirectoryIcons` | `boolean` | No | `true` | Whether to render directory type icons. ディレクトリアイコンを描画するかどうか。 |
|
|
169
|
+
| `showFileIcons` | `boolean` | No | `true` | Whether to render file type icons. ファイルアイコンを描画するかどうか。 |
|
|
170
|
+
| `iconOverrides` | `DirectoryTreeIconOverrides` | No | - | Icon overrides applied globally. グローバルに適用するアイコン差し替え設定。 |
|
|
171
|
+
| `expandIconSize` | `number` | No | - | Size of the expand icon. 展開アイコンのサイズ。 |
|
|
172
|
+
| `itemHeight` | `number \| ((entry, index) => number)` | No | `20` | Row height in px, or a function computing the height per entry (variable-height rows). Tree connector lines follow each row's cumulative offset. Invalid values (`NaN` / `Infinity` / `0` / negative) fall back to `20`. Memoize the function to keep its identity stable. 各行の高さ (px)、またはエントリごとに高さを算出する関数(可変高さ行)。ツリー接続線は各行の累積オフセットに追従する。不正値(`NaN` / `Infinity` / `0` / 負値)は `20` にフォールバックする。関数は identity を安定させること。 |
|
|
173
|
+
| `removeRootIndent` | `boolean` | No | `false` | If true, removes indentation for root-level items. ルートレベルのアイテムのインデントを削除するかどうか。 |
|
|
174
|
+
| `highlightStyles` | `HighlightStyles` | No | - | Custom hover and selection styles. ホバー時および選択時のカスタムスタイル。 |
|
|
175
|
+
| `entryClassName` | `string` | No | - | Additional CSS classes for each entry row. 各エントリ(行)に適用する追加の CSS クラス名。 |
|
|
176
|
+
| `entryStyle` | `React.CSSProperties` | No | - | Additional inline styles for each entry row. 各エントリ(行)に適用する追加のインラインスタイル。 |
|
|
177
|
+
| `nameClassName` | `string` | No | - | Additional CSS classes for the name label. 名前(ラベル)部分に適用する追加の CSS クラス名。 |
|
|
178
|
+
| `nameStyle` | `React.CSSProperties` | No | - | Additional inline styles for the name label. 名前(ラベル)部分に適用する追加のインラインスタイル。 |
|
|
179
|
+
| `directoryNameClassName` | `string` | No | - | Additional CSS classes specifically for directory names. ディレクトリ名特有の追加の CSS クラス名。 |
|
|
180
|
+
| `directoryNameStyle` | `React.CSSProperties` | No | - | Additional inline styles specifically for directory names. ディレクトリ名特有の追加のインラインスタイル。 |
|
|
181
|
+
| `fileNameClassName` | `string` | No | - | Additional CSS classes specifically for file names. ファイル名特有の追加の CSS クラス名。 |
|
|
182
|
+
| `fileNameStyle` | `React.CSSProperties` | No | - | Additional inline styles specifically for file names. ファイル名特有の追加のインラインスタイル。 |
|
|
183
|
+
|
|
184
|
+
#### 3.2.4. `grid` Configuration (TreeGrid columns mode)
|
|
185
|
+
|
|
186
|
+
When `grid` is provided, the tree renders as a **TreeGrid**: the name/tree column is frozen on the left
|
|
187
|
+
(indentation and connector lines stay confined there) while the remaining columns render as a
|
|
188
|
+
vertically-aligned grid — with an aligned column header, an optional footer aggregating over **all**
|
|
189
|
+
entries, and a horizontally scrollable numeric region. When omitted, the classic label tree is rendered.
|
|
190
|
+
`grid` 指定時は TreeGrid として描画する(凍結名前列+整列データ列+整列ヘッダ/フッタ+数値領域横スクロール)。省略時は従来のラベルツリー。
|
|
191
|
+
|
|
192
|
+
`DirectoryTreeGrid`:
|
|
193
|
+
|
|
194
|
+
| Property | Type | Required | Default | Description |
|
|
195
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
196
|
+
| `columns` | `DirectoryTreeColumn[]` | Yes | - | Column definitions (**stable reference required**). `columns[0]` is always the name/tree column. 列定義(参照安定必須)。`columns[0]` が名前/ツリー列。 |
|
|
197
|
+
| `showHeader` | `boolean` | No | `true` | Render the aligned column header row. 整列した列ヘッダ行を描画するか。 |
|
|
198
|
+
| `showFooter` | `boolean` | No | `false` | Render the footer row (aggregates over all entries). フッタ行(全 entries 集計)を描画するか。 |
|
|
199
|
+
| `headerClassName` | `string` | No | - | Extra class for the header row. ヘッダ行の追加クラス。 |
|
|
200
|
+
| `footerClassName` | `string` | No | - | Extra class for the footer row. フッタ行の追加クラス。 |
|
|
201
|
+
| `rowClassName` | `string \| ((entry) => string)` | No | - | Extra class (or per-entry factory) for body rows. 行の追加クラス(またはエントリ別ファクトリ)。 |
|
|
202
|
+
| `scrollBarWidth` | `number` | No | `virtualScroll.scrollBarOptions.width ?? 12` | Vertical scrollbar width used to align the header/footer numeric viewport with the body. 縦バー幅(ヘッダ/フッタと本体の数値ビューポート整合に使用)。 |
|
|
203
|
+
|
|
204
|
+
`DirectoryTreeColumn`:
|
|
205
|
+
|
|
206
|
+
| Property | Type | Required | Default | Description |
|
|
207
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
208
|
+
| `key` | `string` | Yes | - | Unique column key. 一意なキー。 |
|
|
209
|
+
| `header` | `ReactNode` | No | - | Column header content. 列ヘッダ内容。 |
|
|
210
|
+
| `footer` | `ReactNode \| ((allEntries: DirectoryEntry[]) => ReactNode)` | No | - | Footer content. The function form receives **every node of `entries` (collapsed subtrees included)**. フッタ内容。関数形は entries の全ノード(折り畳み含む)を受け取る。 |
|
|
211
|
+
| `width` | `number` | No | `240` (name) / `96` (data) | Column width in px (name column = frozen width; data column = grid track). 列幅 (px)。 |
|
|
212
|
+
| `align` | `'left' \| 'right' \| 'center'` | No | `'left'` | Cell alignment. セル寄せ。 |
|
|
213
|
+
| `className` | `string` | No | - | Extra class for this column's cells. この列のセルの追加クラス。 |
|
|
214
|
+
| `render` | `(entry, ctx: { indentLevel; isDirectory; isExpanded }) => ReactNode` | No* | - | Cell renderer. In the name column, indent/glyph/icon are drawn by the library and this output is the label (falls back to `entry.label ?? entry.name`). *Required for data columns. セル描画。名前列では indent/glyph/icon はライブラリが描画。 |
|
|
215
|
+
|
|
216
|
+
**Rendering rules / behavior:**
|
|
217
|
+
|
|
218
|
+
1. Indentation, the expand glyph, and the type icon are applied to the **name cell only**; data cells carry no per-depth offset, so numeric columns align across the whole hierarchy.
|
|
219
|
+
2. The name column is **frozen** at `x=0`; only the data region scrolls horizontally, driven by the `--dt-hscroll` CSS variable shared by the header, every body row, and the footer (so they stay aligned).
|
|
220
|
+
3. The header/body/footer numeric viewport width all resolve to `measuredWidth − nameWidth − scrollBarWidth`; `grid.scrollBarWidth` defaults to the actual VirtualScroll scrollbar width so columns stay aligned even when it is customized.
|
|
221
|
+
4. Grid mode uses `role="treegrid"` / `row` / `gridcell` / `columnheader`; `aria-expanded` / `aria-level` are set on the row.
|
|
222
|
+
5. `grid.columns` and each `render` / `footer` **must be stable references** (memoize them) — otherwise every visible row re-renders.
|
|
223
|
+
6. `visual.removeRootIndent` is **ignored in grid mode** (the frozen name column must start at `x=0`).
|
|
224
|
+
7. With empty `entries`, grid mode still renders the header and footer (footer aggregates over 0 entries) with the `treegrid` role; only the body has no rows.
|
|
225
|
+
8. Horizontal scrolling only updates the shared CSS variable — the virtualized body is **not** re-rendered (the `VirtualScroll` element is memoized and the rows are `React.memo`-skipped).
|
|
226
|
+
|
|
227
|
+
### 3.3. Usage
|
|
228
|
+
|
|
229
|
+
Here is a basic example of how to use the `DirectoryTree` component with the `useDirectoryTreeState` hook.
|
|
230
|
+
これは `DirectoryTree` コンポーネントを `useDirectoryTreeState` フックと共に使用する基本的な使用例です。
|
|
231
|
+
|
|
232
|
+
**Note:** This component is designed to be used with the `useDirectoryTreeState` hook to manage the expansion state. The `lineColor` can be controlled based on the current theme.
|
|
233
|
+
**注:** このコンポーネントは、展開状態を管理するために `useDirectoryTreeState` フックと共に使用するように設計されています。`lineColor` は現在のテーマに基づいて制御できます。
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
import React, { useState } from 'react'
|
|
237
|
+
import { DirectoryTree, DirectoryEntry } from './DirectoryTree' // Adjust the import path
|
|
238
|
+
import { useDirectoryTreeState } from './useDirectoryTreeState' // Adjust the import path
|
|
239
|
+
import { useTheme } from './useTheme' // Adjust the import path
|
|
240
|
+
|
|
241
|
+
const sampleEntries: DirectoryEntry[] = [
|
|
242
|
+
{
|
|
243
|
+
name: 'src',
|
|
244
|
+
absolutePath: '/src',
|
|
245
|
+
relativePath: 'src',
|
|
246
|
+
children: [
|
|
247
|
+
{
|
|
248
|
+
name: 'components',
|
|
249
|
+
absolutePath: '/src/components',
|
|
250
|
+
relativePath: 'src/components',
|
|
251
|
+
children: [
|
|
252
|
+
{
|
|
253
|
+
name: 'Button.tsx',
|
|
254
|
+
absolutePath: '/src/components/Button.tsx',
|
|
255
|
+
relativePath: 'src/components/Button.tsx',
|
|
256
|
+
children: null,
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: 'App.tsx',
|
|
262
|
+
absolutePath: '/src/App.tsx',
|
|
263
|
+
relativePath: 'src/App.tsx',
|
|
264
|
+
children: null,
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
name: 'package.json',
|
|
270
|
+
absolutePath: '/package.json',
|
|
271
|
+
relativePath: 'package.json',
|
|
272
|
+
children: null,
|
|
273
|
+
},
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
const App = () => {
|
|
277
|
+
const [selectedFile, setSelectedFile] = useState<string | null>(null)
|
|
278
|
+
const { theme } = useTheme()
|
|
279
|
+
const treeState = useDirectoryTreeState({
|
|
280
|
+
storageKey: 'my-tree-state',
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
// Calculate line color based on theme
|
|
284
|
+
const lineColor = theme === "dark" ? "#4A5568" : "#A0AEC0"
|
|
285
|
+
|
|
286
|
+
const handleEntryClick = (event: DirectoryTreeClickEvent) => {
|
|
287
|
+
console.log(`Entry clicked: ${event.entry.absolutePath}`)
|
|
288
|
+
setSelectedFile(event.entry.absolutePath)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<div style={{ width: '300px', height: '500px', border: '1px solid #ccc' }}>
|
|
293
|
+
<DirectoryTree
|
|
294
|
+
entries={sampleEntries}
|
|
295
|
+
onEntryClick={handleEntryClick}
|
|
296
|
+
selectedPath={selectedFile}
|
|
297
|
+
toggle={treeState.toggle}
|
|
298
|
+
isExpanded={treeState.isExpanded}
|
|
299
|
+
expandMultiple={treeState.expandMultiple}
|
|
300
|
+
collapseMultiple={treeState.collapseMultiple}
|
|
301
|
+
isPending={treeState.isPending}
|
|
302
|
+
lineColor={lineColor}
|
|
303
|
+
/>
|
|
304
|
+
{selectedFile && <p>Selected file: {selectedFile}</p>}
|
|
305
|
+
</div>
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
/>
|
|
309
|
+
{selectedFile && <p>Selected file: {selectedFile}</p>}
|
|
310
|
+
</div>
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export default App
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## 4. Internal Hooks
|
|
318
|
+
|
|
319
|
+
This section describes the hooks used internally by the `DirectoryTree` component.
|
|
320
|
+
このセクションでは、`DirectoryTree` コンポーネントが内部で使用するフックについて説明します。
|
|
321
|
+
|
|
322
|
+
### 4.1. `useDirectoryTreeState`
|
|
323
|
+
|
|
324
|
+
#### 4.1.1. Overview
|
|
325
|
+
|
|
326
|
+
This hook manages the expansion state of the directory tree. It persists the state to `localStorage` to maintain it across sessions and uses `useTransition` to perform state updates without blocking the UI.
|
|
327
|
+
このフックは、ディレクトリツリーの展開状態を管理します。状態を `localStorage` に永続化してセッション間で維持し、`useTransition` を使用して UI をブロックせずに状態更新を実行します。
|
|
328
|
+
|
|
329
|
+
#### 4.1.2. Input (Options)
|
|
330
|
+
|
|
331
|
+
| Prop Name | Type | Required | Default | Description |
|
|
332
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
333
|
+
| `initialEntries` | `DirectoryEntry[]` | No | `[]` | The initial set of directory entries. Used to determine the default expansion state if no state is found in `localStorage`. 初期ディレクトリ エントリのセット。`localStorage` に状態が見つからない場合に、デフォルトの展開状態を決定するために使用されます。 |
|
|
334
|
+
| `storageKey` | `string` | No | `directory-tree-state` | The key used to store the expansion state in `localStorage`. 展開状態を `localStorage` に保存するために使用されるキー。 |
|
|
335
|
+
|
|
336
|
+
#### 4.1.3. Output
|
|
337
|
+
|
|
338
|
+
The hook returns a `DirectoryTreeState` object with the following properties:
|
|
339
|
+
このフックは、次のプロパティを持つ `DirectoryTreeState` オブジェクトを返します。
|
|
340
|
+
|
|
341
|
+
| Property | Type | Description |
|
|
342
|
+
| :--- | :--- | :--- |
|
|
343
|
+
| `expanded` | `Set<string>` | A set of absolute paths of the expanded directories. 展開されているディレクトリの絶対パスのセット。 |
|
|
344
|
+
| `toggle` | `(path: string) => void` | Function to toggle the expansion state of a directory. ディレクトリの展開状態を切り替える関数。 |
|
|
345
|
+
| `expand` | `(path: string) => void` | Function to expand a directory. ディレクトリを展開する関数。 |
|
|
346
|
+
| `collapse` | `(path: string) => void` | Function to collapse a directory. ディレクトリを折りたたむ関数。 |
|
|
347
|
+
| `collapseMultiple` | `(paths: string[]) => void` | Function to collapse multiple directories. 複数のディレクトリを折りたたむ関数。 |
|
|
348
|
+
| `expandMultiple` | `(paths: string[]) => void` | Function to expand multiple directories. 複数のディレクトリを展開する関数。 |
|
|
349
|
+
| `isPending` | `boolean` | A flag that is true when a state transition is pending. 状態遷移が保留中の場合に true となるフラグ。 |
|
|
350
|
+
|
|
351
|
+
## 5. Data Structures
|
|
352
|
+
|
|
353
|
+
### 5.1. `DirectoryEntry`
|
|
354
|
+
|
|
355
|
+
This type represents a single node in the directory tree.
|
|
356
|
+
この型は、ディレクトリツリー内の単一ノードを表します。
|
|
357
|
+
|
|
358
|
+
| Property | Type | Description |
|
|
359
|
+
| :--- | :--- | :--- |
|
|
360
|
+
| `name` | `string` | The name of the file or directory. ファイルまたはディレクトリの名前。 |
|
|
361
|
+
| `type` | `"file" \| "directory"` | The type of the entry. エントリの種別。 |
|
|
362
|
+
| `absolutePath` | `string` | The absolute path of the entry. エントリの絶対パス。 |
|
|
363
|
+
| `relativePath` | `string` | The relative path of the entry. エントリの相対パス。 |
|
|
364
|
+
| `children` | `DirectoryEntry[] \| undefined` | An array of child entries if the type is `directory`. `type` が `directory` の場合の子エントリの配列。 |
|
|
365
|
+
|
|
366
|
+
### 5.2. `DirectoryTreeState`
|
|
367
|
+
|
|
368
|
+
This type represents the state object managed by the `useDirectoryTreeState` hook.
|
|
369
|
+
この型は `useDirectoryTreeState` フックによって管理される状態オブジェクトを表します。
|
|
370
|
+
|
|
371
|
+
| Property | Type | Description |
|
|
372
|
+
| :--- | :--- | :--- |
|
|
373
|
+
| `expanded` | `Set<string>` | A set of absolute paths of the expanded directories. 展開されているディレクトリの絶対パスのセット。 |
|
|
374
|
+
| `toggle` | `(path: string) => void` | Function to toggle the expansion state of a directory. ディレクトリの展開状態を切り替える関数。 |
|
|
375
|
+
| `expand` | `(path: string) => void` | Function to expand a directory. ディレクトリを展開する関数。 |
|
|
376
|
+
| `collapse` | `(path: string) => void` | Function to collapse a directory. ディレクトリを折りたたむ関数。 |
|
|
377
|
+
| `collapseMultiple` | `(paths: string[]) => void` | Function to collapse multiple directories. 複数のディレクトリを折りたたむ関数。 |
|
|
378
|
+
| `expandMultiple` | `(paths: string[]) => void` | Function to expand multiple directories. 複数のディレクトリを展開する関数。 |
|
|
379
|
+
| `isPending` | `boolean` | A flag that is true when a state transition is pending. 状態遷移が保留中の場合に true となるフラグ。 |
|
|
380
|
+
|
|
381
|
+
## 6. Functional Requirements
|
|
382
|
+
|
|
383
|
+
### 6.1. Directory Expansion and Collapse
|
|
384
|
+
|
|
385
|
+
* **Single Click**: A single click on a directory icon or name toggles its expansion state (expanded or collapsed).
|
|
386
|
+
* **Double Click**: A double click on a directory can trigger one of two behaviors, selectable via a prop:
|
|
387
|
+
1. Toggle the expansion state of all descendant directories.
|
|
388
|
+
2. Recursively expand or collapse the directory and all its subdirectories.
|
|
389
|
+
* **シングルクリック**: ディレクトリのアイコンまたは名前をシングルクリックすると、その展開状態 (展開または折りたたみ) が切り替わります。
|
|
390
|
+
* **ダブルクリック**: ディレクトリをダブルクリックすると、プロパティを介して選択可能な 2 つの動作のいずれかがトリガーされます。
|
|
391
|
+
1. すべての子孫ディレクトリの展開状態を切り替える。
|
|
392
|
+
2. ディレクトリとそのすべてのサブディレクトリを再帰的に展開または折りたたむ。
|
|
393
|
+
|
|
394
|
+
### 6.2. File Selection
|
|
395
|
+
|
|
396
|
+
* A single click on a file name triggers the `onEntryClick` callback, passing a `DirectoryTreeClickEvent` containing the entry and metadata.
|
|
397
|
+
* The selected file is highlighted.
|
|
398
|
+
* ファイル名をクリックすると `onEntryClick` コールバックがトリガーされ、エントリとメタデータを含む `DirectoryTreeClickEvent` が渡されます。
|
|
399
|
+
* 選択されたファイルがハイライト表示されます。
|
|
400
|
+
|
|
401
|
+
## 7. Non-Functional Requirements
|
|
402
|
+
|
|
403
|
+
### 7.1. Performance
|
|
404
|
+
|
|
405
|
+
* The component must render efficiently, even with a large number of files and directories.
|
|
406
|
+
* `React.memo` should be used for `DirectoryEntryItem` to prevent unnecessary re-renders.
|
|
407
|
+
* The `useDirectoryTreeState` hook will use `useTransition` to prevent UI blocking during state updates, improving the user experience.
|
|
408
|
+
* In grid mode, the `VirtualScroll` element is memoized so a horizontal-scroll tick (which only changes the `--dt-hscroll` variable) does not re-render the virtualized body; both `DirectoryEntryItem` and `DirectoryTreeGridRow` are `React.memo`-guarded.
|
|
409
|
+
* 多数のファイルやディレクトリがあっても、コンポーネントは効率的にレンダリングされる必要があります。
|
|
410
|
+
* 不要な再レンダリングを防ぐために、`DirectoryEntryItem` には `React.memo` を使用する必要があります。
|
|
411
|
+
* grid モードでは `VirtualScroll` 要素をメモ化し、横スクロール(`--dt-hscroll` 変数のみ変化)で仮想化本体を再レンダーしない。`DirectoryEntryItem` / `DirectoryTreeGridRow` はいずれも `React.memo` 対象。
|
|
412
|
+
* `useDirectoryTreeState` フックは `useTransition` を使用して、状態更新中の UI ブロッキングを防ぎ、ユーザーエクスペリエンスを向上させます。
|
|
413
|
+
|
|
414
|
+
### 7.2. Styling
|
|
415
|
+
|
|
416
|
+
* The component is styled using Tailwind CSS for flexibility.
|
|
417
|
+
* The color and width of the tree lines are customizable via props.
|
|
418
|
+
* コンポーネントは、柔軟性のために Tailwind CSS を使用してスタイリングされます。
|
|
419
|
+
* ツリーラインの色と幅は、プロパティを介してカスタマイズ可能です。
|
|
420
|
+
|
|
421
|
+
## 8. Dependencies
|
|
422
|
+
|
|
423
|
+
* `react` / `react-dom` (peer dependencies)
|
|
424
|
+
* `@aiquants/virtualscroll` (virtual scrolling engine; also provides the horizontal `ScrollBar` and `onWheelHorizontal`)
|
|
425
|
+
* `@heroicons/react`, `@phosphor-icons/react` (default type / expand icons)
|
|
426
|
+
* `tailwind-merge` (class-name merging)
|
|
427
|
+
* Tailwind CSS v4 (styling)
|
|
428
|
+
|
|
429
|
+
## 9. Test Specification
|
|
430
|
+
|
|
431
|
+
The package ships a comprehensive Vitest suite. Coverage is **100% (statements / functions / lines)** on the
|
|
432
|
+
new TreeGrid modules (`gridUtils.ts`, `useEntryInteraction.ts`, `entryRendering.tsx`, `DirectoryTreeGrid.tsx`)
|
|
433
|
+
and 100% statements/functions/lines on `DirectoryTree.tsx` (residual branches are defensive null-safety/dedup
|
|
434
|
+
guards). Run `pnpm run test:coverage`.
|
|
435
|
+
|
|
436
|
+
### 9.1. Classic tree ([DirectoryTree.test.tsx](../tests/DirectoryTree.test.tsx))
|
|
437
|
+
|
|
438
|
+
* **Basic Rendering**: the file/directory hierarchy renders (file name, expanding directories, etc.).
|
|
439
|
+
* **Selection Action**: clicking a file triggers the selection callbacks (`onEntryClick`) with correct parameters.
|
|
440
|
+
* **Custom Styling**: highlight class names (`directorySelectedClassName` / `itemSelectedClassName`) and styles are applied to active entries.
|
|
441
|
+
* **Hover Interaction**: custom hover styling (`hoverClassName` / `hoverStyle`) is applied on mouse enter.
|
|
442
|
+
* **Entry-specific Custom Styling**: entry (`entryClassName`/`entryStyle`) and name (`nameClassName`/`nameStyle`, `directoryNameClassName`/`directoryNameStyle`, `fileNameClassName`/`fileNameStyle`) classes/styles are merged and applied.
|
|
443
|
+
|
|
444
|
+
### 9.2. Behavior ([DirectoryTree.behavior.test.tsx](../tests/DirectoryTree.behavior.test.tsx))
|
|
445
|
+
|
|
446
|
+
* **Interaction**: plain-row `onMouseLeave` / `onKeyDown(Enter)`; double-click actions (`expandMultiple` on all-collapsed, `collapseMultiple` on all-expanded, `collapseMultiple` on mixed+toggle).
|
|
447
|
+
* **removeRootIndent / empty entries**: root indent removal renders without error; empty `entries` renders the placeholder container only.
|
|
448
|
+
* **Tree-line geometry render**: an expanded tree with `viewportHeightOverride` + `showTreeLines` paints the connector canvas.
|
|
449
|
+
* **Scroll callbacks**: vertical wheel drives `handleVirtualScroll` / `handleRangeChange`.
|
|
450
|
+
* **Plain-row style branches**: selected file / itemSelected / directory selection (default & override); icon-hidden; `initialScrollOffset`; entry-reference-change re-render.
|
|
451
|
+
* **Grid horizontal wiring**: horizontal wheel advances `--dt-hscroll`; the horizontal `ScrollBar` `onScroll` updates hscroll; `scrollBarOptions.width` is reflected in the header spacer; **rows are not re-rendered on horizontal scroll** (memoization guard).
|
|
452
|
+
|
|
453
|
+
### 9.3. TreeGrid mode ([DirectoryTreeGrid.test.tsx](../tests/DirectoryTreeGrid.test.tsx))
|
|
454
|
+
|
|
455
|
+
* **Semantics**: `role="treegrid"` when `grid` is set, `role="tree"` otherwise; aligned column headers.
|
|
456
|
+
* **Alignment invariant**: name cells indent by depth (`0/16/32px`); data cells carry **no** per-depth padding.
|
|
457
|
+
* **ClassName consistency**: verifies that `columns[i].className` is applied to body, header, and footer cells for both the name column and numeric columns.
|
|
458
|
+
* **Footer**: `showFooter` aggregates over all entries (collapsed subtrees included); function and `ReactNode` footer forms.
|
|
459
|
+
* **Row states**: selection (`selectedPath` / `selectedItems`), highlight classes/styles, hover, `align=center`, `rowClassName` factory, custom / fallback name render, name-only columns, empty data cell.
|
|
460
|
+
* **removeRootIndent ignored in grid** (normal `0/16px` indent preserved); **empty grid** still renders header/footer/`treegrid` role.
|
|
461
|
+
* **Memo comparator** (`isEntryExpanded`, entry-reference change, directory expansion change).
|
|
462
|
+
* **`isEntryExpanded`** helper: directory expanded/collapsed, file short-circuit.
|
|
463
|
+
|
|
464
|
+
### 9.4. Pure helpers
|
|
465
|
+
|
|
466
|
+
* **[gridUtils.test.ts](../tests/gridUtils.test.ts)**: `buildNumericTemplate`, `computeGridMetrics`, `clampHScroll`, `flattenAllEntries` (incl. default/invalid widths, zero-width viewport, NaN clamp).
|
|
467
|
+
* **[treeLineGeometry.test.ts](../tests/treeLineGeometry.test.ts)**: `buildTreeLineGeometry` — every connector variant (directory/file, last-child, ancestor lines, glyphs), `hideLines` skip, empty input, glyph-size fallback, and defensive out-of-bounds fallbacks.
|
|
468
|
+
* **[useEntryInteraction.test.tsx](../tests/useEntryInteraction.test.tsx)**: file select, directory single-click (delayed toggle), double-click (recursive), `preventDefault`, unmount timer cleanup.
|
|
469
|
+
* **[entryRendering.test.tsx](../tests/entryRendering.test.tsx)**: `resolveExpandIcon`, `resolveEntryIcon` (all icon paths + sanitizer edge cases), `getFileExtension`.
|
|
470
|
+
|
|
471
|
+
### 9.5. VirtualScroll horizontal wheel (`@aiquants/virtualscroll`)
|
|
472
|
+
|
|
473
|
+
* **[wheel-horizontal.spec.tsx](../../virtualscroll/tests/integration/wheel-horizontal.spec.tsx)**: `onWheelHorizontal` fires on horizontal-dominant / shift+wheel gestures, not on vertical-dominant; `deltaMode` LINE/PAGE scaling; a no-op (backward-compatible) when the handler is absent.
|