@bendyline/docblocks-react 2.7.3 → 2.7.4

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 (37) hide show
  1. package/THIRD_PARTY_NOTICES.txt +23 -23
  2. package/dist/{AiDiagrams-QE3LVYFK.js → AiDiagrams-QZMJNGDT.js} +3 -3
  3. package/dist/{AiEditorAssistant-YLJ5OZK2.js → AiEditorAssistant-4HFFGVU4.js} +5 -5
  4. package/dist/{AiStudyDialog-4TQZZGU3.js → AiStudyDialog-P3E7PJV5.js} +2 -2
  5. package/dist/{AudioExportDialog-F23QXNPB.js → AudioExportDialog-IOYIEAER.js} +1 -1
  6. package/dist/{DocumentConflictDialog-5LRVIGXS.js → DocumentConflictDialog-DZ2BV2IH.js} +1 -1
  7. package/dist/ExportDialog-LGWBXIWN.js +9 -0
  8. package/dist/{ExportToolbarControls-A2YBKUUD.js → ExportToolbarControls-HOV424TO.js} +84 -13
  9. package/dist/{GitUI-RCWUZVS6.js → GitUI-F3CNBURE.js} +1 -1
  10. package/dist/{KeyboardShortcutsDialog-G5OBDTNR.js → KeyboardShortcutsDialog-HVKDFP5Z.js} +1 -1
  11. package/dist/{PresentationMaker-P2JP6UUX.js → PresentationMaker-MUMFAXEI.js} +3 -3
  12. package/dist/{QuickOpenDialog-2KK4SEBU.js → QuickOpenDialog-ENVFGIHS.js} +1 -1
  13. package/dist/{Settings-ByNImjk5.d.ts → Settings-CfhY489G.d.ts} +1 -1
  14. package/dist/{ShareDialog-XUA2OVGI.js → ShareDialog-PF7D7ORP.js} +1 -1
  15. package/dist/{SpeechToolbarControl-CLU7RB2G.js → SpeechToolbarControl-KNI2C4JJ.js} +1 -1
  16. package/dist/{TextFromNarration-NITJQ4IB.js → TextFromNarration-EYOMW2IW.js} +1 -1
  17. package/dist/{chunk-6PA7QBZT.js → chunk-7CSBRAMT.js} +20 -4
  18. package/dist/{chunk-YPIVCCTB.js → chunk-7V34VUK6.js} +1 -1
  19. package/dist/{chunk-3CE5RH2B.js → chunk-D4HIOS7E.js} +2 -2
  20. package/dist/{chunk-ZMCQO57Q.js → chunk-FBPCURCS.js} +8 -1
  21. package/dist/{chunk-Y5OE3HJC.js → chunk-GFK4TCKN.js} +11 -5
  22. package/dist/chunk-H3BK5MK6.js +34 -0
  23. package/dist/{chunk-6B7G33D5.js → chunk-KBUHWZCF.js} +1 -1
  24. package/dist/{chunk-EOU2JWAQ.js → chunk-PNZFIIWW.js} +70 -11
  25. package/dist/{chunk-56KO2LYN.js → chunk-VJPGZCFS.js} +4 -0
  26. package/dist/editor.js +3 -3
  27. package/dist/export/index.d.ts +9 -2
  28. package/dist/export/index.js +4 -4
  29. package/dist/index.d.ts +7 -3
  30. package/dist/index.js +503 -186
  31. package/dist/{run-export-OHRNT2TP.js → run-export-SPNNH32M.js} +1 -1
  32. package/dist/settings/index.d.ts +1 -1
  33. package/dist/settings/index.js +2 -2
  34. package/package.json +8 -8
  35. package/src/styles/docblocks.css +32 -9
  36. package/dist/ExportDialog-JRVZCG3J.js +0 -9
  37. package/dist/chunk-YBEYTVU2.js +0 -12
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  } from "./chunk-DICE7VBJ.js";
18
18
  import {
19
19
  useConfirmDialog
20
- } from "./chunk-6B7G33D5.js";
20
+ } from "./chunk-KBUHWZCF.js";
21
21
  import {
22
22
  createSpeechInputProvider
23
23
  } from "./chunk-KBDZDFPU.js";
@@ -51,7 +51,7 @@ import {
51
51
  } from "./chunk-KWG6UP5O.js";
52
52
  import {
53
53
  runExport
54
- } from "./chunk-6PA7QBZT.js";
54
+ } from "./chunk-7CSBRAMT.js";
55
55
  import {
56
56
  createImageSaveOutput
57
57
  } from "./chunk-UONFWKJ7.js";
@@ -87,7 +87,7 @@ import {
87
87
  saveProofingPreferences,
88
88
  saveThemePreference,
89
89
  saveWriteCanvasPreferences
90
- } from "./chunk-ZMCQO57Q.js";
90
+ } from "./chunk-FBPCURCS.js";
91
91
  import {
92
92
  FindButton,
93
93
  focusFindInput,
@@ -100,18 +100,18 @@ import {
100
100
  matchShortcut,
101
101
  shortcutLabel
102
102
  } from "./chunk-ACQF63SB.js";
103
- import "./chunk-EOU2JWAQ.js";
103
+ import "./chunk-PNZFIIWW.js";
104
104
  import {
105
105
  buildIssueReportUrl,
106
106
  displayBuildDate,
107
107
  localDateStamp,
108
108
  openExternalUrl
109
- } from "./chunk-YPIVCCTB.js";
109
+ } from "./chunk-7V34VUK6.js";
110
110
  import "./chunk-K4I3NJFL.js";
111
111
  import {
112
112
  ExportDialog
113
- } from "./chunk-Y5OE3HJC.js";
114
- import "./chunk-YBEYTVU2.js";
113
+ } from "./chunk-GFK4TCKN.js";
114
+ import "./chunk-H3BK5MK6.js";
115
115
  import {
116
116
  DEFAULT_OPTIONS,
117
117
  loadLastExportOptions
@@ -119,13 +119,14 @@ import {
119
119
  import {
120
120
  Dialog,
121
121
  dismissTopmostDialog
122
- } from "./chunk-56KO2LYN.js";
122
+ } from "./chunk-VJPGZCFS.js";
123
123
 
124
124
  // src/FileExplorer/FileExplorer.tsx
125
125
  import { useCallback as useCallback5, useEffect as useEffect5, useMemo, useRef as useRef5, useState as useState4 } from "react";
126
126
  import {
127
127
  isFileSystemMoveStateError,
128
- parseWorkspacePath as parseWorkspacePath4
128
+ parseWorkspacePath as parseWorkspacePath4,
129
+ tryParseWorkspacePath as tryParseWorkspacePath2
129
130
  } from "@bendyline/docblocks/filesystem";
130
131
 
131
132
  // src/FileExplorer/useFileTree.ts
@@ -139,6 +140,7 @@ import {
139
140
  workspacePathDirname
140
141
  } from "@bendyline/docblocks/filesystem";
141
142
  var DIRECTORY_NOT_FOUND_RETRY_DELAYS_MS = Object.freeze([40, 120]);
143
+ var AUTO_RETRY_DELAYS_MS = Object.freeze([1e3, 4e3, 15e3]);
142
144
  function normalisePath(path) {
143
145
  return parseWorkspacePath(path);
144
146
  }
@@ -167,10 +169,13 @@ function sameEntry(left, right) {
167
169
  if (left.kind !== right.kind || left.name !== right.name || left.path !== right.path)
168
170
  return false;
169
171
  if (left.kind === "file" && right.kind === "file") {
170
- return left.lastModified === right.lastModified;
172
+ return left.lastModified === right.lastModified && left.readIssue?.code === right.readIssue?.code && left.readIssue?.message === right.readIssue?.message;
171
173
  }
172
174
  return true;
173
175
  }
176
+ function hasUnreadableFile(entries) {
177
+ return entries.some((entry) => entry.kind === "file" && entry.readIssue !== void 0);
178
+ }
174
179
  function reconcileEntries(current, incoming) {
175
180
  const currentByPath = new Map(current.map((entry) => [entry.path, entry]));
176
181
  const reconciled = incoming.map((entry) => {
@@ -189,7 +194,8 @@ async function readProviderDirectory(provider, path) {
189
194
  kind: entry.kind,
190
195
  name: entry.name,
191
196
  path: entry.path,
192
- ...entry.kind === "file" ? { lastModified: entry.lastModified } : {}
197
+ ...entry.kind === "file" ? { lastModified: entry.lastModified } : {},
198
+ ...entry.kind === "file" && entry.readIssue ? { readIssue: entry.readIssue } : {}
193
199
  }));
194
200
  };
195
201
  for (let attempt = 0; ; attempt += 1) {
@@ -237,64 +243,111 @@ function useFileTree(provider, metadataRefreshKey, metadataRefreshPath) {
237
243
  providerRef.current = provider;
238
244
  const directoryRequestSequenceRef = useRef(0);
239
245
  const directoryRequestsRef = useRef(/* @__PURE__ */ new Map());
246
+ const autoRetriesRef = useRef(/* @__PURE__ */ new Map());
247
+ const autoRetryLoadRef = useRef(async () => void 0);
240
248
  const reportRootIssue = useCallback((caught) => {
241
249
  setRootIssue(readIssue(caught, ""));
242
250
  }, []);
243
- const loadRoot = useCallback(async (showLoading = true) => {
244
- const sourceProvider = providerRef.current;
245
- if (!sourceProvider) {
246
- setEntries([]);
247
- setLoading(false);
248
- return;
249
- }
250
- const requestId = ++directoryRequestSequenceRef.current;
251
- directoryRequestsRef.current.set("", requestId);
252
- const isCurrent = () => providerRef.current === sourceProvider && directoryRequestsRef.current.get("") === requestId;
253
- if (showLoading) setLoading(true);
254
- try {
255
- const root = await readProviderDirectory(sourceProvider, "");
256
- if (!isCurrent()) return;
257
- setEntries((current) => reconcileEntries(current, root));
258
- setRootIssue(null);
259
- } catch (caught) {
260
- if (isCurrent()) setRootIssue(readIssue(caught, ""));
261
- } finally {
262
- if (isCurrent()) setLoading(false);
263
- }
251
+ const cancelAutoRetry = useCallback((directoryPath) => {
252
+ const state = autoRetriesRef.current.get(directoryPath);
253
+ if (state?.timer) clearTimeout(state.timer);
254
+ autoRetriesRef.current.delete(directoryPath);
264
255
  }, []);
265
- const loadChildren = useCallback(async (dirPath) => {
266
- const sourceProvider = providerRef.current;
267
- if (!sourceProvider) return;
268
- const canonical = normalisePath(dirPath);
269
- const requestId = ++directoryRequestSequenceRef.current;
270
- directoryRequestsRef.current.set(canonical, requestId);
271
- const isCurrent = () => providerRef.current === sourceProvider && directoryRequestsRef.current.get(canonical) === requestId;
272
- try {
273
- const children = await readProviderDirectory(sourceProvider, dirPath);
274
- if (!isCurrent()) return;
275
- setChildEntries((prev) => {
276
- const current = prev.get(canonical) ?? [];
277
- const reconciled = reconcileEntries(current, children);
278
- if (prev.has(canonical) && reconciled === current) return prev;
279
- const next = new Map(prev);
280
- next.set(canonical, reconciled);
281
- return next;
282
- });
283
- setChildIssues((prev) => {
284
- if (!prev.has(canonical)) return prev;
285
- const next = new Map(prev);
286
- next.delete(canonical);
287
- return next;
288
- });
289
- } catch (caught) {
290
- if (!isCurrent()) return;
291
- setChildIssues((prev) => {
292
- const next = new Map(prev);
293
- next.set(canonical, readIssue(caught, dirPath));
294
- return next;
295
- });
256
+ const cancelAllAutoRetries = useCallback(() => {
257
+ for (const state of autoRetriesRef.current.values()) {
258
+ if (state.timer) clearTimeout(state.timer);
296
259
  }
260
+ autoRetriesRef.current.clear();
297
261
  }, []);
262
+ const settleAutoRetry = useCallback(
263
+ (directoryPath, needsRetry) => {
264
+ if (!needsRetry) {
265
+ cancelAutoRetry(directoryPath);
266
+ return;
267
+ }
268
+ const current = autoRetriesRef.current.get(directoryPath);
269
+ if (current?.timer) return;
270
+ const attempts = current?.attempts ?? 0;
271
+ const delay = AUTO_RETRY_DELAYS_MS[attempts];
272
+ if (delay === void 0) return;
273
+ const next = { attempts: attempts + 1, timer: null };
274
+ next.timer = setTimeout(() => {
275
+ next.timer = null;
276
+ void autoRetryLoadRef.current(directoryPath);
277
+ }, delay);
278
+ autoRetriesRef.current.set(directoryPath, next);
279
+ },
280
+ [cancelAutoRetry]
281
+ );
282
+ const loadRoot = useCallback(
283
+ async (showLoading = true) => {
284
+ const sourceProvider = providerRef.current;
285
+ if (!sourceProvider) {
286
+ setEntries([]);
287
+ setLoading(false);
288
+ return;
289
+ }
290
+ const requestId = ++directoryRequestSequenceRef.current;
291
+ directoryRequestsRef.current.set("", requestId);
292
+ const isCurrent = () => providerRef.current === sourceProvider && directoryRequestsRef.current.get("") === requestId;
293
+ if (showLoading) setLoading(true);
294
+ try {
295
+ const root = await readProviderDirectory(sourceProvider, "");
296
+ if (!isCurrent()) return;
297
+ setEntries((current) => reconcileEntries(current, root));
298
+ setRootIssue(null);
299
+ settleAutoRetry("", hasUnreadableFile(root));
300
+ } catch (caught) {
301
+ if (isCurrent()) {
302
+ const issue = readIssue(caught, "");
303
+ setRootIssue(issue);
304
+ settleAutoRetry("", issue.retryable);
305
+ }
306
+ } finally {
307
+ if (isCurrent()) setLoading(false);
308
+ }
309
+ },
310
+ [settleAutoRetry]
311
+ );
312
+ const loadChildren = useCallback(
313
+ async (dirPath) => {
314
+ const sourceProvider = providerRef.current;
315
+ if (!sourceProvider) return;
316
+ const canonical = normalisePath(dirPath);
317
+ const requestId = ++directoryRequestSequenceRef.current;
318
+ directoryRequestsRef.current.set(canonical, requestId);
319
+ const isCurrent = () => providerRef.current === sourceProvider && directoryRequestsRef.current.get(canonical) === requestId;
320
+ try {
321
+ const children = await readProviderDirectory(sourceProvider, dirPath);
322
+ if (!isCurrent()) return;
323
+ setChildEntries((prev) => {
324
+ const current = prev.get(canonical) ?? [];
325
+ const reconciled = reconcileEntries(current, children);
326
+ if (prev.has(canonical) && reconciled === current) return prev;
327
+ const next = new Map(prev);
328
+ next.set(canonical, reconciled);
329
+ return next;
330
+ });
331
+ setChildIssues((prev) => {
332
+ if (!prev.has(canonical)) return prev;
333
+ const next = new Map(prev);
334
+ next.delete(canonical);
335
+ return next;
336
+ });
337
+ settleAutoRetry(canonical, hasUnreadableFile(children));
338
+ } catch (caught) {
339
+ if (!isCurrent()) return;
340
+ const issue = readIssue(caught, dirPath);
341
+ setChildIssues((prev) => {
342
+ const next = new Map(prev);
343
+ next.set(canonical, issue);
344
+ return next;
345
+ });
346
+ settleAutoRetry(canonical, issue.retryable);
347
+ }
348
+ },
349
+ [settleAutoRetry]
350
+ );
298
351
  useEffect(() => {
299
352
  setEntries([]);
300
353
  setExpanded(/* @__PURE__ */ new Set());
@@ -304,8 +357,10 @@ function useFileTree(provider, metadataRefreshKey, metadataRefreshPath) {
304
357
  setSelectedKind(null);
305
358
  setRootIssue(null);
306
359
  directoryRequestsRef.current.clear();
360
+ cancelAllAutoRetries();
307
361
  void loadRoot();
308
- }, [provider, loadRoot]);
362
+ return cancelAllAutoRetries;
363
+ }, [provider, loadRoot, cancelAllAutoRetries]);
309
364
  const toggleExpand = useCallback(
310
365
  (path) => {
311
366
  setExpanded((prev) => {
@@ -352,10 +407,12 @@ function useFileTree(provider, metadataRefreshKey, metadataRefreshPath) {
352
407
  }, [loadRoot, loadChildren, expanded]);
353
408
  const retryDirectory = useCallback(
354
409
  async (path) => {
355
- if (normalisePath(path) === "") await loadRoot();
410
+ const canonical = normalisePath(path);
411
+ cancelAutoRetry(canonical);
412
+ if (canonical === "") await loadRoot();
356
413
  else await loadChildren(path);
357
414
  },
358
- [loadRoot, loadChildren]
415
+ [loadRoot, loadChildren, cancelAutoRetry]
359
416
  );
360
417
  const refreshRef = useRef(refresh);
361
418
  refreshRef.current = refresh;
@@ -366,6 +423,7 @@ function useFileTree(provider, metadataRefreshKey, metadataRefreshPath) {
366
423
  },
367
424
  [loadChildren, loadRoot]
368
425
  );
426
+ autoRetryLoadRef.current = refreshDirectory;
369
427
  const previousMetadataRefreshKeyRef = useRef(metadataRefreshKey);
370
428
  useEffect(() => {
371
429
  if (Object.is(previousMetadataRefreshKeyRef.current, metadataRefreshKey)) return;
@@ -542,12 +600,13 @@ function useFileTree(provider, metadataRefreshKey, metadataRefreshPath) {
542
600
  setExpanded(nextExpanded);
543
601
  setChildEntries(/* @__PURE__ */ new Map());
544
602
  setChildIssues(/* @__PURE__ */ new Map());
603
+ cancelAllAutoRetries();
545
604
  await loadRoot();
546
605
  for (const dirPath of nextExpanded) {
547
606
  await loadChildren(dirPath);
548
607
  }
549
608
  },
550
- [selectedPath, entries, childEntries, expanded, loadRoot, loadChildren]
609
+ [selectedPath, entries, childEntries, expanded, loadRoot, loadChildren, cancelAllAutoRetries]
551
610
  );
552
611
  return {
553
612
  entries,
@@ -788,6 +847,11 @@ function FileTreeNode({
788
847
  }) {
789
848
  const [renaming, setRenaming] = useState2(false);
790
849
  const [renameValue, setRenameValue] = useState2(entry.name);
850
+ const readIssue2 = entry.kind === "file" ? entry.readIssue : void 0;
851
+ const rowStatus = [
852
+ badge?.label,
853
+ readIssue2 ? `could not be read: ${readIssue2.message}` : void 0
854
+ ].filter((part) => part !== void 0);
791
855
  const [showContext, setShowContext] = useState2(false);
792
856
  const [contextPos, setContextPos] = useState2({ x: 0, y: 0 });
793
857
  const [actionError, setActionError] = useState2(null);
@@ -1034,7 +1098,7 @@ function FileTreeNode({
1034
1098
  "data-path": entry.path,
1035
1099
  "aria-expanded": isDir ? expanded : void 0,
1036
1100
  "aria-selected": selected,
1037
- "aria-label": badge ? `${entry.name}, ${badge.label}` : void 0,
1101
+ "aria-label": rowStatus.length > 0 ? `${entry.name}, ${rowStatus.join(", ")}` : void 0,
1038
1102
  "aria-level": depth + 1,
1039
1103
  "aria-posinset": posInSet,
1040
1104
  "aria-setsize": setSize,
@@ -1086,6 +1150,15 @@ function FileTreeNode({
1086
1150
  ) : /* @__PURE__ */ jsxs(Fragment, { children: [
1087
1151
  /* @__PURE__ */ jsx2("span", { className: "db-tree-label", children: displayFileName(entry.name) }),
1088
1152
  badge && /* @__PURE__ */ jsx2("span", { className: `db-git-badge db-git-badge--${badge.kind}`, "aria-hidden": "true", children: badge.glyph }),
1153
+ readIssue2 && /* @__PURE__ */ jsx2(
1154
+ "span",
1155
+ {
1156
+ className: "db-tree-read-issue",
1157
+ title: `Could not read this file: ${readIssue2.message}`,
1158
+ "aria-hidden": "true",
1159
+ children: "!"
1160
+ }
1161
+ ),
1089
1162
  entry.kind === "file" && entry.lastModified && /* @__PURE__ */ jsx2(LastModifiedTime, { value: entry.lastModified }),
1090
1163
  /* @__PURE__ */ jsx2(
1091
1164
  "button",
@@ -1717,6 +1790,7 @@ import {
1717
1790
  FsError as FsError3,
1718
1791
  SQUISQ_RUNTIME_DIRECTORY_NAME,
1719
1792
  getFileSystemProviderV2 as getFileSystemProviderV23,
1793
+ findWikiArticle,
1720
1794
  parseWorkspacePath as parseWorkspacePath3
1721
1795
  } from "@bendyline/docblocks/filesystem";
1722
1796
  import {
@@ -2149,10 +2223,7 @@ async function writeRuntimeIfNeeded(provider, runtimePath) {
2149
2223
  if (current !== PLAYER_BUNDLE) await writeProviderText(provider, runtimePath, PLAYER_BUNDLE);
2150
2224
  }
2151
2225
  async function renderPlainOutsideInHtml(provider, layout, markdown, themeId) {
2152
- const [{ parseMarkdown }, { markdownDocToPlainHtml }] = await Promise.all([
2153
- import("@bendyline/squisq/markdown"),
2154
- import("@bendyline/squisq-formats/html")
2155
- ]);
2226
+ const { markdownDocToPlainHtml } = await import("@bendyline/squisq-formats/html");
2156
2227
  const container = new FileSystemContentContainer(provider, layout.companionDirectory);
2157
2228
  const images = /* @__PURE__ */ new Map();
2158
2229
  for (const entry of await container.listFiles()) {
@@ -2161,7 +2232,7 @@ async function renderPlainOutsideInHtml(provider, layout, markdown, themeId) {
2161
2232
  images.set(`./${entry.path}`, outputPath);
2162
2233
  }
2163
2234
  return new TextEncoder().encode(
2164
- markdownDocToPlainHtml(parseMarkdown(markdown), {
2235
+ markdownDocToPlainHtml(markdown, {
2165
2236
  title: layout.stem,
2166
2237
  images: images.size > 0 ? images : void 0,
2167
2238
  ...themeId ? { themeId } : {}
@@ -2171,9 +2242,22 @@ async function renderPlainOutsideInHtml(provider, layout, markdown, themeId) {
2171
2242
  async function prepareOutsideInRender(provider, layout, markdown, render) {
2172
2243
  const themeId = await resolveFallbackRenderTheme(markdown, render);
2173
2244
  const htmlOutput = layout.format === "html" ? await readOutsideInHtmlOutput(markdown) : null;
2245
+ let renderedSource = markdown;
2246
+ if (layout.format === "html") {
2247
+ const { parseMarkdown, resolveWikiLinks } = await import("@bendyline/squisq/markdown");
2248
+ renderedSource = await resolveWikiLinks(parseMarkdown(markdown), async (target) => {
2249
+ const article = await findWikiArticle(provider, layout.targetPath, target, ".html");
2250
+ return article.exists ? article.relativePath.split("/").map(encodeURIComponent).join("/") : null;
2251
+ });
2252
+ }
2174
2253
  if (htmlOutput === "static") {
2175
2254
  return {
2176
- bytes: await renderPlainOutsideInHtml(provider, layout, markdown, themeId),
2255
+ bytes: await renderPlainOutsideInHtml(
2256
+ provider,
2257
+ layout,
2258
+ renderedSource,
2259
+ themeId
2260
+ ),
2177
2261
  runtimePath: null
2178
2262
  };
2179
2263
  }
@@ -2181,7 +2265,7 @@ async function prepareOutsideInRender(provider, layout, markdown, render) {
2181
2265
  const outputDirectory = dirname(layout.targetPath);
2182
2266
  const rendered = await renderOutsideInDocument(
2183
2267
  {
2184
- markdown,
2268
+ markdown: renderedSource,
2185
2269
  targetPath: layout.targetPath,
2186
2270
  container: new FileSystemContentContainer(provider, layout.companionDirectory)
2187
2271
  },
@@ -2416,6 +2500,10 @@ var NEW_ITEM_HINT_ID = "db-new-item-hint";
2416
2500
  function normalisePath2(path) {
2417
2501
  return parseWorkspacePath4(path);
2418
2502
  }
2503
+ function readIssueText(issue) {
2504
+ const failingPath = tryParseWorkspacePath2(issue.path);
2505
+ return failingPath && failingPath !== issue.directoryPath ? `${failingPath}: ${issue.message}` : issue.message;
2506
+ }
2419
2507
  function hasEquivalentPath2(paths, path) {
2420
2508
  const canonical = normalisePath2(path);
2421
2509
  return paths.has(path) || paths.has(canonical) || canonical !== "" && paths.has(`/${canonical}`);
@@ -2880,7 +2968,7 @@ function FileExplorer({
2880
2968
  role: "alert",
2881
2969
  "data-directory-path": childIssue.directoryPath,
2882
2970
  children: [
2883
- /* @__PURE__ */ jsx5("span", { children: childIssue.message }),
2971
+ /* @__PURE__ */ jsx5("span", { children: readIssueText(childIssue) }),
2884
2972
  " ",
2885
2973
  /* @__PURE__ */ jsx5(
2886
2974
  "button",
@@ -3161,7 +3249,7 @@ function FileExplorer({
3161
3249
  role: "alert",
3162
3250
  "data-directory-path": tree.rootIssue.directoryPath,
3163
3251
  children: [
3164
- /* @__PURE__ */ jsx5("span", { children: tree.rootIssue.message }),
3252
+ /* @__PURE__ */ jsx5("span", { children: readIssueText(tree.rootIssue) }),
3165
3253
  " ",
3166
3254
  /* @__PURE__ */ jsx5(
3167
3255
  "button",
@@ -3686,13 +3774,15 @@ import {
3686
3774
  FileSystemContentContainer as FileSystemContentContainer2,
3687
3775
  createFileMediaProvider,
3688
3776
  decodeUtf8Text,
3689
- FsError as FsError5,
3690
- getFileSystemProviderV2 as getFileSystemProviderV210,
3777
+ FsError as FsError6,
3778
+ getFileSystemProviderV2 as getFileSystemProviderV211,
3691
3779
  isQuotaExceededError,
3692
3780
  moveFileSystemEntry as moveFileSystemEntry2,
3693
3781
  rewriteCompanionReferences,
3694
3782
  parseWorkspacePath as parseWorkspacePath8,
3695
- workspacePathContains
3783
+ workspacePathContains,
3784
+ findWikiArticle as findWikiArticle2,
3785
+ suggestWikiArticles
3696
3786
  } from "@bendyline/docblocks/filesystem";
3697
3787
  import {
3698
3788
  createFileSystemDocumentTarget as createFileSystemDocumentTarget2,
@@ -3715,12 +3805,14 @@ import {
3715
3805
  getWorkspace,
3716
3806
  listWorkspaces as listWorkspaces2,
3717
3807
  reconcileElectronWorkspaceDescriptors,
3718
- removeWorkspace,
3719
- saveWorkspace as saveWorkspace2,
3808
+ removeWorkspace as removeWorkspace2,
3809
+ saveWorkspace as saveWorkspace3,
3720
3810
  touchWorkspace as touchWorkspace2,
3721
3811
  registerTransientWorkspace,
3722
3812
  getTransientWorkspace,
3723
- unregisterTransientWorkspace
3813
+ unregisterTransientWorkspace,
3814
+ appendWorkspaceBackup,
3815
+ finishWorkspaceBackup
3724
3816
  } from "@bendyline/docblocks/workspace";
3725
3817
 
3726
3818
  // src/AppMenu/AppMenu.tsx
@@ -4077,6 +4169,7 @@ function AppMenu({
4077
4169
  onVersioningPreferenceChange,
4078
4170
  onOpenWorkspaceSettings,
4079
4171
  onDownloadAllWorkspaces,
4172
+ onRestoreBackup,
4080
4173
  onShowKeyboardShortcuts,
4081
4174
  onKeepBrowserData,
4082
4175
  onInstallApp,
@@ -4211,7 +4304,7 @@ function AppMenu({
4211
4304
  children: "Install DocBlocks\u2026"
4212
4305
  }
4213
4306
  ),
4214
- (onDownloadAllWorkspaces || onKeepBrowserData) && /* @__PURE__ */ jsx8("div", { className: "db-app-menu-divider" }),
4307
+ (onDownloadAllWorkspaces || onRestoreBackup || onKeepBrowserData) && /* @__PURE__ */ jsx8("div", { className: "db-app-menu-divider" }),
4215
4308
  onDownloadAllWorkspaces && /* @__PURE__ */ jsx8(
4216
4309
  "button",
4217
4310
  {
@@ -4222,6 +4315,16 @@ function AppMenu({
4222
4315
  children: "Back up all workspaces"
4223
4316
  }
4224
4317
  ),
4318
+ onRestoreBackup && /* @__PURE__ */ jsx8(
4319
+ "button",
4320
+ {
4321
+ className: "db-app-menu-item",
4322
+ role: "menuitem",
4323
+ tabIndex: -1,
4324
+ onClick: () => handleAction(onRestoreBackup),
4325
+ children: "Restore backup\u2026"
4326
+ }
4327
+ ),
4225
4328
  onKeepBrowserData && /* @__PURE__ */ jsx8(
4226
4329
  "button",
4227
4330
  {
@@ -4473,6 +4576,59 @@ function AppMenu({
4473
4576
  ] });
4474
4577
  }
4475
4578
 
4579
+ // src/DocBlocksShell/restore-backup.ts
4580
+ import {
4581
+ readWorkspaceBackup,
4582
+ restoreWorkspaceBackup,
4583
+ WORKSPACE_BACKUP_LIMITS,
4584
+ saveWorkspace as saveWorkspace2,
4585
+ removeWorkspace
4586
+ } from "@bendyline/docblocks/workspace";
4587
+ async function restoreBrowserBackup(file) {
4588
+ if (file.size > 256 * 1024 * 1024) throw new Error("Backup ZIPs must be smaller than 256 MiB.");
4589
+ const { zipToContainer } = await import("@bendyline/squisq-formats/container");
4590
+ const container = await zipToContainer(await file.arrayBuffer(), WORKSPACE_BACKUP_LIMITS);
4591
+ const workspaces = await readWorkspaceBackup(
4592
+ container,
4593
+ file.name.replace(/\.zip$/i, "") || "Workspace"
4594
+ );
4595
+ const { IndexedDBFileSystemProvider, deleteIndexedDBWorkspaceData } = await import("@bendyline/docblocks/filesystem/indexeddb");
4596
+ const staged = [];
4597
+ try {
4598
+ for (const workspace of workspaces) {
4599
+ const descriptor = {
4600
+ id: `restored-${crypto.randomUUID()}`,
4601
+ name: `${workspace.name.slice(0, 1013)} (restored)`,
4602
+ type: "indexeddb",
4603
+ lastOpened: (/* @__PURE__ */ new Date()).toISOString()
4604
+ };
4605
+ const provider = new IndexedDBFileSystemProvider(descriptor.id, descriptor.name).v2;
4606
+ staged.push({ descriptor, provider });
4607
+ await restoreWorkspaceBackup(workspace, provider);
4608
+ }
4609
+ for (const { descriptor } of staged) await saveWorkspace2(descriptor);
4610
+ return staged.map(({ descriptor }) => descriptor);
4611
+ } catch (error) {
4612
+ const failures = [error];
4613
+ for (const { descriptor, provider } of staged) {
4614
+ try {
4615
+ await provider.dispose();
4616
+ await removeWorkspace(descriptor.id);
4617
+ await deleteIndexedDBWorkspaceData(descriptor.id);
4618
+ } catch (cleanupError) {
4619
+ failures.push(cleanupError);
4620
+ }
4621
+ }
4622
+ if (failures.length > 1)
4623
+ throw new Error(
4624
+ `Restore failed; temporary workspace cleanup also failed. ${failures.map((failure) => failure instanceof Error ? failure.message : String(failure)).join("; ")}`
4625
+ );
4626
+ throw error;
4627
+ } finally {
4628
+ for (const { provider } of staged) await provider.dispose();
4629
+ }
4630
+ }
4631
+
4476
4632
  // src/WorkspacePicker/WorkspaceSettingsButton.tsx
4477
4633
  import { useState as useState8, useCallback as useCallback9, useRef as useRef9, useEffect as useEffect9 } from "react";
4478
4634
  import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
@@ -4995,7 +5151,7 @@ function useDocumentSession(autoSaveDelayMs = 500) {
4995
5151
  import { lazy, Suspense } from "react";
4996
5152
  import { jsx as jsx12 } from "react/jsx-runtime";
4997
5153
  var ExportToolbarControlsImplementation = lazy(
4998
- () => import("./ExportToolbarControls-A2YBKUUD.js").then((module) => ({
5154
+ () => import("./ExportToolbarControls-HOV424TO.js").then((module) => ({
4999
5155
  default: module.ExportToolbarControls
5000
5156
  }))
5001
5157
  );
@@ -5565,7 +5721,7 @@ function resolveShellEditorLinkTarget(href, documentPath) {
5565
5721
  targetSegments.push(segment);
5566
5722
  }
5567
5723
  const path = targetSegments.join("/");
5568
- if (!path || path.length > HOST_WIRE_LIMITS2.pathCharacters || !isMarkdownFileName(path)) {
5724
+ if (!path || path.length > HOST_WIRE_LIMITS2.pathCharacters || !isMarkdownFileName(path) && !/\.html?$/i.test(path)) {
5569
5725
  return null;
5570
5726
  }
5571
5727
  return { kind: "workspace", path };
@@ -5588,10 +5744,36 @@ function hasControlCharacter(value) {
5588
5744
  return false;
5589
5745
  }
5590
5746
 
5591
- // src/DocBlocksShell/import-files.ts
5747
+ // src/DocBlocksShell/wiki-articles.ts
5592
5748
  import {
5749
+ ensureWikiArticle,
5750
+ wikiArticleExists,
5593
5751
  FsError as FsError4,
5594
- getFileSystemProviderV2 as getFileSystemProviderV27,
5752
+ getFileSystemProviderV2 as getFileSystemProviderV27
5753
+ } from "@bendyline/docblocks/filesystem";
5754
+ function wikiArticleExtension(format) {
5755
+ return newFileHtmlOutput(format) ? ".html" : ".md";
5756
+ }
5757
+ async function ensureShellWikiArticle(provider, path, format, render) {
5758
+ if (!/\.html?$/i.test(path)) return ensureWikiArticle(provider, path);
5759
+ if (await wikiArticleExists(provider, path)) return false;
5760
+ if (!getFileSystemProviderV27(provider)) {
5761
+ throw new FsError4("not-supported", "This workspace cannot create articles safely.");
5762
+ }
5763
+ try {
5764
+ await createNewOutsideInDocument(provider, path, newFileHtmlOutput(format), render);
5765
+ return true;
5766
+ } catch (error) {
5767
+ if (error instanceof FsError4 && (error.code === "already-exists" || error.code === "conflict") && await wikiArticleExists(provider, path))
5768
+ return false;
5769
+ throw error;
5770
+ }
5771
+ }
5772
+
5773
+ // src/DocBlocksShell/import-files.ts
5774
+ import {
5775
+ FsError as FsError5,
5776
+ getFileSystemProviderV2 as getFileSystemProviderV28,
5595
5777
  parseWorkspacePath as parseWorkspacePath6
5596
5778
  } from "@bendyline/docblocks/filesystem";
5597
5779
  var MAX_NAME_ATTEMPTS = 100;
@@ -5602,7 +5784,7 @@ function withCopySuffix(path, n) {
5602
5784
  return `${path.slice(0, dot)} (${n})${path.slice(dot)}`;
5603
5785
  }
5604
5786
  function isAlreadyExists(error) {
5605
- return error instanceof FsError4 && error.code === "already-exists";
5787
+ return error instanceof FsError5 && error.code === "already-exists";
5606
5788
  }
5607
5789
  async function claimAvailablePath(provider, desiredPath) {
5608
5790
  for (let attempt = 1; attempt <= MAX_NAME_ATTEMPTS; attempt++) {
@@ -5617,7 +5799,7 @@ async function claimAvailablePath(provider, desiredPath) {
5617
5799
  throw new Error(`Too many documents are already named like \u201C${desiredPath}\u201D.`);
5618
5800
  }
5619
5801
  async function writeProviderBytes(provider, path, data, mode) {
5620
- const providerV2 = getFileSystemProviderV27(provider);
5802
+ const providerV2 = getFileSystemProviderV28(provider);
5621
5803
  if (providerV2) {
5622
5804
  await providerV2.writeFile(parseWorkspacePath6(path), data, {
5623
5805
  mode,
@@ -5627,7 +5809,7 @@ async function writeProviderBytes(provider, path, data, mode) {
5627
5809
  return;
5628
5810
  }
5629
5811
  if (mode === "create" && await providerEntryExists(provider, path)) {
5630
- throw new FsError4("already-exists", "File already exists.", { operation: "write", path });
5812
+ throw new FsError5("already-exists", "File already exists.", { operation: "write", path });
5631
5813
  }
5632
5814
  await provider.writeBinary(path, data);
5633
5815
  }
@@ -5660,7 +5842,7 @@ function directoriesUnder(files, root) {
5660
5842
  return [...directories].sort((a, b) => b.split("/").length - a.split("/").length);
5661
5843
  }
5662
5844
  async function removeIfEmptyDirectory(provider, path) {
5663
- const providerV2 = getFileSystemProviderV27(provider);
5845
+ const providerV2 = getFileSystemProviderV28(provider);
5664
5846
  if (!providerV2) return;
5665
5847
  await providerV2.remove(parseWorkspacePath6(path), { recursive: false, missing: "ignore" }).catch(() => void 0);
5666
5848
  }
@@ -6038,7 +6220,7 @@ function resolveShellTheme({
6038
6220
  }
6039
6221
 
6040
6222
  // src/DocBlocksShell/last-state.ts
6041
- import { tryParseWorkspacePath as tryParseWorkspacePath2 } from "@bendyline/docblocks/filesystem";
6223
+ import { tryParseWorkspacePath as tryParseWorkspacePath3 } from "@bendyline/docblocks/filesystem";
6042
6224
  var LAST_STATE_KEY = "docblocks:lastState";
6043
6225
  var MAX_WORKSPACE_ID_CHARACTERS = 256;
6044
6226
  var MAX_WORKSPACE_PATH_CHARACTERS = 4096;
@@ -6059,7 +6241,7 @@ function parseLastState(value) {
6059
6241
  if (!hasExactKeys(record, ["workspaceId", "filePath", "view"])) return null;
6060
6242
  if (!isBoundedText(record.workspaceId, MAX_WORKSPACE_ID_CHARACTERS)) return null;
6061
6243
  if (!isBoundedText(record.filePath, MAX_WORKSPACE_PATH_CHARACTERS)) return null;
6062
- const parsedPath = tryParseWorkspacePath2(record.filePath);
6244
+ const parsedPath = tryParseWorkspacePath3(record.filePath);
6063
6245
  if (!parsedPath) return null;
6064
6246
  if (typeof record.view !== "string" || !EDITOR_VIEWS.has(record.view)) return null;
6065
6247
  return {
@@ -6853,7 +7035,7 @@ function useDocumentLinkProvider(index, selectedFile, ensureIndex) {
6853
7035
 
6854
7036
  // src/WorkspaceIndex/useWorkspaceIndex.ts
6855
7037
  import { useCallback as useCallback17, useEffect as useEffect28, useMemo as useMemo5, useRef as useRef18, useState as useState22, useSyncExternalStore as useSyncExternalStore4 } from "react";
6856
- import { getFileSystemProviderV2 as getFileSystemProviderV28 } from "@bendyline/docblocks/filesystem";
7038
+ import { getFileSystemProviderV2 as getFileSystemProviderV29 } from "@bendyline/docblocks/filesystem";
6857
7039
  import { maybeGetDocBlocksHost as maybeGetDocBlocksHost3 } from "@bendyline/docblocks/host";
6858
7040
  var IDLE_WORKSPACE_INDEX_STATUS = Object.freeze({
6859
7041
  phase: "idle",
@@ -6883,7 +7065,7 @@ function whenIdle(callback) {
6883
7065
  return () => clearTimeout(handle);
6884
7066
  }
6885
7067
  function useWorkspaceIndex(provider, workspaceId, mode = "eager") {
6886
- const providerV2 = provider && mode !== "off" ? getFileSystemProviderV28(provider) : null;
7068
+ const providerV2 = provider && mode !== "off" ? getFileSystemProviderV29(provider) : null;
6887
7069
  const [index, setIndex] = useState22(null);
6888
7070
  const startRequestedRef = useRef18(false);
6889
7071
  useEffect28(() => {
@@ -6946,7 +7128,7 @@ function useWorkspaceIndex(provider, workspaceId, mode = "eager") {
6946
7128
  }
6947
7129
 
6948
7130
  // src/WorkspaceIndex/recent-documents.ts
6949
- import { tryParseWorkspacePath as tryParseWorkspacePath3 } from "@bendyline/docblocks/filesystem";
7131
+ import { tryParseWorkspacePath as tryParseWorkspacePath4 } from "@bendyline/docblocks/filesystem";
6950
7132
  var RECENT_DOCUMENTS_STORAGE_KEY = "docblocks:recentDocuments:v1";
6951
7133
  var MAX_PATHS_PER_WORKSPACE = 50;
6952
7134
  var MAX_WORKSPACES = 50;
@@ -6965,7 +7147,7 @@ function defaultStorage() {
6965
7147
  }
6966
7148
  function canonicalPath(path) {
6967
7149
  if (path.length === 0 || path.length > MAX_PATH_LENGTH) return null;
6968
- return tryParseWorkspacePath3(path) || null;
7150
+ return tryParseWorkspacePath4(path) || null;
6969
7151
  }
6970
7152
  function parseRecentDocuments(value) {
6971
7153
  const parsed = /* @__PURE__ */ new Map();
@@ -7071,14 +7253,14 @@ var WorkspaceAuthorityBarrier = class {
7071
7253
 
7072
7254
  // src/DocBlocksShell/transient-workspace-move.ts
7073
7255
  import {
7074
- getFileSystemProviderV2 as getFileSystemProviderV29
7256
+ getFileSystemProviderV2 as getFileSystemProviderV210
7075
7257
  } from "@bendyline/docblocks/filesystem";
7076
7258
  function pathDepth(path) {
7077
7259
  return path ? path.split("/").length : 0;
7078
7260
  }
7079
7261
  async function copyTransientWorkspaceContents(source, destination) {
7080
- const sourceV2 = getFileSystemProviderV29(source);
7081
- const destinationV2 = getFileSystemProviderV29(destination);
7262
+ const sourceV2 = getFileSystemProviderV210(source);
7263
+ const destinationV2 = getFileSystemProviderV210(destination);
7082
7264
  if (!sourceV2 || !destinationV2) {
7083
7265
  throw new Error("Moving a temporary document requires a current workspace provider.");
7084
7266
  }
@@ -7243,13 +7425,13 @@ function loadEditorShell() {
7243
7425
  return editorShellModulePromise;
7244
7426
  }
7245
7427
  var EditorShell = lazy2(loadEditorShell);
7246
- var DocumentConflictDialog = lazy2(() => import("./DocumentConflictDialog-5LRVIGXS.js"));
7247
- var GitUI = lazy2(() => import("./GitUI-RCWUZVS6.js").then((m) => ({ default: m.GitUI })));
7428
+ var DocumentConflictDialog = lazy2(() => import("./DocumentConflictDialog-DZ2BV2IH.js"));
7429
+ var GitUI = lazy2(() => import("./GitUI-F3CNBURE.js").then((m) => ({ default: m.GitUI })));
7248
7430
  var QuickOpenDialog = lazy2(
7249
- () => import("./QuickOpenDialog-2KK4SEBU.js").then((m) => ({ default: m.QuickOpenDialog }))
7431
+ () => import("./QuickOpenDialog-ENVFGIHS.js").then((m) => ({ default: m.QuickOpenDialog }))
7250
7432
  );
7251
7433
  var KeyboardShortcutsDialog = lazy2(
7252
- () => import("./KeyboardShortcutsDialog-G5OBDTNR.js").then((m) => ({ default: m.KeyboardShortcutsDialog }))
7434
+ () => import("./KeyboardShortcutsDialog-HVKDFP5Z.js").then((m) => ({ default: m.KeyboardShortcutsDialog }))
7253
7435
  );
7254
7436
  var WorkspaceSearchPanel = lazy2(
7255
7437
  () => import("./WorkspaceSearchPanel-J6BKEZQJ.js").then((m) => ({
@@ -7263,37 +7445,37 @@ var GitToolbarControl = lazy2(
7263
7445
  () => import("./GitToolbarControl-B7KG4MFP.js").then((m) => ({ default: m.GitToolbarControl }))
7264
7446
  );
7265
7447
  var AiToolbarControl = lazy2(
7266
- () => import("./AiEditorAssistant-YLJ5OZK2.js").then((module) => ({
7448
+ () => import("./AiEditorAssistant-4HFFGVU4.js").then((module) => ({
7267
7449
  default: module.AiToolbarControl
7268
7450
  }))
7269
7451
  );
7270
7452
  var PresentationMaker = lazy2(
7271
- () => import("./PresentationMaker-P2JP6UUX.js").then((module) => ({
7453
+ () => import("./PresentationMaker-MUMFAXEI.js").then((module) => ({
7272
7454
  default: module.PresentationMaker
7273
7455
  }))
7274
7456
  );
7275
7457
  var AiReviewPanel = lazy2(
7276
- () => import("./AiEditorAssistant-YLJ5OZK2.js").then((module) => ({
7458
+ () => import("./AiEditorAssistant-4HFFGVU4.js").then((module) => ({
7277
7459
  default: module.AiReviewPanel
7278
7460
  }))
7279
7461
  );
7280
7462
  var AiIllustratePanel = lazy2(
7281
- () => import("./AiDiagrams-QE3LVYFK.js").then((module) => ({
7463
+ () => import("./AiDiagrams-QZMJNGDT.js").then((module) => ({
7282
7464
  default: module.AiIllustratePanel
7283
7465
  }))
7284
7466
  );
7285
7467
  var TextFromNarration = lazy2(
7286
- () => import("./TextFromNarration-NITJQ4IB.js").then((module) => ({
7468
+ () => import("./TextFromNarration-EYOMW2IW.js").then((module) => ({
7287
7469
  default: module.TextFromNarration
7288
7470
  }))
7289
7471
  );
7290
7472
  var SpeechToolbarControl = lazy2(
7291
- () => import("./SpeechToolbarControl-CLU7RB2G.js").then((module) => ({
7473
+ () => import("./SpeechToolbarControl-KNI2C4JJ.js").then((module) => ({
7292
7474
  default: module.SpeechToolbarControl
7293
7475
  }))
7294
7476
  );
7295
7477
  var SpeechMenuBridge = lazy2(
7296
- () => import("./SpeechToolbarControl-CLU7RB2G.js").then((module) => ({
7478
+ () => import("./SpeechToolbarControl-KNI2C4JJ.js").then((module) => ({
7297
7479
  default: module.SpeechMenuBridge
7298
7480
  }))
7299
7481
  );
@@ -7434,19 +7616,19 @@ function pathContains(parentPath2, candidatePath) {
7434
7616
  return workspacePathContains(parseWorkspacePath8(parentPath2), parseWorkspacePath8(candidatePath));
7435
7617
  }
7436
7618
  async function readProviderDirectory2(provider, path) {
7437
- const providerV2 = getFileSystemProviderV210(provider);
7619
+ const providerV2 = getFileSystemProviderV211(provider);
7438
7620
  if (!providerV2) return provider.readDirectory(path);
7439
7621
  const entries = await providerV2.readDirectory(parseWorkspacePath8(path));
7440
7622
  return entries.map((entry) => ({ kind: entry.kind, name: entry.name, path: entry.path }));
7441
7623
  }
7442
7624
  async function readProviderText(provider, path) {
7443
- const providerV2 = getFileSystemProviderV210(provider);
7625
+ const providerV2 = getFileSystemProviderV211(provider);
7444
7626
  if (!providerV2) return provider.readFile(path);
7445
7627
  const file = await providerV2.readFile(parseWorkspacePath8(path));
7446
7628
  return file ? decodeUtf8Text(file.data, { label: "The document", path: parseWorkspacePath8(path) }) : null;
7447
7629
  }
7448
7630
  async function pinnedProviderFileExists(provider, path) {
7449
- const providerV2 = getFileSystemProviderV210(provider);
7631
+ const providerV2 = getFileSystemProviderV211(provider);
7450
7632
  if (!providerV2) return provider.exists(path);
7451
7633
  const entry = await providerV2.stat(parseWorkspacePath8(path));
7452
7634
  if (!entry) return false;
@@ -7454,7 +7636,7 @@ async function pinnedProviderFileExists(provider, path) {
7454
7636
  return true;
7455
7637
  }
7456
7638
  async function removePinnedProviderFile(provider, path) {
7457
- const providerV2 = getFileSystemProviderV210(provider);
7639
+ const providerV2 = getFileSystemProviderV211(provider);
7458
7640
  if (providerV2) {
7459
7641
  const canonical = parseWorkspacePath8(path);
7460
7642
  const entry = await providerV2.stat(canonical);
@@ -7472,7 +7654,7 @@ async function removePinnedProviderFile(provider, path) {
7472
7654
  return true;
7473
7655
  }
7474
7656
  async function readStableFileSnapshot(provider, path) {
7475
- const providerV2 = getFileSystemProviderV210(provider);
7657
+ const providerV2 = getFileSystemProviderV211(provider);
7476
7658
  if (providerV2) {
7477
7659
  const read2 = await providerV2.readFile(parseWorkspacePath8(path));
7478
7660
  return {
@@ -7516,7 +7698,7 @@ async function ensureHandleWritePermission(handle) {
7516
7698
  }
7517
7699
  }
7518
7700
  async function copyProviderToContainer(src, container, pathPrefix) {
7519
- const providerV2 = getFileSystemProviderV210(src);
7701
+ const providerV2 = getFileSystemProviderV211(src);
7520
7702
  if (providerV2) {
7521
7703
  const snapshot = await providerV2.snapshot();
7522
7704
  for (const entry of snapshot.entries) {
@@ -7859,7 +8041,7 @@ function DocBlocksShell({
7859
8041
  const effectiveProofingIgnoreStore = proofingIgnoreStore === void 0 ? defaultProofingIgnoreStore : proofingIgnoreStore;
7860
8042
  useEffect29(() => {
7861
8043
  if (!provider || getTransientWorkspace(provider.id)) return;
7862
- const providerV2 = getFileSystemProviderV210(provider);
8044
+ const providerV2 = getFileSystemProviderV211(provider);
7863
8045
  return providerV2 ? retainFileSystemProvider(providerV2) : void 0;
7864
8046
  }, [provider]);
7865
8047
  const [descriptorRefreshKey, setDescriptorRefreshKey] = useState23(0);
@@ -7947,7 +8129,7 @@ function DocBlocksShell({
7947
8129
  void scan();
7948
8130
  const scanOnFocus = () => void scan();
7949
8131
  window.addEventListener("focus", scanOnFocus);
7950
- const providerV2 = getFileSystemProviderV210(provider);
8132
+ const providerV2 = getFileSystemProviderV211(provider);
7951
8133
  const subscription = providerV2?.capabilities.watch ? providerV2.watch(
7952
8134
  (event) => {
7953
8135
  if (event.type !== "modified") void scan();
@@ -8056,7 +8238,7 @@ function DocBlocksShell({
8056
8238
  if (descriptor && descriptor.versioningOverride !== void 0) {
8057
8239
  const moved = { ...descriptor };
8058
8240
  delete moved.versioningOverride;
8059
- await saveWorkspace2(moved);
8241
+ await saveWorkspace3(moved);
8060
8242
  setDescriptorRefreshKey((k) => k + 1);
8061
8243
  }
8062
8244
  },
@@ -8153,7 +8335,13 @@ function DocBlocksShell({
8153
8335
  return {
8154
8336
  resolveTarget: (filename) => resolveTarget(documentId, filename),
8155
8337
  pickTarget: (filename, currentTarget) => pickTarget(documentId, filename, currentTarget?.grantId ?? null),
8156
- saveBlob: (blob, filename, target) => saveBlobToHost(exports, documentId, blob, filename, target?.grantId ?? null)
8338
+ saveBlob: (blob, filename, target) => saveBlobToHost(exports, documentId, blob, filename, target?.grantId ?? null),
8339
+ ...hostSupports2("openSavedExports") && exports.openSaved ? {
8340
+ openSaved: async (target) => {
8341
+ if (!target.grantId) throw new Error("The saved export has no native target.");
8342
+ await exports.openSaved(documentId, target.grantId);
8343
+ }
8344
+ } : {}
8157
8345
  };
8158
8346
  }
8159
8347
  }
@@ -8212,7 +8400,7 @@ function DocBlocksShell({
8212
8400
  const workspaceOutputsResult = workspaceOutputs.status.result;
8213
8401
  useEffect29(() => {
8214
8402
  if (!workspaceOutputsResult?.outputs.some((output) => output.created)) return;
8215
- if (provider && getFileSystemProviderV210(provider)?.capabilities.watch) return;
8403
+ if (provider && getFileSystemProviderV211(provider)?.capabilities.watch) return;
8216
8404
  setExplorerKey((key) => key + 1);
8217
8405
  }, [provider, workspaceOutputsResult]);
8218
8406
  const [fileExplorerSortMode, setFileExplorerSortMode] = useState23(loadFileExplorerSortMode);
@@ -8515,7 +8703,7 @@ function DocBlocksShell({
8515
8703
  }
8516
8704
  return fsProvider;
8517
8705
  } finally {
8518
- if (!transient && !adopted) await getFileSystemProviderV210(fsProvider)?.dispose();
8706
+ if (!transient && !adopted) await getFileSystemProviderV211(fsProvider)?.dispose();
8519
8707
  }
8520
8708
  },
8521
8709
  [adoptSelectedDocument, createDocumentTarget, documentSession, pushHash]
@@ -8567,7 +8755,7 @@ function DocBlocksShell({
8567
8755
  )
8568
8756
  });
8569
8757
  } catch (error) {
8570
- await getFileSystemProviderV210(mem)?.dispose();
8758
+ await getFileSystemProviderV211(mem)?.dispose();
8571
8759
  throw error;
8572
8760
  }
8573
8761
  return adoptTransientWorkspace({
@@ -8613,7 +8801,7 @@ function DocBlocksShell({
8613
8801
  try {
8614
8802
  await writeProviderText(fs, welcomePath, welcomeContent, "create");
8615
8803
  } catch (error) {
8616
- if (!(error instanceof FsError5 && error.code === "already-exists")) throw error;
8804
+ if (!(error instanceof FsError6 && error.code === "already-exists")) throw error;
8617
8805
  const existing = await readProviderText(fs, welcomePath);
8618
8806
  if (existing === null) throw error;
8619
8807
  seededContent = existing;
@@ -8669,8 +8857,8 @@ function DocBlocksShell({
8669
8857
  getDocBlocksHost().env.platform,
8670
8858
  (/* @__PURE__ */ new Date()).toISOString()
8671
8859
  );
8672
- for (const id of reconciliation.removeIds) await removeWorkspace(id);
8673
- for (const workspace of reconciliation.upsert) await saveWorkspace2(workspace);
8860
+ for (const id of reconciliation.removeIds) await removeWorkspace2(id);
8861
+ for (const workspace of reconciliation.upsert) await saveWorkspace3(workspace);
8674
8862
  workspaceIdRemap = reconciliation.idRemap;
8675
8863
  for (const workspace of hostWorkspaces) {
8676
8864
  if (workspace.isDefault === true) defaultHostWorkspaceIds.add(workspace.id);
@@ -8763,12 +8951,12 @@ function DocBlocksShell({
8763
8951
  const p = await createHostProviderFromWorkspace(ws);
8764
8952
  if (!p) continue;
8765
8953
  if (!isCurrent()) {
8766
- await getFileSystemProviderV210(p)?.dispose();
8954
+ await getFileSystemProviderV211(p)?.dispose();
8767
8955
  return;
8768
8956
  }
8769
8957
  await touchWorkspace2(ws.id);
8770
8958
  if (!isCurrent()) {
8771
- await getFileSystemProviderV210(p)?.dispose();
8959
+ await getFileSystemProviderV211(p)?.dispose();
8772
8960
  return;
8773
8961
  }
8774
8962
  fsProvider = p;
@@ -8777,12 +8965,12 @@ function DocBlocksShell({
8777
8965
  const restored = await (await loadNativeFileSystem()).restoreNativeFolder(ws.id);
8778
8966
  if (restored) {
8779
8967
  if (!isCurrent()) {
8780
- await getFileSystemProviderV210(restored)?.dispose();
8968
+ await getFileSystemProviderV211(restored)?.dispose();
8781
8969
  return;
8782
8970
  }
8783
8971
  await touchWorkspace2(ws.id);
8784
8972
  if (!isCurrent()) {
8785
- await getFileSystemProviderV210(restored)?.dispose();
8973
+ await getFileSystemProviderV211(restored)?.dispose();
8786
8974
  return;
8787
8975
  }
8788
8976
  fsProvider = restored;
@@ -8791,12 +8979,12 @@ function DocBlocksShell({
8791
8979
  } else {
8792
8980
  const p = await createIndexedDbFileSystemProvider(ws.id, ws.name);
8793
8981
  if (!isCurrent()) {
8794
- await getFileSystemProviderV210(p)?.dispose();
8982
+ await getFileSystemProviderV211(p)?.dispose();
8795
8983
  return;
8796
8984
  }
8797
8985
  await touchWorkspace2(ws.id);
8798
8986
  if (!isCurrent()) {
8799
- await getFileSystemProviderV210(p)?.dispose();
8987
+ await getFileSystemProviderV211(p)?.dispose();
8800
8988
  return;
8801
8989
  }
8802
8990
  fsProvider = p;
@@ -8814,11 +9002,11 @@ function DocBlocksShell({
8814
9002
  rootPath: info.rootPath,
8815
9003
  lastOpened: (/* @__PURE__ */ new Date()).toISOString()
8816
9004
  };
8817
- await saveWorkspace2(descriptor);
9005
+ await saveWorkspace3(descriptor);
8818
9006
  if (!isCurrent()) return;
8819
9007
  const p = await createHostFileSystemProvider(info.id, info.name, info.rootPath);
8820
9008
  if (!isCurrent()) {
8821
- await getFileSystemProviderV210(p)?.dispose();
9009
+ await getFileSystemProviderV211(p)?.dispose();
8822
9010
  return;
8823
9011
  }
8824
9012
  fsProvider = p;
@@ -8827,7 +9015,7 @@ function DocBlocksShell({
8827
9015
  if (!isCurrent()) return;
8828
9016
  const p = await createIndexedDbFileSystemProvider(defaultWs.id, defaultWs.name);
8829
9017
  if (!isCurrent()) {
8830
- await getFileSystemProviderV210(p)?.dispose();
9018
+ await getFileSystemProviderV211(p)?.dispose();
8831
9019
  return;
8832
9020
  }
8833
9021
  fsProvider = p;
@@ -8848,7 +9036,7 @@ function DocBlocksShell({
8848
9036
  setActiveWorkspaceId(fsProvider.id);
8849
9037
  adopted = true;
8850
9038
  } finally {
8851
- if (!adopted) await getFileSystemProviderV210(fsProvider)?.dispose();
9039
+ if (!adopted) await getFileSystemProviderV211(fsProvider)?.dispose();
8852
9040
  }
8853
9041
  })().catch(() => {
8854
9042
  if (isCurrent()) {
@@ -9101,7 +9289,7 @@ function DocBlocksShell({
9101
9289
  }, [selectedFile, selectedManagedOutput, workspaceOutputsResult]);
9102
9290
  useEffect29(() => {
9103
9291
  if (!provider) return;
9104
- const providerV2 = getFileSystemProviderV210(provider);
9292
+ const providerV2 = getFileSystemProviderV211(provider);
9105
9293
  const lifecycle = maybeGetDocBlocksHost4()?.lifecycle;
9106
9294
  if (!providerV2 || !providerV2.capabilities.watch && !lifecycle?.onResume && !managedOutputRefreshToken)
9107
9295
  return;
@@ -9499,7 +9687,7 @@ function DocBlocksShell({
9499
9687
  } finally {
9500
9688
  if (nextProvider && ownsNextProvider && !adoptedNextProvider) {
9501
9689
  try {
9502
- await getFileSystemProviderV210(nextProvider)?.dispose();
9690
+ await getFileSystemProviderV211(nextProvider)?.dispose();
9503
9691
  } catch {
9504
9692
  }
9505
9693
  }
@@ -9605,14 +9793,14 @@ function DocBlocksShell({
9605
9793
  }
9606
9794
  }
9607
9795
  try {
9608
- await removeWorkspace(activeWorkspaceId);
9796
+ await removeWorkspace2(activeWorkspaceId);
9609
9797
  } catch {
9610
9798
  }
9611
9799
  setDescriptorRefreshKey((key) => key + 1);
9612
9800
  } catch (error) {
9613
9801
  if (destinationProvider && !adoptedDestination) {
9614
9802
  try {
9615
- await getFileSystemProviderV210(destinationProvider)?.dispose();
9803
+ await getFileSystemProviderV211(destinationProvider)?.dispose();
9616
9804
  } catch {
9617
9805
  }
9618
9806
  }
@@ -9647,7 +9835,7 @@ function DocBlocksShell({
9647
9835
  rootPath: info.rootPath,
9648
9836
  lastOpened: (/* @__PURE__ */ new Date()).toISOString()
9649
9837
  };
9650
- await saveWorkspace2(descriptor2);
9838
+ await saveWorkspace3(descriptor2);
9651
9839
  if (requestId !== navigationRequestRef.current) return;
9652
9840
  const provider2 = await createHostFileSystemProvider(info.id, info.name, info.rootPath);
9653
9841
  if (!await transitionAwayFromDocument(requestId)) return;
@@ -9667,7 +9855,7 @@ function DocBlocksShell({
9667
9855
  type: "native",
9668
9856
  lastOpened: (/* @__PURE__ */ new Date()).toISOString()
9669
9857
  };
9670
- await saveWorkspace2(descriptor);
9858
+ await saveWorkspace3(descriptor);
9671
9859
  if (requestId !== navigationRequestRef.current) return;
9672
9860
  if (!await transitionAwayFromDocument(requestId)) return;
9673
9861
  setProvider(nativeProvider);
@@ -9690,7 +9878,7 @@ function DocBlocksShell({
9690
9878
  rootPath: info.rootPath,
9691
9879
  lastOpened: (/* @__PURE__ */ new Date()).toISOString()
9692
9880
  };
9693
- await saveWorkspace2(descriptor);
9881
+ await saveWorkspace3(descriptor);
9694
9882
  if (requestId !== navigationRequestRef.current) return;
9695
9883
  const cloneProvider = await createHostFileSystemProvider(info.id, info.name, info.rootPath);
9696
9884
  if (!await transitionAwayFromDocument(requestId)) return;
@@ -9718,12 +9906,12 @@ function DocBlocksShell({
9718
9906
  if (requestId !== navigationRequestRef.current) return null;
9719
9907
  if (format === "markdown") {
9720
9908
  const content = "# " + filename.replace(/\.[^.]+$/, "") + "\n";
9721
- const providerV2 = getFileSystemProviderV210(provider);
9909
+ const providerV2 = getFileSystemProviderV211(provider);
9722
9910
  if (providerV2) {
9723
9911
  try {
9724
9912
  await writeProviderText(provider, path, content, "create");
9725
9913
  } catch (error) {
9726
- if (error instanceof FsError5 && error.code === "already-exists") {
9914
+ if (error instanceof FsError6 && error.code === "already-exists") {
9727
9915
  throw new Error("A document with that name already exists.");
9728
9916
  }
9729
9917
  throw error;
@@ -9822,7 +10010,7 @@ function DocBlocksShell({
9822
10010
  }
9823
10011
  const path = `/${folderName}`;
9824
10012
  try {
9825
- const providerV2 = getFileSystemProviderV210(provider);
10013
+ const providerV2 = getFileSystemProviderV211(provider);
9826
10014
  if (providerV2) {
9827
10015
  await providerV2.createDirectory(parseWorkspacePath8(path), { mode: "create" });
9828
10016
  } else {
@@ -9832,7 +10020,7 @@ function DocBlocksShell({
9832
10020
  await provider.createDirectory(path);
9833
10021
  }
9834
10022
  } catch (error) {
9835
- const message = error instanceof FsError5 && error.code === "already-exists" ? "A file or folder with that name already exists." : error instanceof Error ? error.message : "Could not create the folder.";
10023
+ const message = error instanceof FsError6 && error.code === "already-exists" ? "A file or folder with that name already exists." : error instanceof Error ? error.message : "Could not create the folder.";
9836
10024
  showToast("error", message);
9837
10025
  return;
9838
10026
  }
@@ -10058,7 +10246,7 @@ function DocBlocksShell({
10058
10246
  });
10059
10247
  }, [handleNewFile, handleOpenFolder, handleRevealWorkspace, acknowledge, openFind]);
10060
10248
  const handleSelect = useCallback18(
10061
- async (path, kind) => {
10249
+ async (path, kind, createWikiArticle = false) => {
10062
10250
  if (!provider || !activeWorkspaceId) return;
10063
10251
  const requestId = ++navigationRequestRef.current;
10064
10252
  if (kind === "directory") {
@@ -10075,6 +10263,17 @@ function DocBlocksShell({
10075
10263
  try {
10076
10264
  const transitioned = await documentSession.transitionWithLoad(async () => {
10077
10265
  if (requestId !== navigationRequestRef.current) return null;
10266
+ if (createWikiArticle) {
10267
+ if (await ensureShellWikiArticle(
10268
+ provider,
10269
+ parseWorkspacePath8(path),
10270
+ defaultNewFileFormat,
10271
+ outsideInRenderRef.current
10272
+ )) {
10273
+ notifyWorkspaceChanged(activeWorkspaceId, path);
10274
+ }
10275
+ if (requestId !== navigationRequestRef.current) return null;
10276
+ }
10078
10277
  openedDocument = await loadEditableShellDocument(
10079
10278
  provider,
10080
10279
  path,
@@ -10126,9 +10325,72 @@ function DocBlocksShell({
10126
10325
  documentSession,
10127
10326
  notifyWorkspaceChanged,
10128
10327
  transitionAwayFromDocument,
10129
- showToast
10328
+ showToast,
10329
+ defaultNewFileFormat
10130
10330
  ]
10131
10331
  );
10332
+ const wikiNavigationRef = useRef19(handleSelect);
10333
+ wikiNavigationRef.current = handleSelect;
10334
+ const wikiLinks = useMemo6(() => {
10335
+ if (!provider || !selectedFile || !activeWorkspaceId) return null;
10336
+ const source = selectedFile;
10337
+ const extension = wikiArticleExtension(defaultNewFileFormat);
10338
+ const lookups = /* @__PURE__ */ new Map();
10339
+ let queue = Promise.resolve();
10340
+ const lookup = (target) => {
10341
+ let result = lookups.get(target);
10342
+ if (!result) {
10343
+ result = queue.then(() => findWikiArticle2(provider, source, target, extension));
10344
+ queue = result.then(
10345
+ () => void 0,
10346
+ () => void 0
10347
+ );
10348
+ lookups.set(target, result);
10349
+ }
10350
+ return result;
10351
+ };
10352
+ return {
10353
+ async suggest(query) {
10354
+ const index = workspaceIndex.index;
10355
+ if (!index) return [];
10356
+ workspaceIndex.ensure();
10357
+ await index.whenListed({ timeoutMs: 5e3 });
10358
+ return suggestWikiArticles(
10359
+ index.getFiles(),
10360
+ index.getDirectories(),
10361
+ source,
10362
+ query,
10363
+ extension
10364
+ );
10365
+ },
10366
+ async exists(target) {
10367
+ return (await lookup(target)).exists;
10368
+ },
10369
+ async resolve(target) {
10370
+ const article = await lookup(target);
10371
+ if (!article.exists) return null;
10372
+ const href = selectedSourceFile !== selectedFile ? `/${article.path}` : article.relativePath;
10373
+ return href.split("/").map(encodeURIComponent).join("/");
10374
+ },
10375
+ async open(target, { create }) {
10376
+ const requestId = navigationRequestRef.current;
10377
+ const navigate = wikiNavigationRef.current;
10378
+ const { path } = await lookup(target);
10379
+ if (requestId !== navigationRequestRef.current || navigate !== wikiNavigationRef.current)
10380
+ return;
10381
+ await navigate(path, "file", create);
10382
+ }
10383
+ };
10384
+ }, [
10385
+ provider,
10386
+ selectedFile,
10387
+ selectedSourceFile,
10388
+ activeWorkspaceId,
10389
+ defaultNewFileFormat,
10390
+ workspaceIndex.index,
10391
+ workspaceIndex.ensure,
10392
+ workspaceIndex.status.generation
10393
+ ]);
10132
10394
  const handleEnableOutsideInEditing = useCallback18(
10133
10395
  async (path) => {
10134
10396
  if (!provider || !activeWorkspaceId) {
@@ -10256,7 +10518,7 @@ function DocBlocksShell({
10256
10518
  if (!target) {
10257
10519
  showToast(
10258
10520
  "error",
10259
- "DocBlocks only opens HTTP(S) links or Markdown files inside this workspace."
10521
+ "DocBlocks only opens HTTP(S) links or Markdown and HTML files inside this workspace."
10260
10522
  );
10261
10523
  return true;
10262
10524
  }
@@ -10274,14 +10536,14 @@ function DocBlocksShell({
10274
10536
  if (!provider) return;
10275
10537
  try {
10276
10538
  if (!await providerEntryExists(provider, target.path)) {
10277
- showToast("error", "The linked Markdown file was not found in this workspace.");
10539
+ showToast("error", "The linked document was not found in this workspace.");
10278
10540
  return;
10279
10541
  }
10280
10542
  await handleSelect(target.path, "file");
10281
10543
  } catch (error) {
10282
10544
  showToast(
10283
10545
  "error",
10284
- error instanceof Error ? error.message : "Could not open the linked Markdown file."
10546
+ error instanceof Error ? error.message : "Could not open the linked document."
10285
10547
  );
10286
10548
  }
10287
10549
  })();
@@ -10555,7 +10817,7 @@ function DocBlocksShell({
10555
10817
  } finally {
10556
10818
  if (access.owned) {
10557
10819
  try {
10558
- await getFileSystemProviderV210(access.provider)?.dispose();
10820
+ await getFileSystemProviderV211(access.provider)?.dispose();
10559
10821
  } catch {
10560
10822
  }
10561
10823
  }
@@ -10624,7 +10886,7 @@ function DocBlocksShell({
10624
10886
  } finally {
10625
10887
  if (access.owned) {
10626
10888
  try {
10627
- await getFileSystemProviderV210(access.provider)?.dispose();
10889
+ await getFileSystemProviderV211(access.provider)?.dispose();
10628
10890
  } catch {
10629
10891
  }
10630
10892
  }
@@ -10642,32 +10904,73 @@ function DocBlocksShell({
10642
10904
  showToast
10643
10905
  ]
10644
10906
  );
10907
+ const restoreInputRef = useRef19(null);
10908
+ const restoreInProgress = useRef19(false);
10909
+ const handleRestoreBackup = useCallback18(
10910
+ async (file) => {
10911
+ if (restoreInProgress.current) {
10912
+ showToast("error", "A backup is already being restored. Please wait.");
10913
+ return;
10914
+ }
10915
+ restoreInProgress.current = true;
10916
+ try {
10917
+ await documentSession.flush("backup");
10918
+ const restored = await restoreBrowserBackup(file);
10919
+ setDescriptorRefreshKey((key) => key + 1);
10920
+ if (restored[0]) await handleWorkspaceSelect(restored[0]);
10921
+ showToast(
10922
+ "success",
10923
+ `Restored ${restored.length} workspace(s) as new copies. Your existing workspaces are unchanged.`
10924
+ );
10925
+ } catch (error) {
10926
+ showToast(
10927
+ "error",
10928
+ `Could not restore ${file.name}: ${error instanceof Error ? error.message : "Unknown error"}`
10929
+ );
10930
+ } finally {
10931
+ restoreInProgress.current = false;
10932
+ }
10933
+ },
10934
+ [documentSession, handleWorkspaceSelect, showToast]
10935
+ );
10936
+ const importOrRestore = useCallback18(
10937
+ async (files) => {
10938
+ const backups = hostSupports2("nativeWorkspaces") ? [] : files.filter((file) => /\.zip$/i.test(file.name));
10939
+ const documents = files.filter((file) => !backups.includes(file));
10940
+ const result = provider ? await importDroppedFiles(documents, provider) : null;
10941
+ for (const file of backups) await handleRestoreBackup(file);
10942
+ return result;
10943
+ },
10944
+ [handleRestoreBackup, provider]
10945
+ );
10645
10946
  const handleImportFiles = useCallback18(
10646
10947
  async (files) => {
10647
10948
  if (!provider) return;
10648
- const result = await importDroppedFiles(files, provider);
10949
+ const result = await importOrRestore(files);
10950
+ if (!result) return;
10649
10951
  notifyWorkspaceChanged(activeWorkspaceId);
10650
10952
  setExplorerKey((k) => k + 1);
10651
10953
  const summary = summariseImport(result);
10652
10954
  if (summary) showToast(summary.kind, summary.message);
10653
10955
  },
10654
- [activeWorkspaceId, notifyWorkspaceChanged, provider, showToast]
10956
+ [activeWorkspaceId, notifyWorkspaceChanged, provider, showToast, importOrRestore]
10655
10957
  );
10656
10958
  const handleImportFilesToEditor = useCallback18(
10657
10959
  async (files) => {
10658
10960
  if (!provider) return;
10659
- const result = await importDroppedFiles(files, provider);
10961
+ const result = await importOrRestore(files);
10962
+ if (!result) return;
10660
10963
  notifyWorkspaceChanged(activeWorkspaceId);
10661
10964
  setExplorerKey((k) => k + 1);
10662
10965
  const summary = summariseImport(result);
10663
10966
  if (summary) showToast(summary.kind, summary.message);
10664
10967
  const imported = result.imported;
10665
- if (imported.length === 1 && result.failed.length === 0) {
10968
+ if (imported.length === 1 && result.failed.length === 0 && !files.some((file) => /\.zip$/i.test(file.name))) {
10666
10969
  const only = imported[0];
10667
10970
  if (only) void handleSelect(only.path, "file");
10668
10971
  }
10669
10972
  },
10670
- [activeWorkspaceId, notifyWorkspaceChanged, provider, showToast, handleSelect]
10973
+ [activeWorkspaceId, notifyWorkspaceChanged, provider, showToast, handleSelect, importOrRestore]
10671
10974
  );
10672
10975
  const editorDropActive = useEditorFileDrop(
10673
10976
  editorAreaRef,
@@ -10695,7 +10998,7 @@ function DocBlocksShell({
10695
10998
  confirmLabel: "Rename"
10696
10999
  });
10697
11000
  if (!newName || newName === ws.name) return;
10698
- await saveWorkspace2({ ...ws, name: newName });
11001
+ await saveWorkspace3({ ...ws, name: newName });
10699
11002
  setPinnedDocuments(
10700
11003
  (current) => renamePinnedDocumentWorkspace(current, activeWorkspaceId, newName)
10701
11004
  );
@@ -10721,7 +11024,7 @@ function DocBlocksShell({
10721
11024
  throw error;
10722
11025
  }
10723
11026
  if (!isCurrent()) {
10724
- await getFileSystemProviderV210(mem)?.dispose();
11027
+ await getFileSystemProviderV211(mem)?.dispose();
10725
11028
  await revokeAbandonedResource();
10726
11029
  return;
10727
11030
  }
@@ -10769,7 +11072,7 @@ function DocBlocksShell({
10769
11072
  } finally {
10770
11073
  try {
10771
11074
  if (!adoptionStarted) {
10772
- await getFileSystemProviderV210(mem)?.dispose();
11075
+ await getFileSystemProviderV211(mem)?.dispose();
10773
11076
  }
10774
11077
  } finally {
10775
11078
  if (!opened) await revokeAbandonedResource();
@@ -10840,7 +11143,7 @@ function DocBlocksShell({
10840
11143
  push: true
10841
11144
  });
10842
11145
  } finally {
10843
- if (!adoptionStarted) await getFileSystemProviderV210(mem)?.dispose();
11146
+ if (!adoptionStarted) await getFileSystemProviderV211(mem)?.dispose();
10844
11147
  }
10845
11148
  },
10846
11149
  [adoptTransientWorkspace]
@@ -10870,8 +11173,11 @@ function DocBlocksShell({
10870
11173
  import("@bendyline/squisq-formats/container")
10871
11174
  ]);
10872
11175
  const container = new MemoryContentContainer();
10873
- await copyProviderToContainer(provider, container, "");
10874
- const blob = await containerToZip(container);
11176
+ const source = getFileSystemProviderV211(provider);
11177
+ if (!source) throw new Error("This workspace does not support complete backups.");
11178
+ const workspaces = await appendWorkspaceBackup(container, source, []);
11179
+ await finishWorkspaceBackup(container, workspaces);
11180
+ const blob = await containerToZip(container, { compression: "STORE" });
10875
11181
  const url = URL.createObjectURL(blob);
10876
11182
  const a = document.createElement("a");
10877
11183
  a.href = url;
@@ -10880,8 +11186,7 @@ function DocBlocksShell({
10880
11186
  a.click();
10881
11187
  URL.revokeObjectURL(url);
10882
11188
  } catch (err) {
10883
- console.error("Failed to download workspace", err);
10884
- showToast("error", "Failed to download workspace. See console for details.");
11189
+ showToast("error", err instanceof Error ? err.message : "Failed to download workspace.");
10885
11190
  }
10886
11191
  }, [provider, documentSession, showToast]);
10887
11192
  const handleDownloadAllWorkspaces = useCallback18(async () => {
@@ -10901,7 +11206,7 @@ function DocBlocksShell({
10901
11206
  return;
10902
11207
  }
10903
11208
  const container = new MemoryContentContainer();
10904
- const usedFolders = /* @__PURE__ */ new Set();
11209
+ let backupWorkspaces = [];
10905
11210
  const skipped = [];
10906
11211
  for (const ws of candidates) {
10907
11212
  let p = null;
@@ -10927,24 +11232,20 @@ function DocBlocksShell({
10927
11232
  skipped.push(ws.name);
10928
11233
  continue;
10929
11234
  }
10930
- const base = (ws.name || "workspace").replace(/[^a-z0-9_\- ]/gi, "_").trim() || "workspace";
10931
- let folder = base;
10932
- let suffix = 2;
10933
- while (usedFolders.has(folder)) {
10934
- folder = `${base} (${suffix++})`;
10935
- }
10936
- usedFolders.add(folder);
10937
11235
  try {
10938
- await copyProviderToContainer(p, container, folder);
11236
+ const source = getFileSystemProviderV211(p);
11237
+ if (!source) throw new Error("This workspace does not support complete backups.");
11238
+ backupWorkspaces = await appendWorkspaceBackup(container, source, backupWorkspaces);
10939
11239
  } finally {
10940
- if (ownsProvider) await getFileSystemProviderV210(p)?.dispose();
11240
+ if (ownsProvider) await getFileSystemProviderV211(p)?.dispose();
10941
11241
  }
10942
11242
  }
10943
- if (usedFolders.size === 0) {
11243
+ if (backupWorkspaces.length === 0) {
10944
11244
  showToast("error", "No workspaces could be opened for download.");
10945
11245
  return;
10946
11246
  }
10947
- const blob = await containerToZip(container);
11247
+ await finishWorkspaceBackup(container, backupWorkspaces);
11248
+ const blob = await containerToZip(container, { compression: "STORE" });
10948
11249
  const url = URL.createObjectURL(blob);
10949
11250
  const a = document.createElement("a");
10950
11251
  a.href = url;
@@ -10954,12 +11255,11 @@ function DocBlocksShell({
10954
11255
  if (skipped.length > 0) {
10955
11256
  await acknowledge({
10956
11257
  title: "Some workspaces were skipped",
10957
- message: `Downloaded ${usedFolders.size} workspace(s). Skipped ${skipped.length} that require re-granting access: ${skipped.join(", ")}.`
11258
+ message: `Downloaded ${backupWorkspaces.length} workspace(s). Skipped ${skipped.length} that require re-granting access: ${skipped.join(", ")}.`
10958
11259
  });
10959
11260
  }
10960
11261
  } catch (err) {
10961
- console.error("Failed to download all workspaces", err);
10962
- showToast("error", "Failed to download all workspaces. See console for details.");
11262
+ showToast("error", err instanceof Error ? err.message : "Failed to download all workspaces.");
10963
11263
  }
10964
11264
  }, [documentSession, acknowledge, showToast]);
10965
11265
  const handleKeepBrowserData = useCallback18(async () => {
@@ -11050,7 +11350,7 @@ function DocBlocksShell({
11050
11350
  } else if (ws?.type === "indexeddb") {
11051
11351
  await (await loadIndexedDbFileSystem()).deleteIndexedDBWorkspaceData(workspaceId);
11052
11352
  }
11053
- await removeWorkspace(workspaceId);
11353
+ await removeWorkspace2(workspaceId);
11054
11354
  forgetRecentDocuments(workspaceId);
11055
11355
  for (const document2 of pinnedDocuments) {
11056
11356
  if (document2.workspaceId === workspaceId) {
@@ -11078,7 +11378,7 @@ function DocBlocksShell({
11078
11378
  rootPath: info.rootPath,
11079
11379
  lastOpened: (/* @__PURE__ */ new Date()).toISOString()
11080
11380
  };
11081
- await saveWorkspace2(descriptor);
11381
+ await saveWorkspace3(descriptor);
11082
11382
  const p = await createHostFileSystemProvider(info.id, info.name, info.rootPath);
11083
11383
  setProvider(p);
11084
11384
  setActiveWorkspaceId(info.id);
@@ -11219,6 +11519,7 @@ function DocBlocksShell({
11219
11519
  onVersioningPreferenceChange: handleVersioningPreferenceChange,
11220
11520
  onOpenWorkspaceSettings: activeWorkspaceDescriptor ? handleOpenWorkspaceSettings : void 0,
11221
11521
  onDownloadAllWorkspaces: handleDownloadAllWorkspaces,
11522
+ onRestoreBackup: hostSupports2("nativeWorkspaces") ? void 0 : () => restoreInputRef.current?.click(),
11222
11523
  onShowKeyboardShortcuts: singlePane ? void 0 : openKeyboardShortcuts,
11223
11524
  onKeepBrowserData: showBrowserStorageWarning && !browserStoragePersistent ? handleKeepBrowserData : void 0,
11224
11525
  onInstallApp: installPromptEvent ? handleInstallApp : void 0,
@@ -11231,6 +11532,21 @@ function DocBlocksShell({
11231
11532
  settingsRequest
11232
11533
  }
11233
11534
  ),
11535
+ /* @__PURE__ */ jsx18(
11536
+ "input",
11537
+ {
11538
+ ref: restoreInputRef,
11539
+ type: "file",
11540
+ accept: ".zip,application/zip",
11541
+ "aria-label": "Restore workspace backup",
11542
+ hidden: true,
11543
+ onChange: (event) => {
11544
+ const file = event.currentTarget.files?.[0];
11545
+ event.currentTarget.value = "";
11546
+ if (file) void handleRestoreBackup(file);
11547
+ }
11548
+ }
11549
+ ),
11234
11550
  /* @__PURE__ */ jsx18(
11235
11551
  WorkspacePicker,
11236
11552
  {
@@ -11437,6 +11753,7 @@ function DocBlocksShell({
11437
11753
  saveDashboardImageOutput: saveRenderedImageOutput,
11438
11754
  onChange: handleEditorChange,
11439
11755
  onLinkClick: handleEditorLinkClick,
11756
+ wikiLinks,
11440
11757
  colorScheme: resolvedTheme,
11441
11758
  defaultThemeId: workspaceDefaultThemeId,
11442
11759
  defaultThemeLabel: "Workspace default",