@bendyline/docblocks-react 0.1.0 → 1.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.
- package/LICENSE +21 -21
- package/dist/AppMenu/AppMenu.d.ts +6 -1
- package/dist/AppMenu/AppMenu.d.ts.map +1 -1
- package/dist/AppMenu/AppMenu.js +3 -2
- package/dist/AppMenu/AppMenu.js.map +1 -1
- package/dist/AppMenu/index.d.ts +1 -1
- package/dist/AppMenu/index.d.ts.map +1 -1
- package/dist/DocBlocksShell/DocBlocksShell.d.ts +1 -1
- package/dist/DocBlocksShell/DocBlocksShell.d.ts.map +1 -1
- package/dist/DocBlocksShell/DocBlocksShell.js +410 -73
- package/dist/DocBlocksShell/DocBlocksShell.js.map +1 -1
- package/dist/FileExplorer/FileTreeNode.d.ts.map +1 -1
- package/dist/FileExplorer/FileTreeNode.js +34 -8
- package/dist/FileExplorer/FileTreeNode.js.map +1 -1
- package/dist/WorkspacePicker/WorkspacePicker.d.ts.map +1 -1
- package/dist/WorkspacePicker/WorkspacePicker.js +9 -3
- package/dist/WorkspacePicker/WorkspacePicker.js.map +1 -1
- package/dist/icons.d.ts +1 -0
- package/dist/icons.d.ts.map +1 -1
- package/dist/icons.js +3 -0
- package/dist/icons.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/AppMenu/AppMenu.tsx +78 -1
- package/src/AppMenu/index.ts +1 -1
- package/src/DocBlocksShell/DocBlocksShell.tsx +480 -87
- package/src/FileExplorer/FileTreeNode.tsx +68 -26
- package/src/WorkspacePicker/WorkspacePicker.tsx +18 -7
- package/src/fonts/fonts.css +362 -0
- package/src/fonts/licenses/LICENSE-cormorant-garamond.txt +93 -0
- package/src/fonts/licenses/LICENSE-crimson-text.txt +93 -0
- package/src/fonts/licenses/LICENSE-dm-sans.txt +93 -0
- package/src/fonts/licenses/LICENSE-dm-serif-display.txt +93 -0
- package/src/fonts/licenses/LICENSE-hanken-grotesk.txt +94 -0
- package/src/fonts/licenses/LICENSE-ibm-plex-sans.txt +93 -0
- package/src/fonts/licenses/LICENSE-inter.txt +93 -0
- package/src/fonts/licenses/LICENSE-jetbrains-mono.txt +93 -0
- package/src/fonts/licenses/LICENSE-lora.txt +93 -0
- package/src/fonts/licenses/LICENSE-merriweather.txt +93 -0
- package/src/fonts/licenses/LICENSE-oswald.txt +93 -0
- package/src/fonts/licenses/LICENSE-playfair-display.txt +93 -0
- package/src/fonts/licenses/LICENSE-pt-serif.txt +93 -0
- package/src/fonts/licenses/LICENSE-roboto.txt +93 -0
- package/src/fonts/licenses/LICENSE-source-serif-4.txt +93 -0
- package/src/fonts/woff2/cormorant-garamond-400.woff2 +0 -0
- package/src/fonts/woff2/cormorant-garamond-700.woff2 +0 -0
- package/src/fonts/woff2/crimson-text-400.woff2 +0 -0
- package/src/fonts/woff2/crimson-text-400i.woff2 +0 -0
- package/src/fonts/woff2/crimson-text-700.woff2 +0 -0
- package/src/fonts/woff2/dm-sans-400.woff2 +0 -0
- package/src/fonts/woff2/dm-sans-400i.woff2 +0 -0
- package/src/fonts/woff2/dm-sans-500.woff2 +0 -0
- package/src/fonts/woff2/dm-sans-700.woff2 +0 -0
- package/src/fonts/woff2/dm-serif-display-400.woff2 +0 -0
- package/src/fonts/woff2/hanken-grotesk-400.woff2 +0 -0
- package/src/fonts/woff2/hanken-grotesk-400i.woff2 +0 -0
- package/src/fonts/woff2/hanken-grotesk-500.woff2 +0 -0
- package/src/fonts/woff2/hanken-grotesk-600.woff2 +0 -0
- package/src/fonts/woff2/hanken-grotesk-700.woff2 +0 -0
- package/src/fonts/woff2/ibm-plex-sans-400.woff2 +0 -0
- package/src/fonts/woff2/ibm-plex-sans-400i.woff2 +0 -0
- package/src/fonts/woff2/ibm-plex-sans-500.woff2 +0 -0
- package/src/fonts/woff2/ibm-plex-sans-600.woff2 +0 -0
- package/src/fonts/woff2/ibm-plex-sans-700.woff2 +0 -0
- package/src/fonts/woff2/inter-400.woff2 +0 -0
- package/src/fonts/woff2/inter-400i.woff2 +0 -0
- package/src/fonts/woff2/inter-500.woff2 +0 -0
- package/src/fonts/woff2/inter-600.woff2 +0 -0
- package/src/fonts/woff2/inter-700.woff2 +0 -0
- package/src/fonts/woff2/jetbrains-mono-400.woff2 +0 -0
- package/src/fonts/woff2/jetbrains-mono-400i.woff2 +0 -0
- package/src/fonts/woff2/jetbrains-mono-700.woff2 +0 -0
- package/src/fonts/woff2/lora-400.woff2 +0 -0
- package/src/fonts/woff2/lora-400i.woff2 +0 -0
- package/src/fonts/woff2/lora-700.woff2 +0 -0
- package/src/fonts/woff2/merriweather-400.woff2 +0 -0
- package/src/fonts/woff2/merriweather-700.woff2 +0 -0
- package/src/fonts/woff2/oswald-400.woff2 +0 -0
- package/src/fonts/woff2/oswald-700.woff2 +0 -0
- package/src/fonts/woff2/playfair-display-400.woff2 +0 -0
- package/src/fonts/woff2/playfair-display-700.woff2 +0 -0
- package/src/fonts/woff2/pt-serif-400.woff2 +0 -0
- package/src/fonts/woff2/pt-serif-700.woff2 +0 -0
- package/src/fonts/woff2/roboto-400.woff2 +0 -0
- package/src/fonts/woff2/roboto-400i.woff2 +0 -0
- package/src/fonts/woff2/roboto-700.woff2 +0 -0
- package/src/fonts/woff2/source-serif-4-400.woff2 +0 -0
- package/src/fonts/woff2/source-serif-4-400i.woff2 +0 -0
- package/src/fonts/woff2/source-serif-4-700.woff2 +0 -0
- package/src/icons.tsx +10 -0
- package/src/index.ts +1 -1
- package/src/styles/docblocks.css +128 -13
|
@@ -11,15 +11,18 @@ import type { EditorTheme, EditorView } from '@bendyline/squisq-editor-react';
|
|
|
11
11
|
import '@bendyline/squisq-editor-react/styles';
|
|
12
12
|
import { MediaContext } from '@bendyline/squisq-react';
|
|
13
13
|
import type { MediaProvider } from '@bendyline/squisq/schemas';
|
|
14
|
-
import { createMediaProviderFromContainer } from '@bendyline/squisq/storage';
|
|
15
14
|
import type { FileSystemProvider, FileSystemEntry } from '@bendyline/docblocks/filesystem';
|
|
16
15
|
import {
|
|
17
16
|
IndexedDBFileSystemProvider,
|
|
18
|
-
|
|
17
|
+
ElectronFileSystemProvider,
|
|
18
|
+
FileSystemContentContainer,
|
|
19
|
+
createFileMediaProvider,
|
|
19
20
|
openNativeFolder,
|
|
20
21
|
restoreNativeFolder,
|
|
21
22
|
removeDirectoryHandle,
|
|
22
23
|
} from '@bendyline/docblocks/filesystem';
|
|
24
|
+
import type { ContentContainer } from '@bendyline/squisq/storage';
|
|
25
|
+
import { isElectronHost, getDocblocksHost } from '@bendyline/docblocks/host';
|
|
23
26
|
import type { WorkspaceDescriptor } from '@bendyline/docblocks/workspace';
|
|
24
27
|
import {
|
|
25
28
|
ensureDefaultWorkspace,
|
|
@@ -29,7 +32,7 @@ import {
|
|
|
29
32
|
saveWorkspace,
|
|
30
33
|
touchWorkspace,
|
|
31
34
|
} from '@bendyline/docblocks/workspace';
|
|
32
|
-
import { AppMenu } from '../AppMenu/AppMenu.js';
|
|
35
|
+
import { AppMenu, type ThemePreference } from '../AppMenu/AppMenu.js';
|
|
33
36
|
import { FileExplorer } from '../FileExplorer/FileExplorer.js';
|
|
34
37
|
import { WorkspacePicker } from '../WorkspacePicker/WorkspacePicker.js';
|
|
35
38
|
import { WorkspaceSettingsButton } from '../WorkspacePicker/WorkspaceSettingsButton.js';
|
|
@@ -79,6 +82,64 @@ function parseHash(): { workspaceId: string; filePath: string | null } | null {
|
|
|
79
82
|
};
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
const LAST_STATE_KEY = 'docblocks:lastState';
|
|
86
|
+
|
|
87
|
+
interface LastState {
|
|
88
|
+
workspaceId: string;
|
|
89
|
+
filePath: string;
|
|
90
|
+
view: EditorView;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function saveLastState(state: LastState): void {
|
|
94
|
+
try {
|
|
95
|
+
localStorage.setItem(LAST_STATE_KEY, JSON.stringify(state));
|
|
96
|
+
} catch {
|
|
97
|
+
// ignore quota errors
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function loadLastState(): LastState | null {
|
|
102
|
+
try {
|
|
103
|
+
const raw = localStorage.getItem(LAST_STATE_KEY);
|
|
104
|
+
if (!raw) return null;
|
|
105
|
+
return JSON.parse(raw);
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const THEME_PREF_KEY = 'docblocks:themePreference';
|
|
112
|
+
|
|
113
|
+
function loadThemePreference(): ThemePreference {
|
|
114
|
+
try {
|
|
115
|
+
const raw = localStorage.getItem(THEME_PREF_KEY);
|
|
116
|
+
if (raw === 'light' || raw === 'dark' || raw === 'auto') return raw;
|
|
117
|
+
} catch {
|
|
118
|
+
// ignore
|
|
119
|
+
}
|
|
120
|
+
return 'auto';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function saveThemePreference(pref: ThemePreference): void {
|
|
124
|
+
try {
|
|
125
|
+
localStorage.setItem(THEME_PREF_KEY, pref);
|
|
126
|
+
} catch {
|
|
127
|
+
// ignore quota errors
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function dirnameOf(p: string): string {
|
|
132
|
+
const clean = p.replace(/^\/+/, '');
|
|
133
|
+
const idx = clean.lastIndexOf('/');
|
|
134
|
+
return idx === -1 ? '' : clean.slice(0, idx);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function basenameOf(p: string): string {
|
|
138
|
+
const clean = p.replace(/^\/+/, '');
|
|
139
|
+
const idx = clean.lastIndexOf('/');
|
|
140
|
+
return idx === -1 ? clean : clean.slice(idx + 1);
|
|
141
|
+
}
|
|
142
|
+
|
|
82
143
|
function useIsMobile(breakpoint = 768): boolean {
|
|
83
144
|
const [isMobile, setIsMobile] = useState(
|
|
84
145
|
() =>
|
|
@@ -93,9 +154,49 @@ function useIsMobile(breakpoint = 768): boolean {
|
|
|
93
154
|
return isMobile;
|
|
94
155
|
}
|
|
95
156
|
|
|
96
|
-
|
|
157
|
+
function FolderGlyph() {
|
|
158
|
+
return (
|
|
159
|
+
<svg
|
|
160
|
+
viewBox="0 0 24 24"
|
|
161
|
+
fill="none"
|
|
162
|
+
stroke="currentColor"
|
|
163
|
+
strokeWidth="1.5"
|
|
164
|
+
strokeLinejoin="round"
|
|
165
|
+
aria-hidden="true"
|
|
166
|
+
>
|
|
167
|
+
<path d="M3 7a1 1 0 0 1 1-1h5l2 2h9a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7z" />
|
|
168
|
+
</svg>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function FileGlyph() {
|
|
173
|
+
return (
|
|
174
|
+
<svg
|
|
175
|
+
viewBox="0 0 24 24"
|
|
176
|
+
fill="none"
|
|
177
|
+
stroke="currentColor"
|
|
178
|
+
strokeWidth="1.5"
|
|
179
|
+
strokeLinejoin="round"
|
|
180
|
+
aria-hidden="true"
|
|
181
|
+
>
|
|
182
|
+
<path d="M6 3h8l5 5v12a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z" />
|
|
183
|
+
<path d="M14 3v5h5" />
|
|
184
|
+
</svg>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function DocBlocksShell({ theme: _themeProp = 'auto', logoUrl }: DocBlocksShellProps) {
|
|
97
189
|
const osTheme = useOsTheme();
|
|
98
|
-
const
|
|
190
|
+
const [themePreference, setThemePreference] = useState<ThemePreference>(loadThemePreference);
|
|
191
|
+
// "System default" (auto) always follows the OS — the host's theme prop
|
|
192
|
+
// is kept only for API back-compat and does not override OS detection.
|
|
193
|
+
const resolvedTheme: 'light' | 'dark' =
|
|
194
|
+
themePreference === 'light' || themePreference === 'dark' ? themePreference : osTheme;
|
|
195
|
+
|
|
196
|
+
const handleThemeChange = useCallback((pref: ThemePreference) => {
|
|
197
|
+
setThemePreference(pref);
|
|
198
|
+
saveThemePreference(pref);
|
|
199
|
+
}, []);
|
|
99
200
|
const isMobile = useIsMobile();
|
|
100
201
|
const [mobileShowEditor, setMobileShowEditor] = useState(false);
|
|
101
202
|
const [provider, setProvider] = useState<FileSystemProvider | null>(null);
|
|
@@ -110,17 +211,15 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
110
211
|
/** Suppress popstate handling during programmatic navigation. */
|
|
111
212
|
const skipPopState = useRef(false);
|
|
112
213
|
|
|
113
|
-
/**
|
|
114
|
-
|
|
214
|
+
/**
|
|
215
|
+
* Per-file media container: for `notes.md`, images live in
|
|
216
|
+
* `notes_files/` next to the markdown. Created lazily on first write,
|
|
217
|
+
* picked up automatically if it already exists. Set up by the effect
|
|
218
|
+
* below whenever the selected file (or backing provider) changes.
|
|
219
|
+
*/
|
|
220
|
+
const mediaContainerRef = useRef<ContentContainer | null>(null);
|
|
115
221
|
const [mediaProvider, setMediaProvider] = useState<MediaProvider | null>(null);
|
|
116
222
|
|
|
117
|
-
/** Set up persistent media container for a workspace. */
|
|
118
|
-
const setupMediaContainer = useCallback((workspaceId: string) => {
|
|
119
|
-
const mc = new IndexedDBContentContainer(workspaceId);
|
|
120
|
-
mediaContainerRef.current = mc;
|
|
121
|
-
setMediaProvider(createMediaProviderFromContainer(mc));
|
|
122
|
-
}, []);
|
|
123
|
-
|
|
124
223
|
/** Push a new history entry with the given hash. */
|
|
125
224
|
const pushHash = useCallback((wsId: string, filePath?: string | null) => {
|
|
126
225
|
const hash = buildHash(wsId, filePath);
|
|
@@ -130,17 +229,31 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
130
229
|
}
|
|
131
230
|
}, []);
|
|
132
231
|
|
|
133
|
-
/** Open a workspace (and optionally a file) from identifiers.
|
|
232
|
+
/** Open a workspace (and optionally a file) from identifiers.
|
|
233
|
+
* Returns the FileSystemProvider on success, or null on failure. */
|
|
134
234
|
const openFromIds = useCallback(
|
|
135
|
-
async (
|
|
235
|
+
async (
|
|
236
|
+
wsId: string,
|
|
237
|
+
filePath: string | null,
|
|
238
|
+
push: boolean,
|
|
239
|
+
view?: EditorView,
|
|
240
|
+
): Promise<FileSystemProvider | null> => {
|
|
136
241
|
const workspaces = await listWorkspaces();
|
|
137
242
|
const ws = workspaces.find((w) => w.id === wsId);
|
|
138
|
-
if (!ws) return
|
|
243
|
+
if (!ws) return null;
|
|
139
244
|
|
|
140
245
|
let fsProvider: FileSystemProvider | null = null;
|
|
141
|
-
if (ws.type === 'native') {
|
|
246
|
+
if (ws.type === 'electron-native') {
|
|
247
|
+
if (!ws.rootPath) return null;
|
|
248
|
+
await getDocblocksHost().workspaces.register({
|
|
249
|
+
id: ws.id,
|
|
250
|
+
name: ws.name,
|
|
251
|
+
rootPath: ws.rootPath,
|
|
252
|
+
});
|
|
253
|
+
fsProvider = new ElectronFileSystemProvider(ws.id, ws.name, ws.rootPath);
|
|
254
|
+
} else if (ws.type === 'native') {
|
|
142
255
|
const restored = await restoreNativeFolder(ws.id);
|
|
143
|
-
if (!restored) return
|
|
256
|
+
if (!restored) return null;
|
|
144
257
|
fsProvider = restored;
|
|
145
258
|
} else {
|
|
146
259
|
fsProvider = new IndexedDBFileSystemProvider(ws.id, ws.name);
|
|
@@ -149,8 +262,6 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
149
262
|
setProvider(fsProvider);
|
|
150
263
|
setActiveWorkspaceId(ws.id);
|
|
151
264
|
|
|
152
|
-
setupMediaContainer(ws.id);
|
|
153
|
-
|
|
154
265
|
if (filePath) {
|
|
155
266
|
const content = await fsProvider.readFile(filePath);
|
|
156
267
|
if (content !== null) {
|
|
@@ -158,8 +269,10 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
158
269
|
setSelectedFolder(null);
|
|
159
270
|
setFolderEntries([]);
|
|
160
271
|
setEditorContent(content);
|
|
161
|
-
|
|
272
|
+
const effectiveView = view ?? 'wysiwyg';
|
|
273
|
+
setInitialView(effectiveView);
|
|
162
274
|
setEditorKey((k) => k + 1);
|
|
275
|
+
saveLastState({ workspaceId: wsId, filePath, view: effectiveView });
|
|
163
276
|
} else {
|
|
164
277
|
setSelectedFile(null);
|
|
165
278
|
setSelectedFolder(null);
|
|
@@ -178,9 +291,9 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
178
291
|
if (push) {
|
|
179
292
|
pushHash(ws.id, filePath);
|
|
180
293
|
}
|
|
181
|
-
return
|
|
294
|
+
return fsProvider;
|
|
182
295
|
},
|
|
183
|
-
[pushHash
|
|
296
|
+
[pushHash],
|
|
184
297
|
);
|
|
185
298
|
|
|
186
299
|
const seedWelcomeFile = useCallback(
|
|
@@ -191,16 +304,18 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
191
304
|
if (
|
|
192
305
|
entries.length === 1 &&
|
|
193
306
|
entries[0].kind === 'file' &&
|
|
194
|
-
entries[0].path === '
|
|
307
|
+
entries[0].path.replace(/^\//, '') === 'aboutDocblocks.md'
|
|
195
308
|
) {
|
|
196
|
-
const
|
|
309
|
+
const aboutPath = entries[0].path;
|
|
310
|
+
const content = await fs.readFile(aboutPath);
|
|
197
311
|
if (content !== null) {
|
|
198
|
-
setSelectedFile(
|
|
312
|
+
setSelectedFile(aboutPath);
|
|
199
313
|
setEditorContent(content);
|
|
200
314
|
setInitialView('preview');
|
|
201
315
|
setEditorKey((k) => k + 1);
|
|
202
316
|
setExplorerKey((k) => k + 1);
|
|
203
|
-
pushHash(fs.id,
|
|
317
|
+
pushHash(fs.id, aboutPath);
|
|
318
|
+
saveLastState({ workspaceId: fs.id, filePath: aboutPath, view: 'preview' });
|
|
204
319
|
}
|
|
205
320
|
return;
|
|
206
321
|
}
|
|
@@ -239,6 +354,7 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
239
354
|
setEditorKey((k) => k + 1);
|
|
240
355
|
setExplorerKey((k) => k + 1);
|
|
241
356
|
pushHash(fs.id, welcomePath);
|
|
357
|
+
saveLastState({ workspaceId: fs.id, filePath: welcomePath, view: 'preview' });
|
|
242
358
|
},
|
|
243
359
|
[pushHash],
|
|
244
360
|
);
|
|
@@ -249,27 +365,66 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
249
365
|
// Try restoring from URL hash first
|
|
250
366
|
const hashState = parseHash();
|
|
251
367
|
if (hashState) {
|
|
252
|
-
const
|
|
253
|
-
|
|
368
|
+
const restoredProvider = await openFromIds(
|
|
369
|
+
hashState.workspaceId,
|
|
370
|
+
hashState.filePath,
|
|
371
|
+
false,
|
|
372
|
+
);
|
|
373
|
+
if (restoredProvider) {
|
|
374
|
+
// If the hash had no file, check whether we should auto-select the welcome doc
|
|
375
|
+
if (!hashState.filePath) {
|
|
376
|
+
await seedWelcomeFile(restoredProvider);
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Try restoring last viewed document from localStorage
|
|
383
|
+
const lastState = loadLastState();
|
|
384
|
+
if (lastState) {
|
|
385
|
+
const restoredProvider = await openFromIds(
|
|
386
|
+
lastState.workspaceId,
|
|
387
|
+
lastState.filePath,
|
|
388
|
+
true,
|
|
389
|
+
lastState.view,
|
|
390
|
+
);
|
|
391
|
+
if (restoredProvider) return;
|
|
254
392
|
}
|
|
255
393
|
|
|
256
394
|
let fsProvider: FileSystemProvider | null = null;
|
|
257
395
|
|
|
396
|
+
const electron = isElectronHost();
|
|
258
397
|
const workspaces = await listWorkspaces();
|
|
398
|
+
// On desktop, hide web-only workspaces (indexeddb/native) — only
|
|
399
|
+
// folder-based workspaces are valid.
|
|
400
|
+
const candidates = electron
|
|
401
|
+
? workspaces.filter((w) => w.type === 'electron-native')
|
|
402
|
+
: workspaces.filter((w) => w.type !== 'electron-native');
|
|
259
403
|
// Pick the most recently opened workspace
|
|
260
|
-
const sorted = [...
|
|
404
|
+
const sorted = [...candidates].sort((a, b) =>
|
|
261
405
|
(b.lastOpened ?? '').localeCompare(a.lastOpened ?? ''),
|
|
262
406
|
);
|
|
263
407
|
for (const ws of sorted) {
|
|
264
|
-
if (ws.type === 'native') {
|
|
408
|
+
if (ws.type === 'electron-native') {
|
|
409
|
+
if (!ws.rootPath) continue;
|
|
410
|
+
await getDocblocksHost().workspaces.register({
|
|
411
|
+
id: ws.id,
|
|
412
|
+
name: ws.name,
|
|
413
|
+
rootPath: ws.rootPath,
|
|
414
|
+
});
|
|
415
|
+
const p = new ElectronFileSystemProvider(ws.id, ws.name, ws.rootPath);
|
|
416
|
+
await touchWorkspace(ws.id);
|
|
417
|
+
fsProvider = p;
|
|
418
|
+
setProvider(p);
|
|
419
|
+
setActiveWorkspaceId(ws.id);
|
|
420
|
+
break;
|
|
421
|
+
} else if (ws.type === 'native') {
|
|
265
422
|
const restored = await restoreNativeFolder(ws.id);
|
|
266
423
|
if (restored) {
|
|
267
424
|
await touchWorkspace(ws.id);
|
|
268
425
|
fsProvider = restored;
|
|
269
426
|
setProvider(restored);
|
|
270
427
|
setActiveWorkspaceId(ws.id);
|
|
271
|
-
|
|
272
|
-
setupMediaContainer(ws.id);
|
|
273
428
|
break;
|
|
274
429
|
}
|
|
275
430
|
} else {
|
|
@@ -278,21 +433,35 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
278
433
|
fsProvider = p;
|
|
279
434
|
setProvider(p);
|
|
280
435
|
setActiveWorkspaceId(ws.id);
|
|
281
|
-
|
|
282
|
-
setupMediaContainer(ws.id);
|
|
283
436
|
break;
|
|
284
437
|
}
|
|
285
438
|
}
|
|
286
439
|
|
|
287
440
|
if (!fsProvider) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
441
|
+
if (electron) {
|
|
442
|
+
// Desktop: ask the host for the default folder workspace
|
|
443
|
+
// (creates ~/Documents/DocBlocks on first launch).
|
|
444
|
+
const info = await getDocblocksHost().workspaces.getDefault();
|
|
445
|
+
const descriptor: WorkspaceDescriptor = {
|
|
446
|
+
id: info.id,
|
|
447
|
+
name: info.name,
|
|
448
|
+
type: 'electron-native',
|
|
449
|
+
rootPath: info.rootPath,
|
|
450
|
+
lastOpened: new Date().toISOString(),
|
|
451
|
+
};
|
|
452
|
+
await saveWorkspace(descriptor);
|
|
453
|
+
const p = new ElectronFileSystemProvider(info.id, info.name, info.rootPath);
|
|
454
|
+
fsProvider = p;
|
|
455
|
+
setProvider(p);
|
|
456
|
+
setActiveWorkspaceId(info.id);
|
|
457
|
+
} else {
|
|
458
|
+
// Web: create the IndexedDB default workspace
|
|
459
|
+
const defaultWs = await ensureDefaultWorkspace();
|
|
460
|
+
const p = new IndexedDBFileSystemProvider(defaultWs.id, defaultWs.name);
|
|
461
|
+
fsProvider = p;
|
|
462
|
+
setProvider(p);
|
|
463
|
+
setActiveWorkspaceId(defaultWs.id);
|
|
464
|
+
}
|
|
296
465
|
}
|
|
297
466
|
|
|
298
467
|
// Set initial hash
|
|
@@ -301,7 +470,7 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
301
470
|
// Seed welcome file if workspace is empty
|
|
302
471
|
await seedWelcomeFile(fsProvider);
|
|
303
472
|
})();
|
|
304
|
-
}, [openFromIds, pushHash, seedWelcomeFile
|
|
473
|
+
}, [openFromIds, pushHash, seedWelcomeFile]);
|
|
305
474
|
|
|
306
475
|
// Handle browser back/forward
|
|
307
476
|
useEffect(() => {
|
|
@@ -319,23 +488,86 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
319
488
|
return () => window.removeEventListener('popstate', onPopState);
|
|
320
489
|
}, [openFromIds]);
|
|
321
490
|
|
|
491
|
+
// Track view mode changes (Editor/Raw/Play tabs) and persist to localStorage
|
|
492
|
+
useEffect(() => {
|
|
493
|
+
const handler = (e: MouseEvent) => {
|
|
494
|
+
const target = (e.target as HTMLElement).closest?.('[data-view]');
|
|
495
|
+
if (target) {
|
|
496
|
+
const view = target.getAttribute('data-view') as EditorView;
|
|
497
|
+
if (view && activeWorkspaceId && selectedFile) {
|
|
498
|
+
saveLastState({ workspaceId: activeWorkspaceId, filePath: selectedFile, view });
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
window.addEventListener('click', handler, true);
|
|
503
|
+
return () => window.removeEventListener('click', handler, true);
|
|
504
|
+
}, [activeWorkspaceId, selectedFile]);
|
|
505
|
+
|
|
322
506
|
// Auto-save current file
|
|
323
507
|
useAutoSave(provider, selectedFile, editorContent);
|
|
324
508
|
|
|
509
|
+
// Per-file media: for `notes.md` images live in `notes_files/` beside it.
|
|
510
|
+
// Rebuilds whenever the provider or selected file changes.
|
|
511
|
+
useEffect(() => {
|
|
512
|
+
if (!provider || !selectedFile) {
|
|
513
|
+
mediaContainerRef.current = null;
|
|
514
|
+
setMediaProvider(null);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const parentDir = dirnameOf(selectedFile);
|
|
518
|
+
const base = basenameOf(selectedFile);
|
|
519
|
+
const container = new FileSystemContentContainer(provider, parentDir);
|
|
520
|
+
const mp = createFileMediaProvider(container, base);
|
|
521
|
+
mediaContainerRef.current = container;
|
|
522
|
+
setMediaProvider(mp);
|
|
523
|
+
return () => {
|
|
524
|
+
mp.dispose();
|
|
525
|
+
};
|
|
526
|
+
}, [provider, selectedFile]);
|
|
527
|
+
|
|
528
|
+
// React to external file changes watched by the Electron host (chokidar).
|
|
529
|
+
useEffect(() => {
|
|
530
|
+
if (!isElectronHost()) return;
|
|
531
|
+
if (!provider || !(provider instanceof ElectronFileSystemProvider)) return;
|
|
532
|
+
const unwatch = provider.watch(() => {
|
|
533
|
+
setExplorerKey((k) => k + 1);
|
|
534
|
+
// If the open file's contents changed on disk, reload it (best-effort).
|
|
535
|
+
if (selectedFile) {
|
|
536
|
+
(async () => {
|
|
537
|
+
const content = await provider.readFile(selectedFile);
|
|
538
|
+
if (content !== null && content !== editorContent) {
|
|
539
|
+
setEditorContent(content);
|
|
540
|
+
setEditorKey((k) => k + 1);
|
|
541
|
+
}
|
|
542
|
+
})();
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
return unwatch;
|
|
546
|
+
}, [provider, selectedFile, editorContent]);
|
|
547
|
+
|
|
325
548
|
const handleWorkspaceSelect = useCallback(
|
|
326
549
|
async (ws: WorkspaceDescriptor) => {
|
|
327
550
|
await touchWorkspace(ws.id);
|
|
328
|
-
|
|
551
|
+
let nextProvider: FileSystemProvider | null = null;
|
|
552
|
+
if (ws.type === 'electron-native') {
|
|
553
|
+
if (!ws.rootPath) return;
|
|
554
|
+
await getDocblocksHost().workspaces.register({
|
|
555
|
+
id: ws.id,
|
|
556
|
+
name: ws.name,
|
|
557
|
+
rootPath: ws.rootPath,
|
|
558
|
+
});
|
|
559
|
+
nextProvider = new ElectronFileSystemProvider(ws.id, ws.name, ws.rootPath);
|
|
560
|
+
} else if (ws.type === 'native') {
|
|
329
561
|
const restored = await restoreNativeFolder(ws.id);
|
|
330
|
-
if (restored) {
|
|
331
|
-
setProvider(restored);
|
|
332
|
-
} else {
|
|
562
|
+
if (!restored) {
|
|
333
563
|
// Permission denied or handle lost — fall through without changing provider
|
|
334
564
|
return;
|
|
335
565
|
}
|
|
566
|
+
nextProvider = restored;
|
|
336
567
|
} else {
|
|
337
|
-
|
|
568
|
+
nextProvider = new IndexedDBFileSystemProvider(ws.id, ws.name);
|
|
338
569
|
}
|
|
570
|
+
setProvider(nextProvider);
|
|
339
571
|
setActiveWorkspaceId(ws.id);
|
|
340
572
|
setSelectedFile(null);
|
|
341
573
|
setSelectedFolder(null);
|
|
@@ -343,14 +575,35 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
343
575
|
setEditorContent('');
|
|
344
576
|
setEditorKey((k) => k + 1);
|
|
345
577
|
pushHash(ws.id, null);
|
|
346
|
-
|
|
347
|
-
setupMediaContainer(ws.id);
|
|
348
578
|
},
|
|
349
|
-
[pushHash
|
|
579
|
+
[pushHash],
|
|
350
580
|
);
|
|
351
581
|
|
|
352
582
|
const handleOpenFolder = useCallback(async () => {
|
|
353
583
|
try {
|
|
584
|
+
if (isElectronHost()) {
|
|
585
|
+
const info = await getDocblocksHost().workspaces.pickFolder();
|
|
586
|
+
if (!info) return; // user cancelled
|
|
587
|
+
const descriptor: WorkspaceDescriptor = {
|
|
588
|
+
id: info.id,
|
|
589
|
+
name: info.name,
|
|
590
|
+
type: 'electron-native',
|
|
591
|
+
rootPath: info.rootPath,
|
|
592
|
+
lastOpened: new Date().toISOString(),
|
|
593
|
+
};
|
|
594
|
+
await saveWorkspace(descriptor);
|
|
595
|
+
const provider = new ElectronFileSystemProvider(info.id, info.name, info.rootPath);
|
|
596
|
+
setProvider(provider);
|
|
597
|
+
setActiveWorkspaceId(descriptor.id);
|
|
598
|
+
setSelectedFile(null);
|
|
599
|
+
setSelectedFolder(null);
|
|
600
|
+
setFolderEntries([]);
|
|
601
|
+
setEditorContent('');
|
|
602
|
+
setEditorKey((k) => k + 1);
|
|
603
|
+
pushHash(descriptor.id, null);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
354
607
|
const nativeProvider = await openNativeFolder();
|
|
355
608
|
const descriptor: WorkspaceDescriptor = {
|
|
356
609
|
id: nativeProvider.id,
|
|
@@ -367,12 +620,108 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
367
620
|
setEditorContent('');
|
|
368
621
|
setEditorKey((k) => k + 1);
|
|
369
622
|
pushHash(descriptor.id, null);
|
|
370
|
-
|
|
371
|
-
setupMediaContainer(descriptor.id);
|
|
372
623
|
} catch {
|
|
373
624
|
// User cancelled or API not supported
|
|
374
625
|
}
|
|
375
|
-
}, [pushHash
|
|
626
|
+
}, [pushHash]);
|
|
627
|
+
|
|
628
|
+
const handleNewFile = useCallback(async () => {
|
|
629
|
+
if (!provider) return;
|
|
630
|
+
const name = prompt('New document name:', 'Untitled.md');
|
|
631
|
+
if (!name) return;
|
|
632
|
+
const filename = /\.[a-zA-Z0-9]+$/.test(name) ? name : `${name}.md`;
|
|
633
|
+
await provider.writeFile(filename, '# ' + filename.replace(/\.[^.]+$/, '') + '\n');
|
|
634
|
+
setSelectedFile('/' + filename);
|
|
635
|
+
setEditorContent('# ' + filename.replace(/\.[^.]+$/, '') + '\n');
|
|
636
|
+
setInitialView('wysiwyg');
|
|
637
|
+
setEditorKey((k) => k + 1);
|
|
638
|
+
setExplorerKey((k) => k + 1);
|
|
639
|
+
if (activeWorkspaceId) {
|
|
640
|
+
pushHash(activeWorkspaceId, '/' + filename);
|
|
641
|
+
}
|
|
642
|
+
}, [provider, activeWorkspaceId, pushHash]);
|
|
643
|
+
|
|
644
|
+
const handleRevealWorkspace = useCallback(async () => {
|
|
645
|
+
if (!isElectronHost() || !activeWorkspaceId) return;
|
|
646
|
+
const ws = await getWorkspace(activeWorkspaceId);
|
|
647
|
+
if (ws?.type === 'electron-native' && ws.rootPath) {
|
|
648
|
+
await getDocblocksHost().shell.revealInFolder(ws.rootPath);
|
|
649
|
+
}
|
|
650
|
+
}, [activeWorkspaceId]);
|
|
651
|
+
|
|
652
|
+
// Subscribe to native menu commands (Electron host).
|
|
653
|
+
useEffect(() => {
|
|
654
|
+
if (!isElectronHost()) return;
|
|
655
|
+
const host = getDocblocksHost();
|
|
656
|
+
return host.onMenuCommand((cmd) => {
|
|
657
|
+
switch (cmd) {
|
|
658
|
+
case 'file:new':
|
|
659
|
+
handleNewFile();
|
|
660
|
+
break;
|
|
661
|
+
case 'file:openFolder':
|
|
662
|
+
handleOpenFolder();
|
|
663
|
+
break;
|
|
664
|
+
case 'file:revealWorkspace':
|
|
665
|
+
handleRevealWorkspace();
|
|
666
|
+
break;
|
|
667
|
+
case 'help:viewOnGitHub':
|
|
668
|
+
host.shell.openExternal('https://github.com/bendyline/docblocks');
|
|
669
|
+
break;
|
|
670
|
+
case 'help:checkForUpdates':
|
|
671
|
+
host.updater.checkForUpdates().catch(() => {
|
|
672
|
+
// errors surface via updater.onStatus
|
|
673
|
+
});
|
|
674
|
+
break;
|
|
675
|
+
case 'help:about':
|
|
676
|
+
(async () => {
|
|
677
|
+
const version = await host.updater.getVersion();
|
|
678
|
+
alert(`DocBlocks ${version}`);
|
|
679
|
+
})();
|
|
680
|
+
break;
|
|
681
|
+
default:
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
}, [handleNewFile, handleOpenFolder, handleRevealWorkspace]);
|
|
686
|
+
|
|
687
|
+
// Subscribe to open-file / deep-link requests from the OS.
|
|
688
|
+
useEffect(() => {
|
|
689
|
+
if (!isElectronHost()) return;
|
|
690
|
+
const host = getDocblocksHost();
|
|
691
|
+
return host.onOpenRequest(async (req) => {
|
|
692
|
+
if (req.filePath) {
|
|
693
|
+
const workspaces = (await listWorkspaces()).filter(
|
|
694
|
+
(w) => w.type === 'electron-native' && w.rootPath,
|
|
695
|
+
);
|
|
696
|
+
const match = workspaces.find(
|
|
697
|
+
(w) => req.filePath!.startsWith((w.rootPath ?? '') + '/') || req.filePath === w.rootPath,
|
|
698
|
+
);
|
|
699
|
+
if (match && match.rootPath) {
|
|
700
|
+
const rel = '/' + req.filePath.slice(match.rootPath.length).replace(/^\/+/, '');
|
|
701
|
+
await openFromIds(match.id, rel, true);
|
|
702
|
+
}
|
|
703
|
+
} else if (req.url) {
|
|
704
|
+
try {
|
|
705
|
+
const u = new URL(req.url);
|
|
706
|
+
const path = u.searchParams.get('path');
|
|
707
|
+
if (path) {
|
|
708
|
+
const workspaces = (await listWorkspaces()).filter(
|
|
709
|
+
(w) => w.type === 'electron-native' && w.rootPath,
|
|
710
|
+
);
|
|
711
|
+
const match = workspaces.find(
|
|
712
|
+
(w) => path.startsWith((w.rootPath ?? '') + '/') || path === w.rootPath,
|
|
713
|
+
);
|
|
714
|
+
if (match && match.rootPath) {
|
|
715
|
+
const rel = '/' + path.slice(match.rootPath.length).replace(/^\/+/, '');
|
|
716
|
+
await openFromIds(match.id, rel, true);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
} catch {
|
|
720
|
+
// bad URL, ignore
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
}, [openFromIds]);
|
|
376
725
|
|
|
377
726
|
const handleSelect = useCallback(
|
|
378
727
|
async (path: string, kind: 'file' | 'directory') => {
|
|
@@ -395,6 +744,7 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
395
744
|
setInitialView('wysiwyg');
|
|
396
745
|
setEditorKey((k) => k + 1);
|
|
397
746
|
pushHash(activeWorkspaceId, path);
|
|
747
|
+
saveLastState({ workspaceId: activeWorkspaceId, filePath: path, view: 'wysiwyg' });
|
|
398
748
|
if (isMobile) setMobileShowEditor(true);
|
|
399
749
|
}
|
|
400
750
|
},
|
|
@@ -419,21 +769,30 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
419
769
|
}
|
|
420
770
|
}, [provider, selectedFile, selectedFolder, activeWorkspaceId, pushHash]);
|
|
421
771
|
|
|
422
|
-
/**
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
772
|
+
/**
|
|
773
|
+
* Copy non-markdown files from an imported container into the target
|
|
774
|
+
* markdown file's `_files/` sibling folder, preserving the source's
|
|
775
|
+
* relative structure.
|
|
776
|
+
*/
|
|
777
|
+
const persistImportedMedia = useCallback(
|
|
778
|
+
async (
|
|
779
|
+
source: {
|
|
780
|
+
listFiles(): Promise<Array<{ path: string; mimeType: string }>>;
|
|
781
|
+
readFile(path: string): Promise<ArrayBuffer | null>;
|
|
782
|
+
},
|
|
783
|
+
target: FileSystemProvider,
|
|
784
|
+
importedMarkdownPath: string,
|
|
785
|
+
) => {
|
|
786
|
+
const parentDir = dirnameOf(importedMarkdownPath);
|
|
787
|
+
const folder = basenameOf(importedMarkdownPath).replace(/\.[^.]+$/, '') + '_files';
|
|
788
|
+
const mediaRoot = parentDir ? `${parentDir}/${folder}` : folder;
|
|
430
789
|
const entries = await source.listFiles();
|
|
431
790
|
for (const entry of entries) {
|
|
432
791
|
if (entry.path.endsWith('.md')) continue;
|
|
433
792
|
const data = await source.readFile(entry.path);
|
|
434
|
-
if (data)
|
|
435
|
-
|
|
436
|
-
}
|
|
793
|
+
if (!data) continue;
|
|
794
|
+
const cleanPath = entry.path.replace(/^\/+/, '');
|
|
795
|
+
await target.writeBinary(`${mediaRoot}/${cleanPath}`, new Uint8Array(data));
|
|
437
796
|
}
|
|
438
797
|
},
|
|
439
798
|
[],
|
|
@@ -455,17 +814,17 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
455
814
|
const { docxToContainer } = await import('@bendyline/squisq-formats/docx');
|
|
456
815
|
const container = await docxToContainer(await file.arrayBuffer());
|
|
457
816
|
markdown = (await container.readDocument()) ?? '';
|
|
458
|
-
await
|
|
817
|
+
await persistImportedMedia(container, provider, destPath);
|
|
459
818
|
} else if (ext === '.pdf') {
|
|
460
819
|
const { pdfToContainer } = await import('@bendyline/squisq-formats/pdf');
|
|
461
820
|
const container = await pdfToContainer(await file.arrayBuffer());
|
|
462
821
|
markdown = (await container.readDocument()) ?? '';
|
|
463
|
-
await
|
|
822
|
+
await persistImportedMedia(container, provider, destPath);
|
|
464
823
|
} else if (ext === '.dbk' || ext === '.zip') {
|
|
465
824
|
const { zipToContainer } = await import('@bendyline/squisq-formats/container');
|
|
466
825
|
const container = await zipToContainer(await file.arrayBuffer());
|
|
467
826
|
markdown = (await container.readDocument()) ?? '';
|
|
468
|
-
await
|
|
827
|
+
await persistImportedMedia(container, provider, destPath);
|
|
469
828
|
} else {
|
|
470
829
|
continue;
|
|
471
830
|
}
|
|
@@ -475,15 +834,10 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
475
834
|
console.error(`Failed to import ${file.name}:`, err);
|
|
476
835
|
}
|
|
477
836
|
}
|
|
478
|
-
// Refresh
|
|
479
|
-
const mc = mediaContainerRef.current;
|
|
480
|
-
if (mc) {
|
|
481
|
-
setMediaProvider(createMediaProviderFromContainer(mc));
|
|
482
|
-
}
|
|
483
|
-
// Refresh file tree
|
|
837
|
+
// Refresh file tree so the new markdown files + _files folders show up.
|
|
484
838
|
setExplorerKey((k) => k + 1);
|
|
485
839
|
},
|
|
486
|
-
[provider,
|
|
840
|
+
[provider, persistImportedMedia],
|
|
487
841
|
);
|
|
488
842
|
|
|
489
843
|
const handleEditorChange = useCallback((source: string) => {
|
|
@@ -499,7 +853,7 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
499
853
|
await saveWorkspace({ ...ws, name: newName });
|
|
500
854
|
// Bump key to trigger re-render — workspace name is read from the descriptor, not the provider
|
|
501
855
|
setEditorKey((k) => k + 1);
|
|
502
|
-
}, [activeWorkspaceId
|
|
856
|
+
}, [activeWorkspaceId]);
|
|
503
857
|
|
|
504
858
|
const handleDownloadWorkspace = useCallback(async () => {
|
|
505
859
|
if (!provider) return;
|
|
@@ -526,14 +880,49 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
526
880
|
|
|
527
881
|
const handleRemoveWorkspace = useCallback(async () => {
|
|
528
882
|
if (!activeWorkspaceId) return;
|
|
529
|
-
|
|
530
|
-
|
|
883
|
+
const confirmMsg = isElectronHost()
|
|
884
|
+
? 'Remove this workspace from DocBlocks? The files on disk will not be deleted.'
|
|
885
|
+
: 'Remove this workspace? This cannot be undone.';
|
|
886
|
+
if (!confirm(confirmMsg)) return;
|
|
887
|
+
|
|
888
|
+
const ws = await getWorkspace(activeWorkspaceId);
|
|
889
|
+
if (ws?.type === 'electron-native') {
|
|
890
|
+
try {
|
|
891
|
+
await getDocblocksHost().workspaces.unregister(activeWorkspaceId);
|
|
892
|
+
} catch {
|
|
893
|
+
// ignore — host cleanup is best-effort
|
|
894
|
+
}
|
|
895
|
+
} else {
|
|
896
|
+
await removeDirectoryHandle(activeWorkspaceId);
|
|
897
|
+
}
|
|
531
898
|
await removeWorkspace(activeWorkspaceId);
|
|
899
|
+
|
|
900
|
+
const electron = isElectronHost();
|
|
532
901
|
// Switch to most recent remaining workspace or create default
|
|
533
|
-
const remaining = await listWorkspaces()
|
|
902
|
+
const remaining = (await listWorkspaces()).filter((w) =>
|
|
903
|
+
electron ? w.type === 'electron-native' : w.type !== 'electron-native',
|
|
904
|
+
);
|
|
534
905
|
if (remaining.length > 0) {
|
|
535
906
|
const next = remaining[0];
|
|
536
907
|
await handleWorkspaceSelect(next);
|
|
908
|
+
} else if (electron) {
|
|
909
|
+
const info = await getDocblocksHost().workspaces.getDefault();
|
|
910
|
+
const descriptor: WorkspaceDescriptor = {
|
|
911
|
+
id: info.id,
|
|
912
|
+
name: info.name,
|
|
913
|
+
type: 'electron-native',
|
|
914
|
+
rootPath: info.rootPath,
|
|
915
|
+
lastOpened: new Date().toISOString(),
|
|
916
|
+
};
|
|
917
|
+
await saveWorkspace(descriptor);
|
|
918
|
+
const p = new ElectronFileSystemProvider(info.id, info.name, info.rootPath);
|
|
919
|
+
setProvider(p);
|
|
920
|
+
setActiveWorkspaceId(info.id);
|
|
921
|
+
setSelectedFile(null);
|
|
922
|
+
setSelectedFolder(null);
|
|
923
|
+
setFolderEntries([]);
|
|
924
|
+
setEditorContent('');
|
|
925
|
+
setEditorKey((k) => k + 1);
|
|
537
926
|
} else {
|
|
538
927
|
const defaultWs = await ensureDefaultWorkspace();
|
|
539
928
|
const fsProvider = new IndexedDBFileSystemProvider(defaultWs.id, defaultWs.name);
|
|
@@ -544,10 +933,8 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
544
933
|
setFolderEntries([]);
|
|
545
934
|
setEditorContent('');
|
|
546
935
|
setEditorKey((k) => k + 1);
|
|
547
|
-
|
|
548
|
-
setupMediaContainer(defaultWs.id);
|
|
549
936
|
}
|
|
550
|
-
}, [activeWorkspaceId, handleWorkspaceSelect
|
|
937
|
+
}, [activeWorkspaceId, handleWorkspaceSelect]);
|
|
551
938
|
|
|
552
939
|
return (
|
|
553
940
|
<div className={`db-shell${isMobile ? ' db-shell--mobile' : ''}`} data-theme={resolvedTheme}>
|
|
@@ -557,7 +944,11 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
557
944
|
{(!isMobile || !mobileShowEditor) && (
|
|
558
945
|
<div className="db-shell-sidebar">
|
|
559
946
|
<div className="db-shell-sidebar-header">
|
|
560
|
-
<AppMenu
|
|
947
|
+
<AppMenu
|
|
948
|
+
logoUrl={logoUrl}
|
|
949
|
+
themePreference={themePreference}
|
|
950
|
+
onThemeChange={handleThemeChange}
|
|
951
|
+
/>
|
|
561
952
|
<WorkspacePicker
|
|
562
953
|
activeWorkspaceId={activeWorkspaceId}
|
|
563
954
|
onSelect={handleWorkspaceSelect}
|
|
@@ -627,7 +1018,9 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
627
1018
|
</button>
|
|
628
1019
|
)}
|
|
629
1020
|
<div className="db-folder-view-header">
|
|
630
|
-
<span className="db-folder-view-icon"
|
|
1021
|
+
<span className="db-folder-view-icon">
|
|
1022
|
+
<FolderGlyph />
|
|
1023
|
+
</span>
|
|
631
1024
|
<span className="db-folder-view-path">{selectedFolder}</span>
|
|
632
1025
|
</div>
|
|
633
1026
|
{folderEntries.length === 0 ? (
|
|
@@ -641,7 +1034,7 @@ export function DocBlocksShell({ theme = 'auto', logoUrl }: DocBlocksShellProps)
|
|
|
641
1034
|
onClick={() => handleSelect(entry.path, entry.kind)}
|
|
642
1035
|
>
|
|
643
1036
|
<span className="db-folder-view-item-icon">
|
|
644
|
-
{entry.kind === 'directory' ?
|
|
1037
|
+
{entry.kind === 'directory' ? <FolderGlyph /> : <FileGlyph />}
|
|
645
1038
|
</span>
|
|
646
1039
|
{entry.name}
|
|
647
1040
|
</li>
|