@bendyline/docblocks-react 1.1.2 → 2.1.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.
Files changed (225) hide show
  1. package/README.md +37 -5
  2. package/dist/ExportDialog-7LDIXJZ3.js +10 -0
  3. package/dist/ExportDialog-7LDIXJZ3.js.map +1 -0
  4. package/dist/ExportToolbarControls-GHAOJMH6.js +508 -0
  5. package/dist/ExportToolbarControls-GHAOJMH6.js.map +1 -0
  6. package/dist/GitToolbarControl-GNMRVKWQ.js +136 -0
  7. package/dist/GitToolbarControl-GNMRVKWQ.js.map +1 -0
  8. package/dist/GitUI-VHOBQY43.js +1133 -0
  9. package/dist/GitUI-VHOBQY43.js.map +1 -0
  10. package/dist/LazyEditorShell-EF2BWRXU.js +6 -0
  11. package/dist/LazyEditorShell-EF2BWRXU.js.map +1 -0
  12. package/dist/ShareDialog-63ENBEDZ.js +213 -0
  13. package/dist/ShareDialog-63ENBEDZ.js.map +1 -0
  14. package/dist/chunk-4XXSWN3T.js +13 -0
  15. package/dist/chunk-4XXSWN3T.js.map +1 -0
  16. package/dist/chunk-57ZJJOC5.js +89 -0
  17. package/dist/chunk-57ZJJOC5.js.map +1 -0
  18. package/dist/chunk-5A4KEWKT.js +97 -0
  19. package/dist/chunk-5A4KEWKT.js.map +1 -0
  20. package/dist/chunk-DD4VFJQI.js +51 -0
  21. package/dist/chunk-DD4VFJQI.js.map +1 -0
  22. package/dist/chunk-FEWQUQML.js +256 -0
  23. package/dist/chunk-FEWQUQML.js.map +1 -0
  24. package/dist/chunk-FMGTHSTX.js +110 -0
  25. package/dist/chunk-FMGTHSTX.js.map +1 -0
  26. package/dist/chunk-UNYZQXWR.js +324 -0
  27. package/dist/chunk-UNYZQXWR.js.map +1 -0
  28. package/dist/chunk-VHDP4IPL.js +70 -0
  29. package/dist/chunk-VHDP4IPL.js.map +1 -0
  30. package/dist/chunk-ZI56BNB7.js +270 -0
  31. package/dist/chunk-ZI56BNB7.js.map +1 -0
  32. package/dist/editor.d.ts +11 -0
  33. package/dist/editor.js +9 -0
  34. package/dist/editor.js.map +1 -0
  35. package/dist/export/index.d.ts +93 -0
  36. package/dist/export/index.js +27 -0
  37. package/dist/export/index.js.map +1 -0
  38. package/dist/index.d.ts +346 -18
  39. package/dist/index.js +6169 -18
  40. package/dist/index.js.map +1 -1
  41. package/dist/run-export-ICTZITXC.js +10 -0
  42. package/dist/run-export-ICTZITXC.js.map +1 -0
  43. package/dist/settings/index.d.ts +36 -0
  44. package/dist/settings/index.js +14 -0
  45. package/dist/settings/index.js.map +1 -0
  46. package/package.json +33 -13
  47. package/src/styles/docblocks.css +2738 -132
  48. package/dist/AppMenu/AppMenu.d.ts +0 -26
  49. package/dist/AppMenu/AppMenu.d.ts.map +0 -1
  50. package/dist/AppMenu/AppMenu.js +0 -28
  51. package/dist/AppMenu/AppMenu.js.map +0 -1
  52. package/dist/AppMenu/index.d.ts +0 -3
  53. package/dist/AppMenu/index.d.ts.map +0 -1
  54. package/dist/AppMenu/index.js +0 -2
  55. package/dist/AppMenu/index.js.map +0 -1
  56. package/dist/DocBlocksShell/DocBlocksShell.d.ts +0 -36
  57. package/dist/DocBlocksShell/DocBlocksShell.d.ts.map +0 -1
  58. package/dist/DocBlocksShell/DocBlocksShell.js +0 -1351
  59. package/dist/DocBlocksShell/DocBlocksShell.js.map +0 -1
  60. package/dist/DocBlocksShell/index.d.ts +0 -3
  61. package/dist/DocBlocksShell/index.d.ts.map +0 -1
  62. package/dist/DocBlocksShell/index.js +0 -2
  63. package/dist/DocBlocksShell/index.js.map +0 -1
  64. package/dist/Export/ExportDialog.d.ts +0 -16
  65. package/dist/Export/ExportDialog.d.ts.map +0 -1
  66. package/dist/Export/ExportDialog.js +0 -158
  67. package/dist/Export/ExportDialog.js.map +0 -1
  68. package/dist/Export/ExportToolbarControls.d.ts +0 -17
  69. package/dist/Export/ExportToolbarControls.d.ts.map +0 -1
  70. package/dist/Export/ExportToolbarControls.js +0 -200
  71. package/dist/Export/ExportToolbarControls.js.map +0 -1
  72. package/dist/Export/export-options.d.ts +0 -53
  73. package/dist/Export/export-options.d.ts.map +0 -1
  74. package/dist/Export/export-options.js +0 -49
  75. package/dist/Export/export-options.js.map +0 -1
  76. package/dist/Export/index.d.ts +0 -6
  77. package/dist/Export/index.d.ts.map +0 -1
  78. package/dist/Export/index.js +0 -3
  79. package/dist/Export/index.js.map +0 -1
  80. package/dist/Export/run-export.d.ts +0 -8
  81. package/dist/Export/run-export.d.ts.map +0 -1
  82. package/dist/Export/run-export.js +0 -296
  83. package/dist/Export/run-export.js.map +0 -1
  84. package/dist/Export/transform-summaries.d.ts +0 -7
  85. package/dist/Export/transform-summaries.d.ts.map +0 -1
  86. package/dist/Export/transform-summaries.js +0 -6
  87. package/dist/Export/transform-summaries.js.map +0 -1
  88. package/dist/FileExplorer/FileExplorer.d.ts +0 -21
  89. package/dist/FileExplorer/FileExplorer.d.ts.map +0 -1
  90. package/dist/FileExplorer/FileExplorer.js +0 -140
  91. package/dist/FileExplorer/FileExplorer.js.map +0 -1
  92. package/dist/FileExplorer/FileTreeNode.d.ts +0 -18
  93. package/dist/FileExplorer/FileTreeNode.d.ts.map +0 -1
  94. package/dist/FileExplorer/FileTreeNode.js +0 -112
  95. package/dist/FileExplorer/FileTreeNode.js.map +0 -1
  96. package/dist/FileExplorer/index.d.ts +0 -6
  97. package/dist/FileExplorer/index.d.ts.map +0 -1
  98. package/dist/FileExplorer/index.js +0 -4
  99. package/dist/FileExplorer/index.js.map +0 -1
  100. package/dist/FileExplorer/useFileTree.d.ts +0 -35
  101. package/dist/FileExplorer/useFileTree.d.ts.map +0 -1
  102. package/dist/FileExplorer/useFileTree.js +0 -124
  103. package/dist/FileExplorer/useFileTree.js.map +0 -1
  104. package/dist/WorkspacePicker/WorkspacePicker.d.ts +0 -17
  105. package/dist/WorkspacePicker/WorkspacePicker.d.ts.map +0 -1
  106. package/dist/WorkspacePicker/WorkspacePicker.js +0 -63
  107. package/dist/WorkspacePicker/WorkspacePicker.js.map +0 -1
  108. package/dist/WorkspacePicker/WorkspaceSettingsButton.d.ts +0 -11
  109. package/dist/WorkspacePicker/WorkspaceSettingsButton.d.ts.map +0 -1
  110. package/dist/WorkspacePicker/WorkspaceSettingsButton.js +0 -27
  111. package/dist/WorkspacePicker/WorkspaceSettingsButton.js.map +0 -1
  112. package/dist/WorkspacePicker/WorkspaceSettingsDialog.d.ts +0 -20
  113. package/dist/WorkspacePicker/WorkspaceSettingsDialog.d.ts.map +0 -1
  114. package/dist/WorkspacePicker/WorkspaceSettingsDialog.js +0 -36
  115. package/dist/WorkspacePicker/WorkspaceSettingsDialog.js.map +0 -1
  116. package/dist/WorkspacePicker/index.d.ts +0 -3
  117. package/dist/WorkspacePicker/index.d.ts.map +0 -1
  118. package/dist/WorkspacePicker/index.js +0 -2
  119. package/dist/WorkspacePicker/index.js.map +0 -1
  120. package/dist/hooks/index.d.ts +0 -2
  121. package/dist/hooks/index.d.ts.map +0 -1
  122. package/dist/hooks/index.js +0 -2
  123. package/dist/hooks/index.js.map +0 -1
  124. package/dist/hooks/useAutoSave.d.ts +0 -15
  125. package/dist/hooks/useAutoSave.d.ts.map +0 -1
  126. package/dist/hooks/useAutoSave.js +0 -83
  127. package/dist/hooks/useAutoSave.js.map +0 -1
  128. package/dist/icons.d.ts +0 -7
  129. package/dist/icons.d.ts.map +0 -1
  130. package/dist/icons.js +0 -27
  131. package/dist/icons.js.map +0 -1
  132. package/dist/index.d.ts.map +0 -1
  133. package/dist/monaco-slim.d.ts +0 -19
  134. package/dist/monaco-slim.d.ts.map +0 -1
  135. package/dist/monaco-slim.js +0 -19
  136. package/dist/monaco-slim.js.map +0 -1
  137. package/dist/preferences/versioning.d.ts +0 -27
  138. package/dist/preferences/versioning.d.ts.map +0 -1
  139. package/dist/preferences/versioning.js +0 -62
  140. package/dist/preferences/versioning.js.map +0 -1
  141. package/src/AppMenu/AppMenu.tsx +0 -264
  142. package/src/AppMenu/index.ts +0 -2
  143. package/src/DocBlocksShell/DocBlocksShell.tsx +0 -1778
  144. package/src/DocBlocksShell/index.ts +0 -2
  145. package/src/Export/ExportDialog.tsx +0 -383
  146. package/src/Export/ExportToolbarControls.tsx +0 -313
  147. package/src/Export/export-options.ts +0 -99
  148. package/src/Export/index.ts +0 -5
  149. package/src/Export/run-export.ts +0 -349
  150. package/src/Export/transform-summaries.ts +0 -14
  151. package/src/FileExplorer/FileExplorer.tsx +0 -282
  152. package/src/FileExplorer/FileTreeNode.tsx +0 -214
  153. package/src/FileExplorer/index.ts +0 -5
  154. package/src/FileExplorer/useFileTree.ts +0 -178
  155. package/src/WorkspacePicker/WorkspacePicker.tsx +0 -137
  156. package/src/WorkspacePicker/WorkspaceSettingsButton.tsx +0 -88
  157. package/src/WorkspacePicker/WorkspaceSettingsDialog.tsx +0 -121
  158. package/src/WorkspacePicker/index.ts +0 -2
  159. package/src/fonts/fonts.css +0 -362
  160. package/src/fonts/licenses/LICENSE-cormorant-garamond.txt +0 -93
  161. package/src/fonts/licenses/LICENSE-crimson-text.txt +0 -93
  162. package/src/fonts/licenses/LICENSE-dm-sans.txt +0 -93
  163. package/src/fonts/licenses/LICENSE-dm-serif-display.txt +0 -93
  164. package/src/fonts/licenses/LICENSE-hanken-grotesk.txt +0 -94
  165. package/src/fonts/licenses/LICENSE-ibm-plex-sans.txt +0 -93
  166. package/src/fonts/licenses/LICENSE-inter.txt +0 -93
  167. package/src/fonts/licenses/LICENSE-jetbrains-mono.txt +0 -93
  168. package/src/fonts/licenses/LICENSE-lora.txt +0 -93
  169. package/src/fonts/licenses/LICENSE-merriweather.txt +0 -93
  170. package/src/fonts/licenses/LICENSE-oswald.txt +0 -93
  171. package/src/fonts/licenses/LICENSE-playfair-display.txt +0 -93
  172. package/src/fonts/licenses/LICENSE-pt-serif.txt +0 -93
  173. package/src/fonts/licenses/LICENSE-roboto.txt +0 -93
  174. package/src/fonts/licenses/LICENSE-source-serif-4.txt +0 -93
  175. package/src/fonts/woff2/cormorant-garamond-400.woff2 +0 -0
  176. package/src/fonts/woff2/cormorant-garamond-700.woff2 +0 -0
  177. package/src/fonts/woff2/crimson-text-400.woff2 +0 -0
  178. package/src/fonts/woff2/crimson-text-400i.woff2 +0 -0
  179. package/src/fonts/woff2/crimson-text-700.woff2 +0 -0
  180. package/src/fonts/woff2/dm-sans-400.woff2 +0 -0
  181. package/src/fonts/woff2/dm-sans-400i.woff2 +0 -0
  182. package/src/fonts/woff2/dm-sans-500.woff2 +0 -0
  183. package/src/fonts/woff2/dm-sans-700.woff2 +0 -0
  184. package/src/fonts/woff2/dm-serif-display-400.woff2 +0 -0
  185. package/src/fonts/woff2/hanken-grotesk-400.woff2 +0 -0
  186. package/src/fonts/woff2/hanken-grotesk-400i.woff2 +0 -0
  187. package/src/fonts/woff2/hanken-grotesk-500.woff2 +0 -0
  188. package/src/fonts/woff2/hanken-grotesk-600.woff2 +0 -0
  189. package/src/fonts/woff2/hanken-grotesk-700.woff2 +0 -0
  190. package/src/fonts/woff2/ibm-plex-sans-400.woff2 +0 -0
  191. package/src/fonts/woff2/ibm-plex-sans-400i.woff2 +0 -0
  192. package/src/fonts/woff2/ibm-plex-sans-500.woff2 +0 -0
  193. package/src/fonts/woff2/ibm-plex-sans-600.woff2 +0 -0
  194. package/src/fonts/woff2/ibm-plex-sans-700.woff2 +0 -0
  195. package/src/fonts/woff2/inter-400.woff2 +0 -0
  196. package/src/fonts/woff2/inter-400i.woff2 +0 -0
  197. package/src/fonts/woff2/inter-500.woff2 +0 -0
  198. package/src/fonts/woff2/inter-600.woff2 +0 -0
  199. package/src/fonts/woff2/inter-700.woff2 +0 -0
  200. package/src/fonts/woff2/jetbrains-mono-400.woff2 +0 -0
  201. package/src/fonts/woff2/jetbrains-mono-400i.woff2 +0 -0
  202. package/src/fonts/woff2/jetbrains-mono-700.woff2 +0 -0
  203. package/src/fonts/woff2/lora-400.woff2 +0 -0
  204. package/src/fonts/woff2/lora-400i.woff2 +0 -0
  205. package/src/fonts/woff2/lora-700.woff2 +0 -0
  206. package/src/fonts/woff2/merriweather-400.woff2 +0 -0
  207. package/src/fonts/woff2/merriweather-700.woff2 +0 -0
  208. package/src/fonts/woff2/oswald-400.woff2 +0 -0
  209. package/src/fonts/woff2/oswald-700.woff2 +0 -0
  210. package/src/fonts/woff2/playfair-display-400.woff2 +0 -0
  211. package/src/fonts/woff2/playfair-display-700.woff2 +0 -0
  212. package/src/fonts/woff2/pt-serif-400.woff2 +0 -0
  213. package/src/fonts/woff2/pt-serif-700.woff2 +0 -0
  214. package/src/fonts/woff2/roboto-400.woff2 +0 -0
  215. package/src/fonts/woff2/roboto-400i.woff2 +0 -0
  216. package/src/fonts/woff2/roboto-700.woff2 +0 -0
  217. package/src/fonts/woff2/source-serif-4-400.woff2 +0 -0
  218. package/src/fonts/woff2/source-serif-4-400i.woff2 +0 -0
  219. package/src/fonts/woff2/source-serif-4-700.woff2 +0 -0
  220. package/src/hooks/index.ts +0 -1
  221. package/src/hooks/useAutoSave.ts +0 -116
  222. package/src/icons.tsx +0 -70
  223. package/src/index.ts +0 -31
  224. package/src/monaco-slim.ts +0 -20
  225. package/src/preferences/versioning.ts +0 -69
@@ -1,282 +0,0 @@
1
- /**
2
- * FileExplorer — tree component rendering FileSystemProvider contents.
3
- *
4
- * Shows a file/folder tree with expand/collapse, selection, and a toolbar
5
- * for creating new files/folders.
6
- */
7
-
8
- import { useCallback, useState } from 'react';
9
- import type { FileSystemProvider, FileSystemEntry } from '@bendyline/docblocks/filesystem';
10
- import { useFileTree } from './useFileTree.js';
11
- import { FileTreeNode } from './FileTreeNode.js';
12
- import { NewFileIcon, NewFolderIcon, RefreshIcon } from '../icons.js';
13
-
14
- const SUPPORTED_EXTENSIONS = new Set(['.txt', '.md', '.docx', '.pdf', '.dbk', '.zip']);
15
-
16
- /**
17
- * Hide auto-generated `<basename>_files/` companion folders from the
18
- * tree. They hold per-document images and version snapshots — useful
19
- * to the editor, noisy in the user-facing file list. The folders still
20
- * exist on disk; we just don't surface them in the explorer.
21
- */
22
- function isHiddenEntry(entry: FileSystemEntry): boolean {
23
- if (entry.kind !== 'directory') return false;
24
- const name = entry.path.replace(/^\/+/, '').split('/').pop() ?? '';
25
- return name.endsWith('_files');
26
- }
27
-
28
- function filterVisible(entries: FileSystemEntry[]): FileSystemEntry[] {
29
- return entries.filter((e) => !isHiddenEntry(e));
30
- }
31
-
32
- export interface FileExplorerProps {
33
- /** The filesystem to display. */
34
- provider: FileSystemProvider | null;
35
- /** Called when any entry is selected (file or directory). */
36
- onSelect?: (path: string, kind: 'file' | 'directory') => void;
37
- /** Called after any mutation (create, delete, rename). */
38
- onTreeChange?: () => void;
39
- /** Called when supported files are dropped onto the explorer. */
40
- onImportFiles?: (files: File[]) => void;
41
- /** Optional className for the root element. */
42
- className?: string;
43
- }
44
-
45
- export function FileExplorer({
46
- provider,
47
- onSelect,
48
- onTreeChange,
49
- onImportFiles,
50
- className,
51
- }: FileExplorerProps) {
52
- const tree = useFileTree(provider);
53
- const { childEntries } = tree as typeof tree & {
54
- childEntries: Map<string, FileSystemEntry[]>;
55
- };
56
-
57
- const [newItemName, setNewItemName] = useState('');
58
- const [newItemType, setNewItemType] = useState<'file' | 'directory' | null>(null);
59
- const [dragOver, setDragOver] = useState(false);
60
- const dragCounter = useState({ current: 0 })[0];
61
-
62
- const hasSupported = useCallback((dt: DataTransfer): boolean => {
63
- for (const item of Array.from(dt.items)) {
64
- if (item.kind !== 'file') continue;
65
- const name = (item as DataTransferItem & { getAsFile(): File | null }).getAsFile?.()?.name;
66
- // During dragover the filename may not be available, so accept broadly
67
- if (!name) return true;
68
- const ext = name.slice(name.lastIndexOf('.')).toLowerCase();
69
- if (SUPPORTED_EXTENSIONS.has(ext)) return true;
70
- }
71
- return dt.items.length > 0;
72
- }, []);
73
-
74
- const handleDragEnter = useCallback(
75
- (e: React.DragEvent) => {
76
- e.preventDefault();
77
- dragCounter.current += 1;
78
- if (hasSupported(e.dataTransfer)) setDragOver(true);
79
- },
80
- [hasSupported, dragCounter],
81
- );
82
-
83
- const handleDragOver = useCallback((e: React.DragEvent) => {
84
- e.preventDefault();
85
- e.dataTransfer.dropEffect = 'copy';
86
- }, []);
87
-
88
- const handleDragLeave = useCallback(
89
- (e: React.DragEvent) => {
90
- e.preventDefault();
91
- dragCounter.current -= 1;
92
- if (dragCounter.current <= 0) {
93
- dragCounter.current = 0;
94
- setDragOver(false);
95
- }
96
- },
97
- [dragCounter],
98
- );
99
-
100
- const handleDrop = useCallback(
101
- (e: React.DragEvent) => {
102
- e.preventDefault();
103
- dragCounter.current = 0;
104
- setDragOver(false);
105
- const supported = Array.from(e.dataTransfer.files).filter((f) => {
106
- const ext = f.name.slice(f.name.lastIndexOf('.')).toLowerCase();
107
- return SUPPORTED_EXTENSIONS.has(ext);
108
- });
109
- if (supported.length > 0) onImportFiles?.(supported);
110
- },
111
- [onImportFiles, dragCounter],
112
- );
113
-
114
- const handleSelect = useCallback(
115
- (path: string) => {
116
- // Determine kind from entries (root + children)
117
- const findKind = (p: string): 'file' | 'directory' => {
118
- for (const e of tree.entries) {
119
- if (e.path === p) return e.kind;
120
- }
121
- for (const children of childEntries.values()) {
122
- for (const e of children) {
123
- if (e.path === p) return e.kind;
124
- }
125
- }
126
- return 'file';
127
- };
128
- const kind = findKind(path);
129
- tree.select(path, kind);
130
- onSelect?.(path, kind);
131
- },
132
- [tree, onSelect, childEntries],
133
- );
134
-
135
- const handleNewItemSubmit = useCallback(async () => {
136
- if (!newItemName.trim()) {
137
- setNewItemType(null);
138
- return;
139
- }
140
- const name = newItemName.trim();
141
- // Scope to currently selected folder (or root)
142
- const prefix =
143
- tree.selectedKind === 'directory' && tree.selectedPath ? `${tree.selectedPath}/` : '';
144
- if (newItemType === 'file') {
145
- const filename = name.endsWith('.md') ? name : `${name}.md`;
146
- await tree.createFile(`${prefix}${filename}`, '');
147
- } else if (newItemType === 'directory') {
148
- await tree.createDirectory(`${prefix}${name}`);
149
- }
150
- setNewItemName('');
151
- setNewItemType(null);
152
- onTreeChange?.();
153
- }, [newItemName, newItemType, tree, onTreeChange]);
154
-
155
- const handleDelete = useCallback(
156
- async (path: string) => {
157
- await tree.deleteEntry(path);
158
- onTreeChange?.();
159
- },
160
- [tree, onTreeChange],
161
- );
162
-
163
- const handleRename = useCallback(
164
- async (oldPath: string, newPath: string) => {
165
- await tree.renameEntry(oldPath, newPath);
166
- onTreeChange?.();
167
- },
168
- [tree, onTreeChange],
169
- );
170
-
171
- const renderEntries = useCallback(
172
- (entries: FileSystemEntry[], depth: number): React.ReactNode => {
173
- return filterVisible(entries).map((entry) => (
174
- <FileTreeNode
175
- key={entry.path}
176
- entry={entry}
177
- depth={depth}
178
- expanded={tree.expanded.has(entry.path)}
179
- selected={tree.selectedPath === entry.path}
180
- onToggle={tree.toggleExpand}
181
- onSelect={handleSelect}
182
- onDelete={handleDelete}
183
- onRename={handleRename}
184
- renderChildren={(dirPath: string) => {
185
- const children = childEntries.get(dirPath) ?? [];
186
- return renderEntries(children, depth + 1);
187
- }}
188
- />
189
- ));
190
- },
191
- [tree, handleSelect, handleDelete, handleRename, childEntries],
192
- );
193
-
194
- return (
195
- <div
196
- className={`db-file-explorer ${dragOver ? 'db-file-explorer--drop-active' : ''} ${className ?? ''}`}
197
- onDragEnter={handleDragEnter}
198
- onDragOver={handleDragOver}
199
- onDragLeave={handleDragLeave}
200
- onDrop={handleDrop}
201
- >
202
- {/* Toolbar */}
203
- <div className="db-explorer-toolbar">
204
- <span className="db-explorer-title">Files</span>
205
- <div className="db-explorer-actions">
206
- <button
207
- className="db-explorer-btn"
208
- onClick={() => setNewItemType('file')}
209
- title="New File"
210
- aria-label="New File"
211
- >
212
- <NewFileIcon />
213
- </button>
214
- <button
215
- className="db-explorer-btn"
216
- onClick={() => setNewItemType('directory')}
217
- title="New Folder"
218
- aria-label="New Folder"
219
- >
220
- <NewFolderIcon />
221
- </button>
222
- <button
223
- className="db-explorer-btn"
224
- onClick={() => tree.refresh()}
225
- title="Refresh file list"
226
- aria-label="Refresh file list"
227
- >
228
- <RefreshIcon />
229
- </button>
230
- </div>
231
- </div>
232
-
233
- {/* New item input */}
234
- {newItemType && (
235
- <div className="db-new-item">
236
- <form
237
- className="db-new-item-row"
238
- onSubmit={(e) => {
239
- e.preventDefault();
240
- handleNewItemSubmit();
241
- }}
242
- >
243
- <input
244
- className="db-new-item-input"
245
- placeholder={newItemType === 'file' ? 'filename' : 'folder-name'}
246
- value={newItemName}
247
- onChange={(e) => setNewItemName(e.target.value)}
248
- onKeyDown={(e) => {
249
- if (e.key === 'Escape') {
250
- setNewItemType(null);
251
- setNewItemName('');
252
- }
253
- }}
254
- autoFocus
255
- />
256
- {newItemType === 'file' && <span className="db-new-item-suffix">.md</span>}
257
- <button type="submit" className="db-new-item-add">
258
- Add
259
- </button>
260
- </form>
261
- </div>
262
- )}
263
-
264
- {/* Tree — role="tree" only when there are real treeitem children; the
265
- loading/empty states get a status role so axe doesn't flag the
266
- tree as missing required children. */}
267
- {tree.loading ? (
268
- <div className="db-tree" role="status" aria-live="polite">
269
- <div className="db-tree-loading">Loading...</div>
270
- </div>
271
- ) : filterVisible(tree.entries).length === 0 ? (
272
- <div className="db-tree">
273
- <div className="db-tree-empty">No files yet</div>
274
- </div>
275
- ) : (
276
- <div className="db-tree" role="tree">
277
- {renderEntries(tree.entries, 0)}
278
- </div>
279
- )}
280
- </div>
281
- );
282
- }
@@ -1,214 +0,0 @@
1
- /**
2
- * FileTreeNode — recursive tree node for the file explorer.
3
- */
4
-
5
- import { useCallback, useState, useRef, useEffect, useLayoutEffect } from 'react';
6
- import { createPortal } from 'react-dom';
7
- import type { FileSystemEntry } from '@bendyline/docblocks/filesystem';
8
- import { MoreIcon } from '../icons.js';
9
-
10
- export interface FileTreeNodeProps {
11
- entry: FileSystemEntry;
12
- depth: number;
13
- expanded: boolean;
14
- selected: boolean;
15
- children?: FileSystemEntry[];
16
- onToggle: (path: string) => void;
17
- onSelect: (path: string) => void;
18
- onDelete: (path: string) => Promise<void>;
19
- onRename: (oldPath: string, newPath: string) => Promise<void>;
20
- renderChildren?: (dirPath: string) => React.ReactNode;
21
- }
22
-
23
- export function FileTreeNode({
24
- entry,
25
- depth,
26
- expanded,
27
- selected,
28
- onToggle,
29
- onSelect,
30
- onDelete,
31
- onRename,
32
- renderChildren,
33
- }: FileTreeNodeProps) {
34
- const [renaming, setRenaming] = useState(false);
35
- const [renameValue, setRenameValue] = useState(entry.name);
36
- const [showContext, setShowContext] = useState(false);
37
- const [contextPos, setContextPos] = useState({ x: 0, y: 0 });
38
- const inputRef = useRef<HTMLInputElement>(null);
39
- const contextRef = useRef<HTMLDivElement>(null);
40
- const nodeRef = useRef<HTMLDivElement>(null);
41
-
42
- const isDir = entry.kind === 'directory';
43
- const icon = isDir ? (expanded ? '\u25BE' : '\u25B8') : '\u00A0\u00A0';
44
-
45
- const handleClick = useCallback(() => {
46
- if (isDir) {
47
- onToggle(entry.path);
48
- }
49
- onSelect(entry.path);
50
- }, [isDir, entry.path, onToggle, onSelect]);
51
-
52
- const handleContextMenu = useCallback((e: React.MouseEvent) => {
53
- e.preventDefault();
54
- e.stopPropagation();
55
- setContextPos({ x: e.clientX, y: e.clientY });
56
- setShowContext(true);
57
- }, []);
58
-
59
- const handleMoreClick = useCallback((e: React.MouseEvent) => {
60
- e.preventDefault();
61
- e.stopPropagation();
62
- const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
63
- setContextPos({ x: rect.right, y: rect.bottom });
64
- setShowContext(true);
65
- }, []);
66
-
67
- const handleRenameStart = useCallback(() => {
68
- setRenameValue(entry.name);
69
- setRenaming(true);
70
- setShowContext(false);
71
- }, [entry.name]);
72
-
73
- const handleRenameSubmit = useCallback(async () => {
74
- if (renameValue && renameValue !== entry.name) {
75
- const parentPath = entry.path.includes('/')
76
- ? entry.path.slice(0, entry.path.lastIndexOf('/'))
77
- : '';
78
- const newPath = parentPath ? `${parentPath}/${renameValue}` : renameValue;
79
- await onRename(entry.path, newPath);
80
- }
81
- setRenaming(false);
82
- }, [renameValue, entry.name, entry.path, onRename]);
83
-
84
- const handleDeleteClick = useCallback(async () => {
85
- setShowContext(false);
86
- await onDelete(entry.path);
87
- }, [entry.path, onDelete]);
88
-
89
- // Close context menu on outside click or scroll
90
- useEffect(() => {
91
- if (!showContext) return;
92
- function handleClose(e: Event) {
93
- if (contextRef.current && !contextRef.current.contains(e.target as Node)) {
94
- setShowContext(false);
95
- }
96
- }
97
- // Use click (not mousedown) so menu button clicks register first
98
- document.addEventListener('click', handleClose, true);
99
- document.addEventListener('contextmenu', handleClose, true);
100
- document.addEventListener('scroll', () => setShowContext(false), { capture: true, once: true });
101
- return () => {
102
- document.removeEventListener('click', handleClose, true);
103
- document.removeEventListener('contextmenu', handleClose, true);
104
- };
105
- }, [showContext]);
106
-
107
- // Focus input when renaming
108
- useEffect(() => {
109
- if (renaming && inputRef.current) {
110
- inputRef.current.focus();
111
- inputRef.current.select();
112
- }
113
- }, [renaming]);
114
-
115
- // Clamp menu inside viewport after it renders
116
- useLayoutEffect(() => {
117
- if (!showContext || !contextRef.current) return;
118
- const menu = contextRef.current;
119
- const rect = menu.getBoundingClientRect();
120
- const margin = 4;
121
- let left = contextPos.x;
122
- let top = contextPos.y;
123
- if (rect.right > window.innerWidth - margin) {
124
- left = Math.max(margin, window.innerWidth - rect.width - margin);
125
- }
126
- if (rect.bottom > window.innerHeight - margin) {
127
- top = Math.max(margin, window.innerHeight - rect.height - margin);
128
- }
129
- if (left !== contextPos.x || top !== contextPos.y) {
130
- menu.style.left = `${left}px`;
131
- menu.style.top = `${top}px`;
132
- }
133
- }, [showContext, contextPos]);
134
-
135
- return (
136
- <div className="db-tree-node" ref={nodeRef}>
137
- <div
138
- className={`db-tree-row ${selected ? 'db-tree-row--selected' : ''}`}
139
- style={{ paddingLeft: depth * 16 + 4 }}
140
- onClick={handleClick}
141
- onContextMenu={handleContextMenu}
142
- role="treeitem"
143
- aria-expanded={isDir ? expanded : undefined}
144
- aria-selected={selected}
145
- tabIndex={0}
146
- onKeyDown={(e) => {
147
- if (e.key === 'Enter') handleClick();
148
- }}
149
- >
150
- <span className="db-tree-icon">{icon}</span>
151
- {renaming ? (
152
- <input
153
- ref={inputRef}
154
- className="db-tree-rename-input"
155
- value={renameValue}
156
- onChange={(e) => setRenameValue(e.target.value)}
157
- onBlur={handleRenameSubmit}
158
- onKeyDown={(e) => {
159
- if (e.key === 'Enter') handleRenameSubmit();
160
- if (e.key === 'Escape') setRenaming(false);
161
- }}
162
- onClick={(e) => e.stopPropagation()}
163
- />
164
- ) : (
165
- <>
166
- <span className="db-tree-label">
167
- {entry.name.endsWith('.md') ? entry.name.slice(0, -3) : entry.name}
168
- </span>
169
- <button
170
- type="button"
171
- className={`db-tree-more${showContext ? ' db-tree-more--active' : ''}`}
172
- onClick={handleMoreClick}
173
- onContextMenu={handleMoreClick}
174
- aria-label="More actions"
175
- aria-haspopup="menu"
176
- aria-expanded={showContext}
177
- tabIndex={-1}
178
- >
179
- <MoreIcon width={14} height={14} />
180
- </button>
181
- </>
182
- )}
183
- </div>
184
-
185
- {/* Context menu — portaled so ancestor overflow:hidden doesn't clip it */}
186
- {showContext &&
187
- createPortal(
188
- <div
189
- ref={contextRef}
190
- className="db-tree-context"
191
- style={{ left: contextPos.x, top: contextPos.y }}
192
- >
193
- <button className="db-tree-context-item" onClick={handleRenameStart}>
194
- Rename
195
- </button>
196
- <button
197
- className="db-tree-context-item db-tree-context-item--danger"
198
- onClick={handleDeleteClick}
199
- >
200
- Delete
201
- </button>
202
- </div>,
203
- document.body,
204
- )}
205
-
206
- {/* Render children if directory is expanded */}
207
- {isDir && expanded && renderChildren && (
208
- <div className="db-tree-children" role="group">
209
- {renderChildren(entry.path)}
210
- </div>
211
- )}
212
- </div>
213
- );
214
- }
@@ -1,5 +0,0 @@
1
- export { FileExplorer } from './FileExplorer.js';
2
- export type { FileExplorerProps } from './FileExplorer.js';
3
- export { FileTreeNode } from './FileTreeNode.js';
4
- export type { FileTreeNodeProps } from './FileTreeNode.js';
5
- export { useFileTree } from './useFileTree.js';
@@ -1,178 +0,0 @@
1
- /**
2
- * useFileTree — hook that reads from a FileSystemProvider,
3
- * maintains expanded/collapsed state, and provides CRUD operations.
4
- */
5
-
6
- import { useState, useCallback, useEffect, useRef } from 'react';
7
- import type { FileSystemProvider, FileSystemEntry } from '@bendyline/docblocks/filesystem';
8
-
9
- export interface FileTreeState {
10
- /** Flat list of entries for the current directory view. */
11
- entries: FileSystemEntry[];
12
- /** Set of expanded directory paths. */
13
- expanded: Set<string>;
14
- /** Currently selected path — file or directory (null if none). */
15
- selectedPath: string | null;
16
- /** Kind of the selected entry. */
17
- selectedKind: 'file' | 'directory' | null;
18
- /** Whether the tree is loading. */
19
- loading: boolean;
20
- }
21
-
22
- export interface FileTreeActions {
23
- /** Toggle a directory's expanded state. */
24
- toggleExpand: (path: string) => void;
25
- /** Select a file or directory. */
26
- select: (path: string, kind: 'file' | 'directory') => void;
27
- /** Create a new file with optional initial content. */
28
- createFile: (path: string, content?: string) => Promise<void>;
29
- /** Create a new directory. */
30
- createDirectory: (path: string) => Promise<void>;
31
- /** Delete a file or directory. */
32
- deleteEntry: (path: string) => Promise<void>;
33
- /** Rename a file or directory. */
34
- renameEntry: (oldPath: string, newPath: string) => Promise<void>;
35
- /** Force refresh the tree. */
36
- refresh: () => Promise<void>;
37
- }
38
-
39
- export function useFileTree(provider: FileSystemProvider | null): FileTreeState & FileTreeActions {
40
- const [entries, setEntries] = useState<FileSystemEntry[]>([]);
41
- const [expanded, setExpanded] = useState<Set<string>>(new Set());
42
- const [selectedPath, setSelectedPath] = useState<string | null>(null);
43
- const [selectedKind, setSelectedKind] = useState<'file' | 'directory' | null>(null);
44
- const [loading, setLoading] = useState(false);
45
-
46
- // Track child entries for expanded directories
47
- const [childEntries, setChildEntries] = useState<Map<string, FileSystemEntry[]>>(new Map());
48
-
49
- const providerRef = useRef(provider);
50
- providerRef.current = provider;
51
-
52
- const loadRoot = useCallback(async () => {
53
- if (!providerRef.current) {
54
- setEntries([]);
55
- return;
56
- }
57
- setLoading(true);
58
- try {
59
- const root = await providerRef.current.readDirectory('');
60
- setEntries(root);
61
- } finally {
62
- setLoading(false);
63
- }
64
- }, []);
65
-
66
- const loadChildren = useCallback(async (dirPath: string) => {
67
- if (!providerRef.current) return;
68
- const children = await providerRef.current.readDirectory(dirPath);
69
- setChildEntries((prev) => {
70
- const next = new Map(prev);
71
- next.set(dirPath, children);
72
- return next;
73
- });
74
- }, []);
75
-
76
- // Load root on provider change
77
- useEffect(() => {
78
- setExpanded(new Set());
79
- setChildEntries(new Map());
80
- setSelectedPath(null);
81
- setSelectedKind(null);
82
- loadRoot();
83
- }, [provider, loadRoot]);
84
-
85
- const toggleExpand = useCallback(
86
- (path: string) => {
87
- setExpanded((prev) => {
88
- const next = new Set(prev);
89
- if (next.has(path)) {
90
- next.delete(path);
91
- } else {
92
- next.add(path);
93
- // Load children when expanding
94
- loadChildren(path);
95
- }
96
- return next;
97
- });
98
- },
99
- [loadChildren],
100
- );
101
-
102
- const select = useCallback((path: string, kind: 'file' | 'directory') => {
103
- setSelectedPath(path);
104
- setSelectedKind(kind);
105
- }, []);
106
-
107
- const refresh = useCallback(async () => {
108
- await loadRoot();
109
- // Reload all expanded directories
110
- const expandedPaths = [...expanded];
111
- for (const dirPath of expandedPaths) {
112
- await loadChildren(dirPath);
113
- }
114
- }, [loadRoot, loadChildren, expanded]);
115
-
116
- const createFile = useCallback(
117
- async (path: string, content = '') => {
118
- if (!providerRef.current) return;
119
- await providerRef.current.writeFile(path, content);
120
- await refresh();
121
- },
122
- [refresh],
123
- );
124
-
125
- const createDirectory = useCallback(
126
- async (path: string) => {
127
- if (!providerRef.current) return;
128
- await providerRef.current.createDirectory(path);
129
- await refresh();
130
- },
131
- [refresh],
132
- );
133
-
134
- const deleteEntry = useCallback(
135
- async (path: string) => {
136
- if (!providerRef.current) return;
137
- await providerRef.current.delete(path);
138
- if (selectedPath === path) {
139
- setSelectedPath(null);
140
- setSelectedKind(null);
141
- }
142
- await refresh();
143
- },
144
- [refresh, selectedPath],
145
- );
146
-
147
- const renameEntry = useCallback(
148
- async (oldPath: string, newPath: string) => {
149
- if (!providerRef.current) return;
150
- await providerRef.current.rename(oldPath, newPath);
151
- if (selectedPath === oldPath) {
152
- setSelectedPath(newPath);
153
- }
154
- await refresh();
155
- },
156
- [refresh, selectedPath],
157
- );
158
-
159
- // Merge root entries with child entries for a flat tree representation
160
- // (consumers use `expanded` and `childEntries` to render recursively)
161
-
162
- return {
163
- entries,
164
- expanded,
165
- selectedPath,
166
- selectedKind,
167
- loading,
168
- toggleExpand,
169
- select,
170
- createFile,
171
- createDirectory,
172
- deleteEntry,
173
- renameEntry,
174
- refresh,
175
- // Expose child entries for rendering
176
- ...{ childEntries },
177
- } as FileTreeState & FileTreeActions & { childEntries: Map<string, FileSystemEntry[]> };
178
- }