@bendyline/docblocks-react 2.3.4 → 2.4.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/dist/index.js CHANGED
@@ -8,8 +8,9 @@ import {
8
8
  useGitContext
9
9
  } from "./chunk-HFYPTTCH.js";
10
10
  import {
11
+ createImageSaveOutput,
11
12
  updateExportTargetExtension
12
- } from "./chunk-BI7NVU6T.js";
13
+ } from "./chunk-FNSRCCBC.js";
13
14
  import {
14
15
  AccentColorSettings,
15
16
  DB_CHROME_COLORS,
@@ -658,6 +659,7 @@ function FileTreeNode({
658
659
  selected,
659
660
  badge,
660
661
  gitActions,
662
+ actions = [],
661
663
  focusable = true,
662
664
  posInSet,
663
665
  setSize,
@@ -829,6 +831,18 @@ function FileTreeNode({
829
831
  );
830
832
  }
831
833
  }, [closeMenu, entry.path, onTogglePin, pinned]);
834
+ const handleCustomAction = useCallback2(
835
+ async (action) => {
836
+ closeMenu(false);
837
+ setActionError(null);
838
+ try {
839
+ await action.onSelect();
840
+ } catch (caught) {
841
+ setActionError(caught instanceof Error ? caught.message : `Unable to ${action.label}.`);
842
+ }
843
+ },
844
+ [closeMenu]
845
+ );
832
846
  useEffect2(() => {
833
847
  if (!showContext) return;
834
848
  function handleClose(e) {
@@ -1009,6 +1023,22 @@ function FileTreeNode({
1009
1023
  children: pinned ? "Unpin" : "Pin"
1010
1024
  }
1011
1025
  ),
1026
+ actions.length > 0 && /* @__PURE__ */ jsxs2(Fragment, { children: [
1027
+ /* @__PURE__ */ jsx3("div", { className: "db-tree-context-divider", role: "separator" }),
1028
+ actions.map((action) => /* @__PURE__ */ jsx3(
1029
+ "button",
1030
+ {
1031
+ type: "button",
1032
+ role: "menuitem",
1033
+ tabIndex: -1,
1034
+ className: "db-tree-context-item",
1035
+ disabled: action.disabled,
1036
+ onClick: () => void handleCustomAction(action),
1037
+ children: action.label
1038
+ },
1039
+ action.label
1040
+ ))
1041
+ ] }),
1012
1042
  gitActions && (gitActions.viewChanges || gitActions.fileHistory) && /* @__PURE__ */ jsxs2(Fragment, { children: [
1013
1043
  /* @__PURE__ */ jsx3("div", { className: "db-tree-context-divider", role: "separator" }),
1014
1044
  gitActions.viewChanges && /* @__PURE__ */ jsx3(
@@ -1565,7 +1595,18 @@ function sortFileEntries(entries, mode) {
1565
1595
 
1566
1596
  // src/FileExplorer/FileExplorer.tsx
1567
1597
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1568
- var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".txt", ".md", ".docx", ".pdf", ".dbk", ".zip"]);
1598
+ var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
1599
+ ".txt",
1600
+ ".md",
1601
+ ".html",
1602
+ ".htm",
1603
+ ".docx",
1604
+ ".pdf",
1605
+ ".pptx",
1606
+ ".xlsx",
1607
+ ".dbk",
1608
+ ".zip"
1609
+ ]);
1569
1610
  var INTERNAL_DRAG_TYPE = "application/x-docblocks-entry";
1570
1611
  var NEW_ITEM_ERROR_ID = "db-new-item-error";
1571
1612
  function normalisePath2(path) {
@@ -1616,6 +1657,7 @@ function FileExplorer({
1616
1657
  onPinnedDocumentDelete,
1617
1658
  onTogglePin,
1618
1659
  onSelect,
1660
+ actionsForEntry,
1619
1661
  onTreeMutation,
1620
1662
  onTreeChange,
1621
1663
  onImportFiles,
@@ -1975,6 +2017,7 @@ function FileExplorer({
1975
2017
  selected: tree.selectedPath !== null && normalisePath2(tree.selectedPath) === normalisePath2(entry.path),
1976
2018
  badge: badgeFor(entry),
1977
2019
  gitActions: gitActionsFor(entry),
2020
+ actions: actionsForEntry?.(entry),
1978
2021
  onToggle: tree.toggleExpand,
1979
2022
  onSelect: handleSelect,
1980
2023
  confirmDelete,
@@ -2029,6 +2072,7 @@ function FileExplorer({
2029
2072
  childIssues,
2030
2073
  badgeFor,
2031
2074
  gitActionsFor,
2075
+ actionsForEntry,
2032
2076
  draggedEntry,
2033
2077
  dropTarget,
2034
2078
  handleInternalDragStart,
@@ -2523,21 +2567,22 @@ import {
2523
2567
  DocumentVersionManager
2524
2568
  } from "@bendyline/squisq/versions";
2525
2569
  import {
2526
- FileSystemContentContainer,
2570
+ FileSystemContentContainer as FileSystemContentContainer2,
2527
2571
  createFileMediaProvider,
2528
2572
  decodeUtf8Text,
2529
- FsError as FsError5,
2530
- getFileSystemProviderV2 as getFileSystemProviderV25,
2573
+ FsError as FsError6,
2574
+ getFileSystemProviderV2 as getFileSystemProviderV27,
2531
2575
  isQuotaExceededError,
2532
2576
  moveFileSystemEntry as moveFileSystemEntry2,
2533
- parseWorkspacePath as parseWorkspacePath5,
2577
+ parseWorkspacePath as parseWorkspacePath7,
2534
2578
  workspacePathContains
2535
2579
  } from "@bendyline/docblocks/filesystem";
2536
2580
  import {
2537
- createFileSystemDocumentTarget,
2581
+ createFileSystemDocumentTarget as createFileSystemDocumentTarget2,
2538
2582
  DocumentCommitConflictError,
2539
2583
  DocumentSessionConflictError
2540
2584
  } from "@bendyline/docblocks/document";
2585
+ import { createMediaProviderFromContainer } from "@bendyline/squisq/storage";
2541
2586
  import { isElectronHost as isElectronHost3, getDocBlocksHost } from "@bendyline/docblocks/host";
2542
2587
  import {
2543
2588
  parseSharedDocumentHash
@@ -3671,7 +3716,123 @@ function hasControlCharacter(value) {
3671
3716
  }
3672
3717
 
3673
3718
  // src/DocBlocksShell/import-files.ts
3674
- import { FsError as FsError3 } from "@bendyline/docblocks/filesystem";
3719
+ import {
3720
+ FsError as FsError3,
3721
+ getFileSystemProviderV2 as getFileSystemProviderV23,
3722
+ parseWorkspacePath as parseWorkspacePath4
3723
+ } from "@bendyline/docblocks/filesystem";
3724
+
3725
+ // src/DocBlocksShell/outside-in-contract.ts
3726
+ var OUTSIDE_IN_FORMAT_IDS = ["html", "docx", "pdf", "pptx", "xlsx"];
3727
+ var FORMAT_IDS = new Set(OUTSIDE_IN_FORMAT_IDS);
3728
+ var UPDATE_FROM_MARKDOWN_KEY = "squisq-updatefrommarkdown";
3729
+ function normalizePath(path) {
3730
+ const leading = path.replace(/\\/g, "/").startsWith("/") ? "/" : "";
3731
+ const parts = path.replace(/\\/g, "/").split("/").filter(Boolean);
3732
+ if (parts.some((part) => part === "." || part === "..")) {
3733
+ throw new Error(`Outside-in paths must be canonical workspace paths: ${path}`);
3734
+ }
3735
+ return leading + parts.join("/");
3736
+ }
3737
+ function join(parent, child) {
3738
+ if (!parent || parent === "/") return parent === "/" ? `/${child}` : child;
3739
+ return `${parent}/${child}`;
3740
+ }
3741
+ function slug(stem) {
3742
+ return stem.normalize("NFKD").replace(/\p{Mark}+/gu, "").toLocaleLowerCase("en-US").replace(/[^\p{Letter}\p{Number}]+/gu, "-").replace(/^-+|-+$/g, "") || "document";
3743
+ }
3744
+ function resolveOutsideInLayout(path) {
3745
+ const targetPath = normalizePath(path);
3746
+ const slash = targetPath.lastIndexOf("/");
3747
+ const parentDirectory = slash < 0 ? "" : slash === 0 ? "/" : targetPath.slice(0, slash);
3748
+ const filename = slash < 0 ? targetPath : targetPath.slice(slash + 1);
3749
+ const dot = filename.lastIndexOf(".");
3750
+ if (dot <= 0) return null;
3751
+ const rawFormat = filename.slice(dot + 1).toLowerCase();
3752
+ const format = rawFormat === "htm" ? "html" : rawFormat;
3753
+ if (!FORMAT_IDS.has(format)) return null;
3754
+ const stem = filename.slice(0, dot);
3755
+ const companionName = `${stem}_files`;
3756
+ const companionDirectory = join(parentDirectory, companionName);
3757
+ const markdownFilename = `${slug(stem)}.md`;
3758
+ const backupDirectory = join(companionDirectory, ".original");
3759
+ const backupFilename = `original.${format}`;
3760
+ return {
3761
+ targetPath,
3762
+ format,
3763
+ parentDirectory,
3764
+ stem,
3765
+ companionName,
3766
+ companionDirectory,
3767
+ markdownFilename,
3768
+ markdownPath: join(companionDirectory, markdownFilename),
3769
+ relativeTargetPath: `../${filename}`,
3770
+ backupDirectory,
3771
+ backupFilename,
3772
+ backupPath: join(backupDirectory, backupFilename)
3773
+ };
3774
+ }
3775
+ function chooseOutsideInMarkdownPath(layout, paths) {
3776
+ const canonical = normalizePath(layout.markdownPath);
3777
+ const normalized = paths.map(normalizePath);
3778
+ const exact = normalized.find((path) => path === canonical);
3779
+ if (exact) return exact;
3780
+ const folded = normalized.find(
3781
+ (path) => path.toLocaleLowerCase("en-US") === canonical.toLocaleLowerCase("en-US")
3782
+ );
3783
+ if (folded) return folded;
3784
+ const prefix = `${normalizePath(layout.companionDirectory).replace(/\/$/, "")}/`;
3785
+ const markdown = normalized.filter(
3786
+ (path) => path.startsWith(prefix) && !path.slice(prefix.length).includes("/") && path.toLocaleLowerCase("en-US").endsWith(".md")
3787
+ );
3788
+ return markdown.length === 1 ? markdown[0] : null;
3789
+ }
3790
+ async function readOutsideInMetadata(source) {
3791
+ const { readOutsideInMetadata: readMetadata } = await import("@bendyline/squisq-formats/outside-in");
3792
+ const metadata = readMetadata(source);
3793
+ return metadata ? {
3794
+ ...metadata,
3795
+ updateFromMarkdown: await isOutsideInMarkdownEditingEnabled(source)
3796
+ } : null;
3797
+ }
3798
+ async function withOutsideInMetadata(source, layout) {
3799
+ const { withOutsideInMetadata: addMetadata } = await import("@bendyline/squisq-formats/outside-in");
3800
+ return addMetadata(source, layout);
3801
+ }
3802
+ async function isOutsideInMarkdownEditingEnabled(source) {
3803
+ const module = await import("@bendyline/squisq-formats/outside-in");
3804
+ if (module.isOutsideInMarkdownEditingEnabled) {
3805
+ return module.isOutsideInMarkdownEditingEnabled(source);
3806
+ }
3807
+ const { parseFrontmatter, splitFrontmatterBlock } = await import("@bendyline/squisq/markdown");
3808
+ const block = splitFrontmatterBlock(source).frontmatter;
3809
+ if (!block) return false;
3810
+ const firstBreak = block.indexOf("\n");
3811
+ if (firstBreak < 0) return false;
3812
+ const yaml = block.slice(firstBreak + 1).replace(/\r?\n---(?:\r?\n)?$/, "");
3813
+ return parseFrontmatter(yaml)?.[UPDATE_FROM_MARKDOWN_KEY] === true;
3814
+ }
3815
+ async function withOutsideInMarkdownEditing(source, layout, enabled = true) {
3816
+ const module = await import("@bendyline/squisq-formats/outside-in");
3817
+ if (module.withOutsideInMarkdownEditing) {
3818
+ return module.withOutsideInMarkdownEditing(source, layout, enabled);
3819
+ }
3820
+ const { setFrontmatterValues } = await import("@bendyline/squisq/markdown");
3821
+ return setFrontmatterValues(await withOutsideInMetadata(source, layout), {
3822
+ [UPDATE_FROM_MARKDOWN_KEY]: enabled
3823
+ });
3824
+ }
3825
+ async function importOutsideInDocument(source, options = {}) {
3826
+ const { importOutsideInDocument: importDocument } = await import("@bendyline/squisq-formats/outside-in");
3827
+ const imported = await importDocument(source, options);
3828
+ const layout = resolveOutsideInLayout(source.targetPath);
3829
+ if (!layout) throw new Error(`Outside-in editing does not support "${source.targetPath}".`);
3830
+ return { ...imported, layout };
3831
+ }
3832
+ async function renderOutsideInDocument(source, options = {}) {
3833
+ const { renderOutsideInDocument: renderDocument } = await import("@bendyline/squisq-formats/outside-in");
3834
+ return renderDocument(source, options);
3835
+ }
3675
3836
 
3676
3837
  // src/DocBlocksShell/provider-io.ts
3677
3838
  import {
@@ -3730,23 +3891,68 @@ async function claimAvailablePath(provider, desiredPath) {
3730
3891
  }
3731
3892
  throw new Error(`Too many documents are already named like \u201C${desiredPath}\u201D.`);
3732
3893
  }
3733
- async function readImportedMarkdown(file, ext, destPath, provider, deps) {
3734
- if (ext === ".md" || ext === ".txt") {
3735
- return file.text();
3894
+ async function writeProviderBytes(provider, path, data, mode) {
3895
+ const providerV2 = getFileSystemProviderV23(provider);
3896
+ if (providerV2) {
3897
+ await providerV2.writeFile(parseWorkspacePath4(path), data, {
3898
+ mode,
3899
+ createParents: true,
3900
+ expectedVersion: mode === "create" ? null : void 0
3901
+ });
3902
+ return;
3736
3903
  }
3737
- if (ext === ".docx") {
3738
- const { docxToContainer } = await import("@bendyline/squisq-formats/docx");
3739
- const container = await docxToContainer(await file.arrayBuffer());
3740
- const markdown = await container.readDocument() ?? "";
3741
- await deps.persistMedia(container, destPath);
3742
- return markdown;
3904
+ if (mode === "create" && await providerEntryExists(provider, path)) {
3905
+ throw new FsError3("already-exists", "File already exists.", { operation: "write", path });
3743
3906
  }
3744
- if (ext === ".pdf") {
3745
- const { pdfToContainer } = await import("@bendyline/squisq-formats/pdf");
3746
- const container = await pdfToContainer(await file.arrayBuffer());
3747
- const markdown = await container.readDocument() ?? "";
3748
- await deps.persistMedia(container, destPath);
3749
- return markdown;
3907
+ await provider.writeBinary(path, data);
3908
+ }
3909
+ var OUTSIDE_IN_EXTENSIONS = /* @__PURE__ */ new Set([".html", ".htm", ".docx", ".pdf", ".pptx", ".xlsx"]);
3910
+ async function claimOutsideInTarget(provider, desiredPath) {
3911
+ for (let attempt = 1; attempt <= MAX_NAME_ATTEMPTS; attempt++) {
3912
+ const candidate = attempt === 1 ? desiredPath : withCopySuffix(desiredPath, attempt);
3913
+ const layout = resolveOutsideInLayout(candidate);
3914
+ if (!layout) throw new Error(`Outside-in editing does not support \u201C${desiredPath}\u201D.`);
3915
+ if (await providerEntryExists(provider, layout.companionDirectory)) continue;
3916
+ try {
3917
+ await writeProviderBytes(provider, candidate, new Uint8Array(), "create");
3918
+ return { path: candidate, renamed: attempt > 1 };
3919
+ } catch (error) {
3920
+ if (!isAlreadyExists(error)) throw error;
3921
+ }
3922
+ }
3923
+ throw new Error(`Too many documents are already named like \u201C${desiredPath}\u201D.`);
3924
+ }
3925
+ async function importOutsideInFile(file, provider) {
3926
+ const claim = await claimOutsideInTarget(provider, file.name);
3927
+ const created = [claim.path];
3928
+ try {
3929
+ const sourceBytes = await file.arrayBuffer();
3930
+ const imported = await importOutsideInDocument({
3931
+ data: sourceBytes,
3932
+ targetPath: claim.path
3933
+ });
3934
+ for (const entry of await imported.container.listFiles()) {
3935
+ if (/\.md$/i.test(entry.path)) continue;
3936
+ const data = await imported.container.readFile(entry.path);
3937
+ if (!data) continue;
3938
+ const destination = `${imported.layout.companionDirectory}/${entry.path.replace(/^\/+/, "")}`;
3939
+ await writeProviderBytes(provider, destination, data, "create");
3940
+ created.push(destination);
3941
+ }
3942
+ await writeProviderText(provider, imported.layout.markdownPath, imported.markdown, "create");
3943
+ created.push(imported.layout.markdownPath);
3944
+ await writeProviderBytes(provider, claim.path, sourceBytes, "upsert");
3945
+ return claim;
3946
+ } catch (error) {
3947
+ for (const path of created.reverse()) {
3948
+ await removeProviderEntry(provider, path).catch(() => void 0);
3949
+ }
3950
+ throw error;
3951
+ }
3952
+ }
3953
+ async function readImportedMarkdown(file, ext, destPath, provider) {
3954
+ if (ext === ".md" || ext === ".txt") {
3955
+ return file.text();
3750
3956
  }
3751
3957
  const snapshot = await decodeDbkWorkspace(await file.arrayBuffer(), {
3752
3958
  targetDocumentPath: destPath
@@ -3761,8 +3967,8 @@ async function writeDbkCompanions(snapshot, provider) {
3761
3967
  await writeProviderText(provider, entry.path, entry.content, "create");
3762
3968
  }
3763
3969
  }
3764
- var SUPPORTED_EXTENSIONS2 = /* @__PURE__ */ new Set([".md", ".txt", ".docx", ".pdf", ".dbk", ".zip"]);
3765
- async function importDroppedFiles(files, provider, deps) {
3970
+ var SUPPORTED_EXTENSIONS2 = /* @__PURE__ */ new Set([".md", ".txt", ...OUTSIDE_IN_EXTENSIONS, ".dbk", ".zip"]);
3971
+ async function importDroppedFiles(files, provider) {
3766
3972
  const result = { imported: [], failed: [], unsupported: [] };
3767
3973
  for (const file of files) {
3768
3974
  const ext = file.name.slice(file.name.lastIndexOf(".")).toLowerCase();
@@ -3773,9 +3979,14 @@ async function importDroppedFiles(files, provider, deps) {
3773
3979
  const baseName = file.name.replace(/\.[^.]+$/, "");
3774
3980
  let claimed = null;
3775
3981
  try {
3982
+ if (OUTSIDE_IN_EXTENSIONS.has(ext)) {
3983
+ const imported = await importOutsideInFile(file, provider);
3984
+ result.imported.push({ source: file.name, ...imported });
3985
+ continue;
3986
+ }
3776
3987
  const claim = await claimAvailablePath(provider, `${baseName}.md`);
3777
3988
  claimed = claim.path;
3778
- const markdown = await readImportedMarkdown(file, ext, claim.path, provider, deps);
3989
+ const markdown = await readImportedMarkdown(file, ext, claim.path, provider);
3779
3990
  await writeProviderText(provider, claim.path, markdown);
3780
3991
  result.imported.push({ source: file.name, path: claim.path, renamed: claim.renamed });
3781
3992
  } catch (error) {
@@ -3807,7 +4018,7 @@ function summariseImport(result) {
3807
4018
  if (renamed.length === 1) {
3808
4019
  return {
3809
4020
  kind: "success",
3810
- message: `Imported as ${renamed[0].path} \u2014 a document named ${renamed[0].source.replace(/\.[^.]+$/, "")}.md already exists.`
4021
+ message: `Imported as ${renamed[0].path} \u2014 a document with that name or companion folder already exists.`
3811
4022
  };
3812
4023
  }
3813
4024
  if (renamed.length > 1) {
@@ -3819,6 +4030,241 @@ function summariseImport(result) {
3819
4030
  return null;
3820
4031
  }
3821
4032
 
4033
+ // src/DocBlocksShell/outside-in.ts
4034
+ import {
4035
+ FileSystemContentContainer,
4036
+ FsError as FsError4,
4037
+ getFileSystemProviderV2 as getFileSystemProviderV24,
4038
+ parseWorkspacePath as parseWorkspacePath5
4039
+ } from "@bendyline/docblocks/filesystem";
4040
+ import {
4041
+ createFileSystemDocumentTarget
4042
+ } from "@bendyline/docblocks/document";
4043
+ var OUTSIDE_IN_EXTENSION = /\.(?:html?|docx|pdf|pptx|xlsx)$/i;
4044
+ var SQUISQ_RUNTIME_DIRECTORY = "_squisq";
4045
+ var SQUISQ_RUNTIME_FILENAME = "squisq-player.js";
4046
+ function withoutLeadingSlash(path) {
4047
+ return parseWorkspacePath5(path);
4048
+ }
4049
+ function withLegacySlash(path, like) {
4050
+ const canonical = withoutLeadingSlash(path);
4051
+ return like.startsWith("/") && canonical ? `/${canonical}` : canonical;
4052
+ }
4053
+ function dirname(path) {
4054
+ const canonical = withoutLeadingSlash(path);
4055
+ const slash = canonical.lastIndexOf("/");
4056
+ return slash < 0 ? "" : canonical.slice(0, slash);
4057
+ }
4058
+ function join2(parent, child) {
4059
+ return parent ? `${parent}/${child}` : child;
4060
+ }
4061
+ function relativePath(fromDirectory, targetPath) {
4062
+ const from = withoutLeadingSlash(fromDirectory).split("/").filter(Boolean);
4063
+ const target = withoutLeadingSlash(targetPath).split("/").filter(Boolean);
4064
+ let shared = 0;
4065
+ while (shared < from.length && shared < target.length && from[shared] === target[shared]) {
4066
+ shared++;
4067
+ }
4068
+ const segments = [...from.slice(shared).map(() => ".."), ...target.slice(shared)];
4069
+ return segments.join("/") || ".";
4070
+ }
4071
+ async function readText(provider, path) {
4072
+ const v2 = getFileSystemProviderV24(provider);
4073
+ if (!v2) return provider.readFile(path);
4074
+ const current = await v2.readFile(parseWorkspacePath5(path));
4075
+ if (!current) return null;
4076
+ return new TextDecoder("utf-8", { fatal: true }).decode(current.data);
4077
+ }
4078
+ async function readBytes(provider, path) {
4079
+ const v2 = getFileSystemProviderV24(provider);
4080
+ if (v2) return (await v2.readFile(parseWorkspacePath5(path)))?.data ?? null;
4081
+ return provider.readBinary(path);
4082
+ }
4083
+ async function writeBytes(provider, path, data, mode = "upsert") {
4084
+ const v2 = getFileSystemProviderV24(provider);
4085
+ if (v2) {
4086
+ await v2.writeFile(parseWorkspacePath5(path), data, {
4087
+ mode,
4088
+ createParents: true,
4089
+ expectedVersion: mode === "create" ? null : void 0
4090
+ });
4091
+ return;
4092
+ }
4093
+ if (mode === "create" && await provider.exists(path)) {
4094
+ throw new FsError4("already-exists", "File already exists.", { operation: "write", path });
4095
+ }
4096
+ await provider.writeBinary(path, data);
4097
+ }
4098
+ async function listCompanionFiles(provider, layout) {
4099
+ try {
4100
+ const entries = await provider.readDirectory(layout.companionDirectory);
4101
+ return entries.filter((entry) => entry.kind === "file").map((entry) => entry.path);
4102
+ } catch (error) {
4103
+ if (error instanceof FsError4 && error.code === "not-found") return [];
4104
+ throw error;
4105
+ }
4106
+ }
4107
+ async function removeOutsideInCompanion(provider, layout) {
4108
+ const providerV2 = getFileSystemProviderV24(provider);
4109
+ if (providerV2) {
4110
+ const path = parseWorkspacePath5(layout.companionDirectory);
4111
+ if (await providerV2.stat(path) !== null) {
4112
+ await providerV2.remove(path, { recursive: true, missing: "ignore" });
4113
+ }
4114
+ return;
4115
+ }
4116
+ if (await provider.exists(layout.companionDirectory)) {
4117
+ await provider.delete(layout.companionDirectory);
4118
+ }
4119
+ }
4120
+ async function persistImportedMedia(provider, layout, container) {
4121
+ const entries = await container.listFiles();
4122
+ for (const entry of entries) {
4123
+ if (/\.md$/i.test(entry.path)) continue;
4124
+ const data = await container.readFile(entry.path);
4125
+ if (!data) continue;
4126
+ await writeBytes(provider, join2(layout.companionDirectory, entry.path), data, "create");
4127
+ }
4128
+ }
4129
+ async function loadEditableShellDocument(provider, selectedPath) {
4130
+ if (!OUTSIDE_IN_EXTENSION.test(selectedPath)) {
4131
+ const content = await readText(provider, selectedPath);
4132
+ return content === null ? null : {
4133
+ displayPath: selectedPath,
4134
+ sourcePath: selectedPath,
4135
+ content,
4136
+ outsideIn: null,
4137
+ outsideInEditingEnabled: true
4138
+ };
4139
+ }
4140
+ const resolved = resolveOutsideInLayout(selectedPath);
4141
+ if (!resolved) return null;
4142
+ const layout = {
4143
+ ...resolved,
4144
+ targetPath: withLegacySlash(resolved.targetPath, selectedPath),
4145
+ companionDirectory: withLegacySlash(resolved.companionDirectory, selectedPath),
4146
+ markdownPath: withLegacySlash(resolved.markdownPath, selectedPath)
4147
+ };
4148
+ const candidates = (await listCompanionFiles(provider, layout)).map(
4149
+ (path) => withLegacySlash(path, selectedPath)
4150
+ );
4151
+ const chosen = chooseOutsideInMarkdownPath(layout, candidates);
4152
+ if (chosen) {
4153
+ const content = await readText(provider, chosen);
4154
+ if (content === null) return null;
4155
+ const metadata = await readOutsideInMetadata(content);
4156
+ if (metadata && metadata.format !== layout.format) {
4157
+ throw new Error(
4158
+ `${chosen} is configured for ${metadata.format}, not ${layout.format}. Rename the rendered file back or repair the companion frontmatter.`
4159
+ );
4160
+ }
4161
+ const linkedContent = await withOutsideInMetadata(content, layout);
4162
+ if (linkedContent !== content) await writeProviderText(provider, chosen, linkedContent);
4163
+ return {
4164
+ displayPath: selectedPath,
4165
+ sourcePath: chosen,
4166
+ content: linkedContent,
4167
+ outsideIn: { ...layout, markdownPath: chosen },
4168
+ outsideInEditingEnabled: await isOutsideInMarkdownEditingEnabled(linkedContent)
4169
+ };
4170
+ }
4171
+ const rendered = await readBytes(provider, selectedPath);
4172
+ if (!rendered) return null;
4173
+ const imported = await importOutsideInDocument({
4174
+ data: rendered,
4175
+ targetPath: selectedPath
4176
+ });
4177
+ const importedLayout = {
4178
+ ...imported.layout,
4179
+ targetPath: withLegacySlash(imported.layout.targetPath, selectedPath),
4180
+ companionDirectory: withLegacySlash(imported.layout.companionDirectory, selectedPath),
4181
+ markdownPath: withLegacySlash(imported.layout.markdownPath, selectedPath)
4182
+ };
4183
+ await persistImportedMedia(provider, importedLayout, imported.container);
4184
+ await writeProviderText(provider, importedLayout.markdownPath, imported.markdown, "create");
4185
+ return {
4186
+ displayPath: selectedPath,
4187
+ sourcePath: importedLayout.markdownPath,
4188
+ content: imported.markdown,
4189
+ outsideIn: importedLayout,
4190
+ outsideInEditingEnabled: false
4191
+ };
4192
+ }
4193
+ async function enableOutsideInMarkdownEditing(provider, document2) {
4194
+ const layout = document2.outsideIn;
4195
+ if (!layout) throw new Error("This file does not support outside-in Markdown editing.");
4196
+ const original = await readBytes(provider, layout.targetPath);
4197
+ if (!original) throw new Error(`The rendered file "${layout.targetPath}" was not found.`);
4198
+ try {
4199
+ await writeBytes(provider, layout.backupPath, original, "create");
4200
+ } catch (error) {
4201
+ if (!(error instanceof FsError4 && error.code === "already-exists")) throw error;
4202
+ }
4203
+ const content = await withOutsideInMarkdownEditing(document2.content, layout);
4204
+ if (content !== document2.content) await writeProviderText(provider, document2.sourcePath, content);
4205
+ return { ...document2, content, outsideIn: layout, outsideInEditingEnabled: true };
4206
+ }
4207
+ async function findRuntimePath(provider, targetPath) {
4208
+ let directory = dirname(targetPath);
4209
+ for (; ; ) {
4210
+ const candidateDirectory = join2(directory, SQUISQ_RUNTIME_DIRECTORY);
4211
+ if (await providerEntryExists(provider, candidateDirectory)) {
4212
+ const providerV2 = getFileSystemProviderV24(provider);
4213
+ if (providerV2) {
4214
+ const entry = await providerV2.stat(parseWorkspacePath5(candidateDirectory));
4215
+ if (entry?.kind !== "directory") {
4216
+ throw new Error(`${candidateDirectory} must be a directory.`);
4217
+ }
4218
+ }
4219
+ return join2(candidateDirectory, SQUISQ_RUNTIME_FILENAME);
4220
+ }
4221
+ if (!directory) break;
4222
+ directory = dirname(directory);
4223
+ }
4224
+ return join2(SQUISQ_RUNTIME_DIRECTORY, SQUISQ_RUNTIME_FILENAME);
4225
+ }
4226
+ async function writeRuntimeIfNeeded(provider, runtimePath) {
4227
+ const { PLAYER_BUNDLE } = await import("@bendyline/squisq-react/standalone-source");
4228
+ const current = await readText(provider, runtimePath);
4229
+ if (current !== PLAYER_BUNDLE) await writeProviderText(provider, runtimePath, PLAYER_BUNDLE);
4230
+ }
4231
+ function createOutsideInDocumentTarget(provider, layout, onCommitted) {
4232
+ const sourceTarget = createFileSystemDocumentTarget(provider, layout.markdownPath);
4233
+ return {
4234
+ key: `${provider.id}:outside-in:${parseWorkspacePath5(layout.targetPath)}`,
4235
+ async commit(request) {
4236
+ if (!await isOutsideInMarkdownEditingEnabled(request.content)) {
4237
+ throw new Error(
4238
+ "Outside-in editing is read-only until squisq-updatefrommarkdown: true is set."
4239
+ );
4240
+ }
4241
+ const runtimePath = layout.format === "html" ? await findRuntimePath(provider, layout.targetPath) : null;
4242
+ const outputDirectory = dirname(layout.targetPath);
4243
+ const rendered = await renderOutsideInDocument(
4244
+ {
4245
+ markdown: request.content,
4246
+ targetPath: layout.targetPath,
4247
+ container: new FileSystemContentContainer(provider, layout.companionDirectory)
4248
+ },
4249
+ runtimePath ? {
4250
+ html: {
4251
+ playerScriptPath: relativePath(outputDirectory, runtimePath),
4252
+ basePath: relativePath(outputDirectory, layout.companionDirectory)
4253
+ }
4254
+ } : {}
4255
+ );
4256
+ await sourceTarget.commit({ ...request, targetKey: sourceTarget.key });
4257
+ if (runtimePath) await writeRuntimeIfNeeded(provider, runtimePath);
4258
+ await writeBytes(provider, layout.targetPath, rendered.bytes);
4259
+ onCommitted?.();
4260
+ return {};
4261
+ }
4262
+ };
4263
+ }
4264
+ function createOutsideInContentContainer(provider, layout) {
4265
+ return new FileSystemContentContainer(provider, layout.companionDirectory);
4266
+ }
4267
+
3822
4268
  // src/components/usePromptDialog.ts
3823
4269
  import React2, { useCallback as useCallback11, useEffect as useEffect12, useRef as useRef11, useState as useState12 } from "react";
3824
4270
 
@@ -4232,9 +4678,9 @@ import { useCallback as useCallback14, useEffect as useEffect14, useRef as useRe
4232
4678
 
4233
4679
  // src/DocBlocksShell/document-links.ts
4234
4680
  import {
4235
- FsError as FsError4,
4236
- getFileSystemProviderV2 as getFileSystemProviderV23,
4237
- parseWorkspacePath as parseWorkspacePath4
4681
+ FsError as FsError5,
4682
+ getFileSystemProviderV2 as getFileSystemProviderV25,
4683
+ parseWorkspacePath as parseWorkspacePath6
4238
4684
  } from "@bendyline/docblocks/filesystem";
4239
4685
  var DEFAULT_DOCUMENT_DISCOVERY_LIMITS = Object.freeze({
4240
4686
  entries: 1e4,
@@ -4257,9 +4703,9 @@ function positiveLimit(value, fallback, label) {
4257
4703
  return selected;
4258
4704
  }
4259
4705
  async function readDirectory(provider, path) {
4260
- const providerV2 = getFileSystemProviderV23(provider);
4706
+ const providerV2 = getFileSystemProviderV25(provider);
4261
4707
  if (!providerV2) return provider.readDirectory(path);
4262
- const entries = await providerV2.readDirectory(parseWorkspacePath4(path));
4708
+ const entries = await providerV2.readDirectory(parseWorkspacePath6(path));
4263
4709
  return entries.map((entry) => ({ kind: entry.kind, name: entry.name, path: entry.path }));
4264
4710
  }
4265
4711
  function cancellationError(signal) {
@@ -4326,7 +4772,7 @@ async function collectMarkdownFiles(provider, root, options = {}) {
4326
4772
  while (pending.length > 0) {
4327
4773
  if (options.signal?.aborted) throw cancellationError(options.signal);
4328
4774
  const current = pending.pop();
4329
- const canonical = parseWorkspacePath4(current.path);
4775
+ const canonical = parseWorkspacePath6(current.path);
4330
4776
  if (visited.has(canonical)) continue;
4331
4777
  visited.add(canonical);
4332
4778
  if (visited.size > maxDirectories) {
@@ -4339,7 +4785,7 @@ async function collectMarkdownFiles(provider, root, options = {}) {
4339
4785
  try {
4340
4786
  entries = await readWithBudget(provider, canonical, deadline, options.signal);
4341
4787
  } catch (error) {
4342
- if (error instanceof FsError4 && error.code === "not-found") continue;
4788
+ if (error instanceof FsError5 && error.code === "not-found") continue;
4343
4789
  throw error;
4344
4790
  }
4345
4791
  entryCount += entries.length;
@@ -4368,7 +4814,7 @@ async function collectMarkdownFiles(provider, root, options = {}) {
4368
4814
  }
4369
4815
  return markdown;
4370
4816
  }
4371
- function dirname(path) {
4817
+ function dirname2(path) {
4372
4818
  const clean = path.replace(/^\/+/, "");
4373
4819
  const index = clean.lastIndexOf("/");
4374
4820
  return index < 0 ? "" : clean.slice(0, index);
@@ -4392,16 +4838,16 @@ function relativeMarkdownLink(fromFile, toFile) {
4392
4838
  }
4393
4839
  function createDocumentLinkCandidates(entries, selectedFile, query) {
4394
4840
  const normalizedQuery = query.trim().toLowerCase();
4395
- const selected = parseWorkspacePath4(selectedFile);
4841
+ const selected = parseWorkspacePath6(selectedFile);
4396
4842
  const candidates = [];
4397
4843
  for (const entry of entries) {
4398
- if (entry.kind !== "file" || parseWorkspacePath4(entry.path) === selected) continue;
4844
+ if (entry.kind !== "file" || parseWorkspacePath6(entry.path) === selected) continue;
4399
4845
  const label = entry.name.replace(/\.md$/iu, "");
4400
4846
  const path = relativeMarkdownLink(selectedFile, entry.path);
4401
4847
  if (normalizedQuery && !label.toLowerCase().includes(normalizedQuery) && !path.toLowerCase().includes(normalizedQuery)) {
4402
4848
  continue;
4403
4849
  }
4404
- const description = dirname(entry.path);
4850
+ const description = dirname2(entry.path);
4405
4851
  candidates.push(description ? { path, label, description } : { path, label });
4406
4852
  }
4407
4853
  candidates.sort((left, right) => left.label.localeCompare(right.label));
@@ -4465,14 +4911,14 @@ var WorkspaceAuthorityBarrier = class {
4465
4911
 
4466
4912
  // src/DocBlocksShell/transient-workspace-move.ts
4467
4913
  import {
4468
- getFileSystemProviderV2 as getFileSystemProviderV24
4914
+ getFileSystemProviderV2 as getFileSystemProviderV26
4469
4915
  } from "@bendyline/docblocks/filesystem";
4470
4916
  function pathDepth(path) {
4471
4917
  return path ? path.split("/").length : 0;
4472
4918
  }
4473
4919
  async function copyTransientWorkspaceContents(source, destination) {
4474
- const sourceV2 = getFileSystemProviderV24(source);
4475
- const destinationV2 = getFileSystemProviderV24(destination);
4920
+ const sourceV2 = getFileSystemProviderV26(source);
4921
+ const destinationV2 = getFileSystemProviderV26(destination);
4476
4922
  if (!sourceV2 || !destinationV2) {
4477
4923
  throw new Error("Moving a temporary document requires a current workspace provider.");
4478
4924
  }
@@ -4542,7 +4988,7 @@ var WELCOME_DOCUMENT_CONTENT = [
4542
4988
  "squisq-cover-slide: false",
4543
4989
  "---",
4544
4990
  "",
4545
- '# DocBlocks: one Markdown file, many finished forms {[title subtitle="Local-first writing for pages, documents, slideshows, and video"]}',
4991
+ '# DocBlocks {[title subtitle="Local-first writing for pages, documents, slideshows, and video"]}',
4546
4992
  "",
4547
4993
  "Write in plain Markdown. Shape it visually. Keep it portable.",
4548
4994
  "",
@@ -4756,7 +5202,7 @@ function stripExtension(name) {
4756
5202
  return name.replace(/\.[^.]+$/, "");
4757
5203
  }
4758
5204
  function normaliseProviderPath(p) {
4759
- const canonical = parseWorkspacePath5(p);
5205
+ const canonical = parseWorkspacePath7(p);
4760
5206
  return canonical ? `/${canonical}` : "/";
4761
5207
  }
4762
5208
  function sameProviderPath(a, b) {
@@ -4769,32 +5215,32 @@ function relocateProviderPath(path, oldPath, newPath) {
4769
5215
  return `${newPath.replace(/\/+$/, "")}${current.slice(oldNormalised.length)}`;
4770
5216
  }
4771
5217
  function pathContains(parentPath2, candidatePath) {
4772
- return workspacePathContains(parseWorkspacePath5(parentPath2), parseWorkspacePath5(candidatePath));
5218
+ return workspacePathContains(parseWorkspacePath7(parentPath2), parseWorkspacePath7(candidatePath));
4773
5219
  }
4774
5220
  async function readProviderDirectory2(provider, path) {
4775
- const providerV2 = getFileSystemProviderV25(provider);
5221
+ const providerV2 = getFileSystemProviderV27(provider);
4776
5222
  if (!providerV2) return provider.readDirectory(path);
4777
- const entries = await providerV2.readDirectory(parseWorkspacePath5(path));
5223
+ const entries = await providerV2.readDirectory(parseWorkspacePath7(path));
4778
5224
  return entries.map((entry) => ({ kind: entry.kind, name: entry.name, path: entry.path }));
4779
5225
  }
4780
5226
  async function readProviderText(provider, path) {
4781
- const providerV2 = getFileSystemProviderV25(provider);
5227
+ const providerV2 = getFileSystemProviderV27(provider);
4782
5228
  if (!providerV2) return provider.readFile(path);
4783
- const file = await providerV2.readFile(parseWorkspacePath5(path));
4784
- return file ? decodeUtf8Text(file.data, { label: "The document", path: parseWorkspacePath5(path) }) : null;
5229
+ const file = await providerV2.readFile(parseWorkspacePath7(path));
5230
+ return file ? decodeUtf8Text(file.data, { label: "The document", path: parseWorkspacePath7(path) }) : null;
4785
5231
  }
4786
5232
  async function pinnedProviderFileExists(provider, path) {
4787
- const providerV2 = getFileSystemProviderV25(provider);
5233
+ const providerV2 = getFileSystemProviderV27(provider);
4788
5234
  if (!providerV2) return provider.exists(path);
4789
- const entry = await providerV2.stat(parseWorkspacePath5(path));
5235
+ const entry = await providerV2.stat(parseWorkspacePath7(path));
4790
5236
  if (!entry) return false;
4791
5237
  if (entry.kind !== "file") throw new Error("The pinned path is no longer a document.");
4792
5238
  return true;
4793
5239
  }
4794
5240
  async function removePinnedProviderFile(provider, path) {
4795
- const providerV2 = getFileSystemProviderV25(provider);
5241
+ const providerV2 = getFileSystemProviderV27(provider);
4796
5242
  if (providerV2) {
4797
- const canonical = parseWorkspacePath5(path);
5243
+ const canonical = parseWorkspacePath7(path);
4798
5244
  const entry = await providerV2.stat(canonical);
4799
5245
  if (!entry) return false;
4800
5246
  if (entry.kind !== "file") throw new Error("The pinned path is no longer a document.");
@@ -4810,13 +5256,13 @@ async function removePinnedProviderFile(provider, path) {
4810
5256
  return true;
4811
5257
  }
4812
5258
  async function readStableFileSnapshot(provider, path) {
4813
- const providerV2 = getFileSystemProviderV25(provider);
5259
+ const providerV2 = getFileSystemProviderV27(provider);
4814
5260
  if (providerV2) {
4815
- const read = await providerV2.readFile(parseWorkspacePath5(path));
5261
+ const read = await providerV2.readFile(parseWorkspacePath7(path));
4816
5262
  return {
4817
5263
  content: read ? decodeUtf8Text(read.data, {
4818
5264
  label: "The document",
4819
- path: parseWorkspacePath5(path)
5265
+ path: parseWorkspacePath7(path)
4820
5266
  }) : null,
4821
5267
  version: read?.entry.version ?? null
4822
5268
  };
@@ -4852,7 +5298,7 @@ async function ensureHandleWritePermission(handle) {
4852
5298
  }
4853
5299
  }
4854
5300
  async function copyProviderToContainer(src, container, pathPrefix) {
4855
- const providerV2 = getFileSystemProviderV25(src);
5301
+ const providerV2 = getFileSystemProviderV27(src);
4856
5302
  if (providerV2) {
4857
5303
  const snapshot = await providerV2.snapshot();
4858
5304
  for (const entry of snapshot.entries) {
@@ -5099,7 +5545,7 @@ function DocBlocksShell({
5099
5545
  const [activeWorkspaceDescriptor, setActiveWorkspaceDescriptor] = useState15(null);
5100
5546
  useEffect15(() => {
5101
5547
  if (!provider || getTransientWorkspace(provider.id)) return;
5102
- const providerV2 = getFileSystemProviderV25(provider);
5548
+ const providerV2 = getFileSystemProviderV27(provider);
5103
5549
  return providerV2 ? retainFileSystemProvider(providerV2) : void 0;
5104
5550
  }, [provider]);
5105
5551
  const [descriptorRefreshKey, setDescriptorRefreshKey] = useState15(0);
@@ -5186,7 +5632,7 @@ function DocBlocksShell({
5186
5632
  void scan();
5187
5633
  const scanOnFocus = () => void scan();
5188
5634
  window.addEventListener("focus", scanOnFocus);
5189
- const providerV2 = getFileSystemProviderV25(provider);
5635
+ const providerV2 = getFileSystemProviderV27(provider);
5190
5636
  const subscription = providerV2?.capabilities.watch ? providerV2.watch(
5191
5637
  (event) => {
5192
5638
  if (event.type !== "modified") void scan();
@@ -5250,6 +5696,28 @@ function DocBlocksShell({
5250
5696
  [activeWorkspaceDescriptor]
5251
5697
  );
5252
5698
  const [selectedFile, setSelectedFile] = useState15(null);
5699
+ const [selectedSourceFile, setSelectedSourceFile] = useState15(null);
5700
+ const [selectedOutsideIn, setSelectedOutsideIn] = useState15(null);
5701
+ const [selectedOutsideInEditingEnabled, setSelectedOutsideInEditingEnabled] = useState15(false);
5702
+ const adoptSelectedDocument = useCallback15((document2) => {
5703
+ setSelectedFile(document2?.displayPath ?? null);
5704
+ setSelectedSourceFile(document2?.sourcePath ?? null);
5705
+ setSelectedOutsideIn(document2?.outsideIn ?? null);
5706
+ setSelectedOutsideInEditingEnabled(document2?.outsideInEditingEnabled ?? false);
5707
+ }, []);
5708
+ const adoptRegularDocument = useCallback15((path) => {
5709
+ if (path === null) {
5710
+ setSelectedFile(null);
5711
+ setSelectedSourceFile(null);
5712
+ setSelectedOutsideIn(null);
5713
+ setSelectedOutsideInEditingEnabled(false);
5714
+ return;
5715
+ }
5716
+ setSelectedFile(path);
5717
+ setSelectedSourceFile(path);
5718
+ setSelectedOutsideIn(null);
5719
+ setSelectedOutsideInEditingEnabled(false);
5720
+ }, []);
5253
5721
  useDocumentTitle(selectedFile, homeDocumentTitle, homeDocumentPath);
5254
5722
  const exportDestinationAdapter = useMemo3(() => {
5255
5723
  if (!selectedFile) return void 0;
@@ -5264,6 +5732,10 @@ function DocBlocksShell({
5264
5732
  }
5265
5733
  return createBrowserSaveAsAdapter();
5266
5734
  }, [activeWorkspaceId, selectedFile]);
5735
+ const saveRenderedImageOutput = useMemo3(
5736
+ () => isElectronHost3() && exportDestinationAdapter ? createImageSaveOutput(exportDestinationAdapter) : void 0,
5737
+ [exportDestinationAdapter]
5738
+ );
5267
5739
  const [selectedFolder, setSelectedFolder] = useState15(null);
5268
5740
  const [folderEntries, setFolderEntries] = useState15([]);
5269
5741
  const visibleFolderEntries = useMemo3(
@@ -5300,9 +5772,12 @@ function DocBlocksShell({
5300
5772
  const preparedCloseRequestRef = useRef15(null);
5301
5773
  const pendingDbkConflictsRef = useRef15(/* @__PURE__ */ new Map());
5302
5774
  const createDocumentTarget = useCallback15(
5303
- (fsProvider, workspaceId, filePath) => {
5775
+ (fsProvider, workspaceId, filePath, outsideIn = null) => {
5776
+ if (outsideIn) {
5777
+ return createOutsideInDocumentTarget(fsProvider, outsideIn, gitScheduleRefresh);
5778
+ }
5304
5779
  const transient = getTransientWorkspace(workspaceId);
5305
- const baseTarget = createFileSystemDocumentTarget(fsProvider, filePath);
5780
+ const baseTarget = createFileSystemDocumentTarget2(fsProvider, filePath);
5306
5781
  const originKind = transient?.descriptor.origin?.kind;
5307
5782
  const needsElectronHost = originKind === "loose-file" || originKind === "dbk";
5308
5783
  if (!transient?.descriptor.origin || needsElectronHost && !isElectronHost3()) {
@@ -5506,52 +5981,58 @@ function DocBlocksShell({
5506
5981
  try {
5507
5982
  await touchWorkspace2(ws.id);
5508
5983
  if (requestId !== navigationRequestRef.current) return null;
5509
- let openedFile = null;
5510
- let openedContent = "";
5984
+ let openedDocument = null;
5511
5985
  const transitioned = await documentSession.transitionWithLoad(async () => {
5512
5986
  if (requestId !== navigationRequestRef.current) return null;
5513
5987
  if (filePath) {
5514
- const content = await readProviderText(fsProvider, filePath);
5988
+ const document2 = await loadEditableShellDocument(fsProvider, filePath);
5515
5989
  if (requestId !== navigationRequestRef.current) return null;
5516
- if (content !== null) {
5517
- openedFile = filePath;
5518
- openedContent = content;
5519
- }
5990
+ if (document2 !== null) openedDocument = document2;
5520
5991
  }
5521
5992
  return {
5522
- target: openedFile ? createDocumentTarget(fsProvider, ws.id, openedFile) : null,
5523
- content: openedContent
5993
+ target: openedDocument ? createDocumentTarget(
5994
+ fsProvider,
5995
+ ws.id,
5996
+ openedDocument.sourcePath,
5997
+ openedDocument.outsideIn
5998
+ ) : null,
5999
+ content: openedDocument?.content ?? ""
5524
6000
  };
5525
6001
  });
5526
6002
  if (!transitioned) return null;
6003
+ const acceptedDocument = openedDocument;
5527
6004
  setProvider(fsProvider);
5528
6005
  adopted = true;
5529
6006
  setActiveWorkspaceId(ws.id);
5530
- if (openedFile) {
5531
- setSelectedFile(openedFile);
6007
+ if (acceptedDocument) {
6008
+ adoptSelectedDocument(acceptedDocument);
5532
6009
  setSelectedFolder(null);
5533
6010
  setFolderEntries([]);
5534
6011
  const effectiveView = view ?? "wysiwyg";
5535
6012
  setInitialView(effectiveView);
5536
6013
  setInitialSharedMode(sharedMode);
5537
6014
  if (!transient) {
5538
- saveLastState({ workspaceId: wsId, filePath: openedFile, view: effectiveView });
6015
+ saveLastState({
6016
+ workspaceId: wsId,
6017
+ filePath: acceptedDocument.displayPath,
6018
+ view: effectiveView
6019
+ });
5539
6020
  }
5540
6021
  } else {
5541
- setSelectedFile(null);
6022
+ adoptSelectedDocument(null);
5542
6023
  setSelectedFolder(null);
5543
6024
  setFolderEntries([]);
5544
6025
  setInitialSharedMode(null);
5545
6026
  }
5546
6027
  if (push) {
5547
- pushHash(ws.id, openedFile);
6028
+ pushHash(ws.id, acceptedDocument?.displayPath ?? null);
5548
6029
  }
5549
6030
  return fsProvider;
5550
6031
  } finally {
5551
- if (!transient && !adopted) await getFileSystemProviderV25(fsProvider)?.dispose();
6032
+ if (!transient && !adopted) await getFileSystemProviderV27(fsProvider)?.dispose();
5552
6033
  }
5553
6034
  },
5554
- [createDocumentTarget, documentSession, pushHash]
6035
+ [adoptSelectedDocument, createDocumentTarget, documentSession, pushHash]
5555
6036
  );
5556
6037
  const adoptTransientWorkspace = useCallback15(
5557
6038
  async (options) => {
@@ -5594,7 +6075,7 @@ function DocBlocksShell({
5594
6075
  });
5595
6076
  mem.replaceContents(snapshot);
5596
6077
  } catch (error) {
5597
- await getFileSystemProviderV25(mem)?.dispose();
6078
+ await getFileSystemProviderV27(mem)?.dispose();
5598
6079
  throw error;
5599
6080
  }
5600
6081
  return adoptTransientWorkspace({
@@ -5622,7 +6103,7 @@ function DocBlocksShell({
5622
6103
  if (content !== null && isCurrent()) {
5623
6104
  await documentSession.transitionTo(createDocumentTarget(fs, fs.id, aboutPath), content);
5624
6105
  if (!isCurrent()) return;
5625
- setSelectedFile(aboutPath);
6106
+ adoptRegularDocument(aboutPath);
5626
6107
  setInitialView("preview");
5627
6108
  setInitialSharedMode(null);
5628
6109
  setExplorerKey((k) => k + 1);
@@ -5639,7 +6120,7 @@ function DocBlocksShell({
5639
6120
  try {
5640
6121
  await writeProviderText(fs, welcomePath, welcomeContent, "create");
5641
6122
  } catch (error) {
5642
- if (!(error instanceof FsError5 && error.code === "already-exists")) throw error;
6123
+ if (!(error instanceof FsError6 && error.code === "already-exists")) throw error;
5643
6124
  const existing = await readProviderText(fs, welcomePath);
5644
6125
  if (existing === null) throw error;
5645
6126
  seededContent = existing;
@@ -5650,7 +6131,7 @@ function DocBlocksShell({
5650
6131
  seededContent
5651
6132
  );
5652
6133
  if (!isCurrent()) return;
5653
- setSelectedFile(welcomePath);
6134
+ adoptRegularDocument(welcomePath);
5654
6135
  setInitialView("preview");
5655
6136
  setInitialSharedMode(null);
5656
6137
  setExplorerKey((k) => k + 1);
@@ -5658,7 +6139,7 @@ function DocBlocksShell({
5658
6139
  saveLastState({ workspaceId: fs.id, filePath: welcomePath, view: "preview" });
5659
6140
  if (!isWelcomeGatewayDismissed()) setShowWelcomeGateway(true);
5660
6141
  },
5661
- [createDocumentTarget, documentSession, pushHash]
6142
+ [adoptRegularDocument, createDocumentTarget, documentSession, pushHash]
5662
6143
  );
5663
6144
  const startupOpenFromIdsRef = useRef15(openFromIds);
5664
6145
  startupOpenFromIdsRef.current = openFromIds;
@@ -5784,12 +6265,12 @@ function DocBlocksShell({
5784
6265
  const p = await createElectronProviderFromWorkspace(ws);
5785
6266
  if (!p) continue;
5786
6267
  if (!isCurrent()) {
5787
- await getFileSystemProviderV25(p)?.dispose();
6268
+ await getFileSystemProviderV27(p)?.dispose();
5788
6269
  return;
5789
6270
  }
5790
6271
  await touchWorkspace2(ws.id);
5791
6272
  if (!isCurrent()) {
5792
- await getFileSystemProviderV25(p)?.dispose();
6273
+ await getFileSystemProviderV27(p)?.dispose();
5793
6274
  return;
5794
6275
  }
5795
6276
  fsProvider = p;
@@ -5798,12 +6279,12 @@ function DocBlocksShell({
5798
6279
  const restored = await (await loadNativeFileSystem()).restoreNativeFolder(ws.id);
5799
6280
  if (restored) {
5800
6281
  if (!isCurrent()) {
5801
- await getFileSystemProviderV25(restored)?.dispose();
6282
+ await getFileSystemProviderV27(restored)?.dispose();
5802
6283
  return;
5803
6284
  }
5804
6285
  await touchWorkspace2(ws.id);
5805
6286
  if (!isCurrent()) {
5806
- await getFileSystemProviderV25(restored)?.dispose();
6287
+ await getFileSystemProviderV27(restored)?.dispose();
5807
6288
  return;
5808
6289
  }
5809
6290
  fsProvider = restored;
@@ -5812,12 +6293,12 @@ function DocBlocksShell({
5812
6293
  } else {
5813
6294
  const p = await createIndexedDbFileSystemProvider(ws.id, ws.name);
5814
6295
  if (!isCurrent()) {
5815
- await getFileSystemProviderV25(p)?.dispose();
6296
+ await getFileSystemProviderV27(p)?.dispose();
5816
6297
  return;
5817
6298
  }
5818
6299
  await touchWorkspace2(ws.id);
5819
6300
  if (!isCurrent()) {
5820
- await getFileSystemProviderV25(p)?.dispose();
6301
+ await getFileSystemProviderV27(p)?.dispose();
5821
6302
  return;
5822
6303
  }
5823
6304
  fsProvider = p;
@@ -5839,7 +6320,7 @@ function DocBlocksShell({
5839
6320
  if (!isCurrent()) return;
5840
6321
  const p = await createElectronFileSystemProvider(info.id, info.name, info.rootPath);
5841
6322
  if (!isCurrent()) {
5842
- await getFileSystemProviderV25(p)?.dispose();
6323
+ await getFileSystemProviderV27(p)?.dispose();
5843
6324
  return;
5844
6325
  }
5845
6326
  fsProvider = p;
@@ -5848,7 +6329,7 @@ function DocBlocksShell({
5848
6329
  if (!isCurrent()) return;
5849
6330
  const p = await createIndexedDbFileSystemProvider(defaultWs.id, defaultWs.name);
5850
6331
  if (!isCurrent()) {
5851
- await getFileSystemProviderV25(p)?.dispose();
6332
+ await getFileSystemProviderV27(p)?.dispose();
5852
6333
  return;
5853
6334
  }
5854
6335
  fsProvider = p;
@@ -5864,7 +6345,7 @@ function DocBlocksShell({
5864
6345
  setActiveWorkspaceId(fsProvider.id);
5865
6346
  adopted = true;
5866
6347
  } finally {
5867
- if (!adopted) await getFileSystemProviderV25(fsProvider)?.dispose();
6348
+ if (!adopted) await getFileSystemProviderV27(fsProvider)?.dispose();
5868
6349
  }
5869
6350
  })().catch(() => {
5870
6351
  if (isCurrent()) {
@@ -6047,12 +6528,21 @@ function DocBlocksShell({
6047
6528
  setVersionsContainer(null);
6048
6529
  return;
6049
6530
  }
6531
+ if (selectedOutsideIn) {
6532
+ const container2 = createOutsideInContentContainer(provider, selectedOutsideIn);
6533
+ const mp2 = createMediaProviderFromContainer(container2);
6534
+ mediaContainerRef.current = container2;
6535
+ versionsContainerRef.current = container2;
6536
+ setMediaProvider(mp2);
6537
+ setVersionsContainer(container2);
6538
+ return () => mp2.dispose();
6539
+ }
6050
6540
  const parentDir = dirnameOf(selectedFile);
6051
6541
  const base = basenameOf(selectedFile);
6052
6542
  const baseNoExt = base.replace(/\.[^.]+$/, "");
6053
- const container = new FileSystemContentContainer(provider, parentDir);
6543
+ const container = new FileSystemContentContainer2(provider, parentDir);
6054
6544
  const vPrefix = parentDir ? `${parentDir}/${baseNoExt}_files` : `${baseNoExt}_files`;
6055
- const vContainer = new FileSystemContentContainer(provider, vPrefix);
6545
+ const vContainer = new FileSystemContentContainer2(provider, vPrefix);
6056
6546
  const mp = createFileMediaProvider(container, base);
6057
6547
  mediaContainerRef.current = container;
6058
6548
  versionsContainerRef.current = vContainer;
@@ -6061,8 +6551,12 @@ function DocBlocksShell({
6061
6551
  return () => {
6062
6552
  mp.dispose();
6063
6553
  };
6064
- }, [provider, selectedFile, mediaEpoch]);
6065
- const documentLinkProvider = useDocumentLinkProvider(provider, selectedFile, documentLinkEpoch);
6554
+ }, [provider, selectedFile, selectedOutsideIn, mediaEpoch]);
6555
+ const documentLinkProvider = useDocumentLinkProvider(
6556
+ provider,
6557
+ selectedSourceFile ?? selectedFile,
6558
+ documentLinkEpoch
6559
+ );
6066
6560
  useEffect15(() => {
6067
6561
  const ref = versioningRef;
6068
6562
  if (!ref) return;
@@ -6083,10 +6577,10 @@ function DocBlocksShell({
6083
6577
  }, [versioningRef, effectiveVersioning, versionBasename, selectedFile, versionsContainer]);
6084
6578
  useEffect15(() => {
6085
6579
  if (!provider) return;
6086
- const providerV2 = getFileSystemProviderV25(provider);
6580
+ const providerV2 = getFileSystemProviderV27(provider);
6087
6581
  if (!providerV2?.capabilities.watch) return;
6088
- if (!selectedFile || !documentSnapshot.targetKey) return;
6089
- const watchedFile = selectedFile;
6582
+ const watchedFile = selectedSourceFile ?? selectedFile;
6583
+ if (!watchedFile || !documentSnapshot.targetKey) return;
6090
6584
  const targetKey = documentSnapshot.targetKey;
6091
6585
  let disposed = false;
6092
6586
  let reading = false;
@@ -6140,7 +6634,7 @@ function DocBlocksShell({
6140
6634
  disposed = true;
6141
6635
  void subscription.dispose();
6142
6636
  };
6143
- }, [provider, selectedFile, documentSession, documentSnapshot.targetKey]);
6637
+ }, [provider, selectedFile, selectedSourceFile, documentSession, documentSnapshot.targetKey]);
6144
6638
  const transitionAwayFromDocument = useCallback15(
6145
6639
  async (requestId) => {
6146
6640
  if (requestId !== navigationRequestRef.current) return false;
@@ -6173,7 +6667,7 @@ function DocBlocksShell({
6173
6667
  }
6174
6668
  const snapshot = await documentSession.resolveConflict("use-external");
6175
6669
  if (!snapshot.targetKey) {
6176
- setSelectedFile(null);
6670
+ adoptSelectedDocument(null);
6177
6671
  if (activeWorkspaceId) pushHash(activeWorkspaceId, null);
6178
6672
  }
6179
6673
  } catch (error) {
@@ -6182,7 +6676,7 @@ function DocBlocksShell({
6182
6676
  message: error instanceof Error ? error.message : "Could not reload the external document."
6183
6677
  });
6184
6678
  }
6185
- }, [activeWorkspaceId, documentSession, pushHash]);
6679
+ }, [activeWorkspaceId, adoptSelectedDocument, documentSession, pushHash]);
6186
6680
  const handleKeepLocalDocument = useCallback15(async () => {
6187
6681
  const conflictKey = documentSession.getSnapshot().conflict?.targetKey;
6188
6682
  try {
@@ -6274,12 +6768,12 @@ function DocBlocksShell({
6274
6768
  if (!await transitionAwayFromDocument(requestId)) return;
6275
6769
  setProvider(nextProvider);
6276
6770
  setActiveWorkspaceId(ws.id);
6277
- setSelectedFile(null);
6771
+ adoptSelectedDocument(null);
6278
6772
  setSelectedFolder(null);
6279
6773
  setFolderEntries([]);
6280
6774
  pushHash(ws.id, null);
6281
6775
  },
6282
- [pushHash, transitionAwayFromDocument]
6776
+ [adoptSelectedDocument, pushHash, transitionAwayFromDocument]
6283
6777
  );
6284
6778
  const unpinDocument = useCallback15((document2) => {
6285
6779
  const key = pinnedDocumentKey(document2);
@@ -6307,7 +6801,7 @@ function DocBlocksShell({
6307
6801
  const handleTogglePin = useCallback15(
6308
6802
  (path) => {
6309
6803
  if (!activeWorkspaceId || !provider) return;
6310
- const canonicalPath = parseWorkspacePath5(path);
6804
+ const canonicalPath = parseWorkspacePath7(path);
6311
6805
  if (!canonicalPath) return;
6312
6806
  const document2 = {
6313
6807
  workspaceId: activeWorkspaceId,
@@ -6397,17 +6891,23 @@ function DocBlocksShell({
6397
6891
  return;
6398
6892
  }
6399
6893
  let disappearedDuringRead = false;
6894
+ let openedDocument = null;
6400
6895
  const transitioned = await documentSession.transitionWithLoad(async () => {
6401
6896
  if (requestId !== navigationRequestRef.current) return null;
6402
- const content = await readProviderText(openedProvider, document2.path);
6403
- if (content === null) {
6897
+ openedDocument = await loadEditableShellDocument(openedProvider, document2.path);
6898
+ if (openedDocument === null) {
6404
6899
  disappearedDuringRead = true;
6405
6900
  return null;
6406
6901
  }
6407
6902
  if (requestId !== navigationRequestRef.current) return null;
6408
6903
  return {
6409
- target: createDocumentTarget(openedProvider, workspace.id, document2.path),
6410
- content
6904
+ target: createDocumentTarget(
6905
+ openedProvider,
6906
+ workspace.id,
6907
+ openedDocument.sourcePath,
6908
+ openedDocument.outsideIn
6909
+ ),
6910
+ content: openedDocument.content
6411
6911
  };
6412
6912
  });
6413
6913
  if (!transitioned) {
@@ -6419,7 +6919,8 @@ function DocBlocksShell({
6419
6919
  setProvider(openedProvider);
6420
6920
  setActiveWorkspaceId(workspace.id);
6421
6921
  setActiveWorkspaceDescriptor(workspace);
6422
- setSelectedFile(document2.path);
6922
+ if (!openedDocument) return;
6923
+ adoptSelectedDocument(openedDocument);
6423
6924
  setSelectedFolder(null);
6424
6925
  setFolderEntries([]);
6425
6926
  setInitialView("wysiwyg");
@@ -6439,7 +6940,7 @@ function DocBlocksShell({
6439
6940
  } finally {
6440
6941
  if (nextProvider && ownsNextProvider && !adoptedNextProvider) {
6441
6942
  try {
6442
- await getFileSystemProviderV25(nextProvider)?.dispose();
6943
+ await getFileSystemProviderV27(nextProvider)?.dispose();
6443
6944
  } catch {
6444
6945
  }
6445
6946
  }
@@ -6447,6 +6948,7 @@ function DocBlocksShell({
6447
6948
  },
6448
6949
  [
6449
6950
  activeWorkspaceId,
6951
+ adoptSelectedDocument,
6450
6952
  closeWelcomeGateway,
6451
6953
  confirmMissingPinnedDocument,
6452
6954
  createDocumentTarget,
@@ -6513,7 +7015,7 @@ function DocBlocksShell({
6513
7015
  setProvider(openedDestination);
6514
7016
  setActiveWorkspaceId(destination.id);
6515
7017
  setActiveWorkspaceDescriptor(destination);
6516
- setSelectedFile(selectedFile);
7018
+ adoptRegularDocument(selectedFile);
6517
7019
  setPinnedDocuments(
6518
7020
  (current) => movePinnedDocumentsToWorkspace(current, activeWorkspaceId, {
6519
7021
  workspaceId: destination.id,
@@ -6550,7 +7052,7 @@ function DocBlocksShell({
6550
7052
  } catch (error) {
6551
7053
  if (destinationProvider && !adoptedDestination) {
6552
7054
  try {
6553
- await getFileSystemProviderV25(destinationProvider)?.dispose();
7055
+ await getFileSystemProviderV27(destinationProvider)?.dispose();
6554
7056
  } catch {
6555
7057
  }
6556
7058
  }
@@ -6559,6 +7061,7 @@ function DocBlocksShell({
6559
7061
  },
6560
7062
  [
6561
7063
  activeWorkspaceId,
7064
+ adoptRegularDocument,
6562
7065
  closeWelcomeGateway,
6563
7066
  createDocumentTarget,
6564
7067
  documentSession,
@@ -6590,7 +7093,7 @@ function DocBlocksShell({
6590
7093
  if (!await transitionAwayFromDocument(requestId)) return;
6591
7094
  setProvider(provider2);
6592
7095
  setActiveWorkspaceId(descriptor2.id);
6593
- setSelectedFile(null);
7096
+ adoptSelectedDocument(null);
6594
7097
  setSelectedFolder(null);
6595
7098
  setFolderEntries([]);
6596
7099
  pushHash(descriptor2.id, null);
@@ -6609,13 +7112,13 @@ function DocBlocksShell({
6609
7112
  if (!await transitionAwayFromDocument(requestId)) return;
6610
7113
  setProvider(nativeProvider);
6611
7114
  setActiveWorkspaceId(descriptor.id);
6612
- setSelectedFile(null);
7115
+ adoptSelectedDocument(null);
6613
7116
  setSelectedFolder(null);
6614
7117
  setFolderEntries([]);
6615
7118
  pushHash(descriptor.id, null);
6616
7119
  } catch {
6617
7120
  }
6618
- }, [pushHash, transitionAwayFromDocument]);
7121
+ }, [adoptSelectedDocument, pushHash, transitionAwayFromDocument]);
6619
7122
  const handleWorkspaceCloned = useCallback15(
6620
7123
  (info) => {
6621
7124
  const requestId = ++navigationRequestRef.current;
@@ -6637,13 +7140,13 @@ function DocBlocksShell({
6637
7140
  if (!await transitionAwayFromDocument(requestId)) return;
6638
7141
  setProvider(cloneProvider);
6639
7142
  setActiveWorkspaceId(descriptor.id);
6640
- setSelectedFile(null);
7143
+ adoptSelectedDocument(null);
6641
7144
  setSelectedFolder(null);
6642
7145
  setFolderEntries([]);
6643
7146
  pushHash(descriptor.id, null);
6644
7147
  })();
6645
7148
  },
6646
- [pushHash, transitionAwayFromDocument]
7149
+ [adoptSelectedDocument, pushHash, transitionAwayFromDocument]
6647
7150
  );
6648
7151
  const handleNewFile = useCallback15(async () => {
6649
7152
  if (!provider) return;
@@ -6661,12 +7164,12 @@ function DocBlocksShell({
6661
7164
  try {
6662
7165
  const transitioned = await documentSession.transitionWithLoad(async () => {
6663
7166
  if (requestId !== navigationRequestRef.current) return null;
6664
- const providerV2 = getFileSystemProviderV25(provider);
7167
+ const providerV2 = getFileSystemProviderV27(provider);
6665
7168
  if (providerV2) {
6666
7169
  try {
6667
7170
  await writeProviderText(provider, path, content, "create");
6668
7171
  } catch (error) {
6669
- if (error instanceof FsError5 && error.code === "already-exists") {
7172
+ if (error instanceof FsError6 && error.code === "already-exists") {
6670
7173
  throw new Error("A document with that name already exists.");
6671
7174
  }
6672
7175
  throw error;
@@ -6693,7 +7196,7 @@ function DocBlocksShell({
6693
7196
  showToast("error", error instanceof Error ? error.message : "Could not create the document.");
6694
7197
  return;
6695
7198
  }
6696
- setSelectedFile(path);
7199
+ adoptRegularDocument(path);
6697
7200
  setInitialView("wysiwyg");
6698
7201
  setInitialSharedMode(null);
6699
7202
  setExplorerKey((k) => k + 1);
@@ -6703,6 +7206,7 @@ function DocBlocksShell({
6703
7206
  }
6704
7207
  }, [
6705
7208
  provider,
7209
+ adoptRegularDocument,
6706
7210
  activeWorkspaceId,
6707
7211
  pushHash,
6708
7212
  closeWelcomeGateway,
@@ -6727,9 +7231,9 @@ function DocBlocksShell({
6727
7231
  }
6728
7232
  const path = `/${folderName}`;
6729
7233
  try {
6730
- const providerV2 = getFileSystemProviderV25(provider);
7234
+ const providerV2 = getFileSystemProviderV27(provider);
6731
7235
  if (providerV2) {
6732
- await providerV2.createDirectory(parseWorkspacePath5(path), { mode: "create" });
7236
+ await providerV2.createDirectory(parseWorkspacePath7(path), { mode: "create" });
6733
7237
  } else {
6734
7238
  if (await providerEntryExists(provider, path)) {
6735
7239
  throw new Error("A file or folder with that name already exists.");
@@ -6737,11 +7241,11 @@ function DocBlocksShell({
6737
7241
  await provider.createDirectory(path);
6738
7242
  }
6739
7243
  } catch (error) {
6740
- 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.";
7244
+ 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.";
6741
7245
  showToast("error", message);
6742
7246
  return;
6743
7247
  }
6744
- setSelectedFile(null);
7248
+ adoptSelectedDocument(null);
6745
7249
  setSelectedFolder(path);
6746
7250
  setFolderEntries([]);
6747
7251
  setExplorerKey((key) => key + 1);
@@ -6750,6 +7254,7 @@ function DocBlocksShell({
6750
7254
  if (effectiveCompact) setMobileShowEditor(true);
6751
7255
  }, [
6752
7256
  provider,
7257
+ adoptSelectedDocument,
6753
7258
  promptForText,
6754
7259
  showToast,
6755
7260
  closeWelcomeGateway,
@@ -6851,22 +7356,27 @@ function DocBlocksShell({
6851
7356
  if (kind === "directory") {
6852
7357
  if (!await transitionAwayFromDocument(requestId)) return;
6853
7358
  if (requestId !== navigationRequestRef.current) return;
6854
- setSelectedFile(null);
7359
+ adoptSelectedDocument(null);
6855
7360
  setSelectedFolder(path);
6856
7361
  const entries = await readProviderDirectory2(provider, path);
6857
7362
  if (requestId !== navigationRequestRef.current) return;
6858
7363
  setFolderEntries(entries);
6859
7364
  pushHash(activeWorkspaceId, null);
6860
7365
  } else {
6861
- let content = null;
7366
+ let openedDocument = null;
6862
7367
  try {
6863
7368
  const transitioned = await documentSession.transitionWithLoad(async () => {
6864
7369
  if (requestId !== navigationRequestRef.current) return null;
6865
- content = await readProviderText(provider, path);
6866
- if (requestId !== navigationRequestRef.current || content === null) return null;
7370
+ openedDocument = await loadEditableShellDocument(provider, path);
7371
+ if (requestId !== navigationRequestRef.current || openedDocument === null) return null;
6867
7372
  return {
6868
- target: createDocumentTarget(provider, activeWorkspaceId, path),
6869
- content
7373
+ target: createDocumentTarget(
7374
+ provider,
7375
+ activeWorkspaceId,
7376
+ openedDocument.sourcePath,
7377
+ openedDocument.outsideIn
7378
+ ),
7379
+ content: openedDocument.content
6870
7380
  };
6871
7381
  });
6872
7382
  if (!transitioned) return;
@@ -6878,7 +7388,8 @@ function DocBlocksShell({
6878
7388
  return;
6879
7389
  }
6880
7390
  if (requestId !== navigationRequestRef.current) return;
6881
- setSelectedFile(path);
7391
+ if (!openedDocument) return;
7392
+ adoptSelectedDocument(openedDocument);
6882
7393
  setSelectedFolder(null);
6883
7394
  setFolderEntries([]);
6884
7395
  setInitialView("wysiwyg");
@@ -6895,15 +7406,84 @@ function DocBlocksShell({
6895
7406
  pushHash,
6896
7407
  effectiveCompact,
6897
7408
  closeWelcomeGateway,
7409
+ adoptSelectedDocument,
6898
7410
  createDocumentTarget,
6899
7411
  documentSession,
6900
7412
  transitionAwayFromDocument,
6901
7413
  showToast
6902
7414
  ]
6903
7415
  );
7416
+ const handleEnableOutsideInEditing = useCallback15(
7417
+ async (path) => {
7418
+ if (!provider || !activeWorkspaceId) {
7419
+ throw new Error("Open the workspace before enabling Markdown editing.");
7420
+ }
7421
+ const requestId = ++navigationRequestRef.current;
7422
+ const editableRef = { current: null };
7423
+ const transitioned = await documentSession.transitionWithLoad(async () => {
7424
+ if (requestId !== navigationRequestRef.current) return null;
7425
+ const opened = await loadEditableShellDocument(provider, path);
7426
+ if (!opened?.outsideIn) {
7427
+ throw new Error("This file does not support outside-in Markdown editing.");
7428
+ }
7429
+ const editable2 = await enableOutsideInMarkdownEditing(provider, opened);
7430
+ editableRef.current = editable2;
7431
+ return {
7432
+ target: createDocumentTarget(
7433
+ provider,
7434
+ activeWorkspaceId,
7435
+ editable2.sourcePath,
7436
+ editable2.outsideIn
7437
+ ),
7438
+ content: editable2.content
7439
+ };
7440
+ });
7441
+ const editable = editableRef.current;
7442
+ if (!transitioned || !editable || requestId !== navigationRequestRef.current) return;
7443
+ adoptSelectedDocument(editable);
7444
+ setSelectedFolder(null);
7445
+ setFolderEntries([]);
7446
+ setInitialView("wysiwyg");
7447
+ setInitialSharedMode(null);
7448
+ closeWelcomeGateway();
7449
+ pushHash(activeWorkspaceId, path);
7450
+ saveLastState({ workspaceId: activeWorkspaceId, filePath: path, view: "wysiwyg" });
7451
+ if (effectiveCompact) setMobileShowEditor(true);
7452
+ showToast(
7453
+ "success",
7454
+ `Markdown editing enabled. The original is backed up at ${editable.outsideIn.backupPath}.`
7455
+ );
7456
+ },
7457
+ [
7458
+ activeWorkspaceId,
7459
+ adoptSelectedDocument,
7460
+ closeWelcomeGateway,
7461
+ createDocumentTarget,
7462
+ documentSession,
7463
+ effectiveCompact,
7464
+ provider,
7465
+ pushHash,
7466
+ showToast
7467
+ ]
7468
+ );
7469
+ const outsideInActionsForEntry = useCallback15(
7470
+ (entry) => {
7471
+ if (entry.kind !== "file" || resolveOutsideInLayout(entry.path) === null) return [];
7472
+ if (selectedOutsideInEditingEnabled && selectedFile !== null && sameProviderPath(selectedFile, entry.path)) {
7473
+ return [];
7474
+ }
7475
+ return [
7476
+ {
7477
+ label: "Allow editing via markdown",
7478
+ onSelect: () => handleEnableOutsideInEditing(entry.path)
7479
+ }
7480
+ ];
7481
+ },
7482
+ [handleEnableOutsideInEditing, selectedFile, selectedOutsideInEditingEnabled]
7483
+ );
6904
7484
  const handleEditorLinkClick = useCallback15(
6905
7485
  (href) => {
6906
- const target = resolveShellEditorLinkTarget(href, selectedFile);
7486
+ const target = resolveShellEditorLinkTarget(href, selectedSourceFile ?? selectedFile);
6907
7487
  if (!target) {
6908
7488
  showToast(
6909
7489
  "error",
@@ -6949,31 +7529,93 @@ function DocBlocksShell({
6949
7529
  })();
6950
7530
  return true;
6951
7531
  },
6952
- [handleSelect, provider, selectedFile, showToast]
7532
+ [handleSelect, provider, selectedFile, selectedSourceFile, showToast]
6953
7533
  );
6954
7534
  const handleTreeMutation = useCallback15(
6955
7535
  async (change, mutate) => {
6956
- if (!provider || !activeWorkspaceId || !selectedFile) {
7536
+ if (!provider) {
6957
7537
  await mutate();
6958
7538
  return;
6959
7539
  }
7540
+ let mutateDocument = mutate;
7541
+ if (change.kind === "file") {
7542
+ const oldLayout = resolveOutsideInLayout(
7543
+ change.type === "move" ? change.oldPath : change.path
7544
+ );
7545
+ if (oldLayout && change.type === "move") {
7546
+ const nextLayout = resolveOutsideInLayout(change.newPath);
7547
+ if (!nextLayout || nextLayout.format !== oldLayout.format) {
7548
+ throw new Error(
7549
+ `Keep the .${oldLayout.format} extension when renaming this outside-in document.`
7550
+ );
7551
+ }
7552
+ }
7553
+ if (oldLayout && change.type === "delete") {
7554
+ mutateDocument = async () => {
7555
+ await mutate();
7556
+ try {
7557
+ await removeOutsideInCompanion(provider, oldLayout);
7558
+ } catch (error) {
7559
+ showToast(
7560
+ "error",
7561
+ error instanceof Error ? `The rendered file was deleted, but its companion could not be removed: ${error.message}` : "The rendered file was deleted, but its companion could not be removed."
7562
+ );
7563
+ }
7564
+ };
7565
+ }
7566
+ }
7567
+ if (!activeWorkspaceId || !selectedFile) {
7568
+ await mutateDocument();
7569
+ return;
7570
+ }
6960
7571
  if (change.type === "move") {
6961
7572
  const nextFile = relocateProviderPath(selectedFile, change.oldPath, change.newPath);
6962
7573
  if (nextFile !== selectedFile) {
6963
- await documentSession.retarget(
6964
- createDocumentTarget(provider, activeWorkspaceId, nextFile),
6965
- mutate
7574
+ const nextSource = relocateProviderPath(
7575
+ selectedSourceFile ?? selectedFile,
7576
+ change.oldPath,
7577
+ change.newPath
7578
+ );
7579
+ let nextOutsideIn = null;
7580
+ if (selectedOutsideIn) {
7581
+ const resolved = resolveOutsideInLayout(nextFile);
7582
+ if (!resolved)
7583
+ throw new Error("The outside-in target must keep a supported extension.");
7584
+ nextOutsideIn = { ...resolved, markdownPath: nextSource };
7585
+ }
7586
+ const snapshot = await documentSession.retarget(
7587
+ createDocumentTarget(provider, activeWorkspaceId, nextSource, nextOutsideIn),
7588
+ mutateDocument
6966
7589
  );
7590
+ if (nextOutsideIn) {
7591
+ const linkedContent = await withOutsideInMetadata(snapshot.content, nextOutsideIn);
7592
+ if (linkedContent !== snapshot.content && snapshot.targetKey) {
7593
+ documentSession.edit(linkedContent, {
7594
+ targetKey: snapshot.targetKey,
7595
+ generation: snapshot.generation
7596
+ });
7597
+ await documentSession.flush("transition");
7598
+ }
7599
+ }
6967
7600
  return;
6968
7601
  }
6969
7602
  }
6970
7603
  if (change.type === "delete" && pathContains(change.path, selectedFile)) {
6971
- await documentSession.delete(mutate);
7604
+ await documentSession.delete(mutateDocument);
6972
7605
  return;
6973
7606
  }
6974
- await mutate();
7607
+ await mutateDocument();
6975
7608
  },
6976
- [provider, activeWorkspaceId, selectedFile, documentSession, createDocumentTarget]
7609
+ [
7610
+ provider,
7611
+ activeWorkspaceId,
7612
+ selectedFile,
7613
+ selectedSourceFile,
7614
+ selectedOutsideIn,
7615
+ documentSession,
7616
+ createDocumentTarget,
7617
+ showToast
7618
+ ]
6977
7619
  );
6978
7620
  const handleTreeChange = useCallback15(
6979
7621
  async (change) => {
@@ -6988,7 +7630,12 @@ function DocBlocksShell({
6988
7630
  const nextFile = selectedFile ? relocateProviderPath(selectedFile, change.oldPath, change.newPath) : null;
6989
7631
  const nextFolder = selectedFolder ? relocateProviderPath(selectedFolder, change.oldPath, change.newPath) : null;
6990
7632
  if (nextFile !== selectedFile) {
6991
- setSelectedFile(nextFile);
7633
+ if (nextFile && selectedOutsideIn) {
7634
+ const opened = await loadEditableShellDocument(provider, nextFile);
7635
+ adoptSelectedDocument(opened);
7636
+ } else {
7637
+ adoptRegularDocument(nextFile);
7638
+ }
6992
7639
  if (activeWorkspaceId) {
6993
7640
  pushHash(activeWorkspaceId, nextFile);
6994
7641
  if (nextFile) {
@@ -7014,7 +7661,7 @@ function DocBlocksShell({
7014
7661
  if (selectedFile) {
7015
7662
  const exists = await providerEntryExists(provider, selectedFile);
7016
7663
  if (!exists) {
7017
- setSelectedFile(null);
7664
+ adoptSelectedDocument(null);
7018
7665
  if (activeWorkspaceId) pushHash(activeWorkspaceId, null);
7019
7666
  }
7020
7667
  }
@@ -7025,7 +7672,10 @@ function DocBlocksShell({
7025
7672
  },
7026
7673
  [
7027
7674
  provider,
7675
+ adoptRegularDocument,
7676
+ adoptSelectedDocument,
7028
7677
  selectedFile,
7678
+ selectedOutsideIn,
7029
7679
  selectedFolder,
7030
7680
  activeWorkspaceId,
7031
7681
  pinnedDocuments,
@@ -7097,7 +7747,7 @@ function DocBlocksShell({
7097
7747
  const parent = dirnameOf(document2.path);
7098
7748
  let newPath;
7099
7749
  try {
7100
- newPath = parseWorkspacePath5(parent ? `${parent}/${newName}` : newName);
7750
+ newPath = parseWorkspacePath7(parent ? `${parent}/${newName}` : newName);
7101
7751
  } catch {
7102
7752
  showToast("error", "Use a valid document name.");
7103
7753
  return;
@@ -7139,7 +7789,7 @@ function DocBlocksShell({
7139
7789
  } finally {
7140
7790
  if (access.owned) {
7141
7791
  try {
7142
- await getFileSystemProviderV25(access.provider)?.dispose();
7792
+ await getFileSystemProviderV27(access.provider)?.dispose();
7143
7793
  } catch {
7144
7794
  }
7145
7795
  }
@@ -7208,7 +7858,7 @@ function DocBlocksShell({
7208
7858
  } finally {
7209
7859
  if (access.owned) {
7210
7860
  try {
7211
- await getFileSystemProviderV25(access.provider)?.dispose();
7861
+ await getFileSystemProviderV27(access.provider)?.dispose();
7212
7862
  } catch {
7213
7863
  }
7214
7864
  }
@@ -7226,42 +7876,15 @@ function DocBlocksShell({
7226
7876
  showToast
7227
7877
  ]
7228
7878
  );
7229
- const persistImportedMedia = useCallback15(
7230
- async (source, target, importedMarkdownPath) => {
7231
- const parentDir = dirnameOf(importedMarkdownPath);
7232
- const folder = basenameOf(importedMarkdownPath).replace(/\.[^.]+$/, "") + "_files";
7233
- const mediaRoot = parentDir ? `${parentDir}/${folder}` : folder;
7234
- const entries = await source.listFiles();
7235
- const targetV2 = getFileSystemProviderV25(target);
7236
- for (const entry of entries) {
7237
- if (entry.path.endsWith(".md")) continue;
7238
- const data = await source.readFile(entry.path);
7239
- if (!data) continue;
7240
- const cleanPath = entry.path.replace(/^\/+/, "");
7241
- const destination = `${mediaRoot}/${cleanPath}`;
7242
- if (targetV2) {
7243
- await targetV2.writeFile(parseWorkspacePath5(destination), data, {
7244
- mode: "upsert",
7245
- createParents: true
7246
- });
7247
- } else {
7248
- await target.writeBinary(destination, data);
7249
- }
7250
- }
7251
- },
7252
- []
7253
- );
7254
7879
  const handleImportFiles = useCallback15(
7255
7880
  async (files) => {
7256
7881
  if (!provider) return;
7257
- const result = await importDroppedFiles(files, provider, {
7258
- persistMedia: (source, path) => persistImportedMedia(source, provider, path)
7259
- });
7882
+ const result = await importDroppedFiles(files, provider);
7260
7883
  setExplorerKey((k) => k + 1);
7261
7884
  const summary = summariseImport(result);
7262
7885
  if (summary) showToast(summary.kind, summary.message);
7263
7886
  },
7264
- [provider, persistImportedMedia, showToast]
7887
+ [provider, showToast]
7265
7888
  );
7266
7889
  const handleEditorChange = useCallback15(
7267
7890
  (source) => {
@@ -7308,7 +7931,7 @@ function DocBlocksShell({
7308
7931
  throw error;
7309
7932
  }
7310
7933
  if (!isCurrent()) {
7311
- await getFileSystemProviderV25(mem)?.dispose();
7934
+ await getFileSystemProviderV27(mem)?.dispose();
7312
7935
  await revokeAbandonedResource();
7313
7936
  return;
7314
7937
  }
@@ -7356,7 +7979,7 @@ function DocBlocksShell({
7356
7979
  } finally {
7357
7980
  try {
7358
7981
  if (!adoptionStarted) {
7359
- await getFileSystemProviderV25(mem)?.dispose();
7982
+ await getFileSystemProviderV27(mem)?.dispose();
7360
7983
  }
7361
7984
  } finally {
7362
7985
  if (!opened) await revokeAbandonedResource();
@@ -7426,7 +8049,7 @@ function DocBlocksShell({
7426
8049
  push: true
7427
8050
  });
7428
8051
  } finally {
7429
- if (!adoptionStarted) await getFileSystemProviderV25(mem)?.dispose();
8052
+ if (!adoptionStarted) await getFileSystemProviderV27(mem)?.dispose();
7430
8053
  }
7431
8054
  },
7432
8055
  [adoptTransientWorkspace]
@@ -7523,7 +8146,7 @@ function DocBlocksShell({
7523
8146
  try {
7524
8147
  await copyProviderToContainer(p, container, folder);
7525
8148
  } finally {
7526
- if (ownsProvider) await getFileSystemProviderV25(p)?.dispose();
8149
+ if (ownsProvider) await getFileSystemProviderV27(p)?.dispose();
7527
8150
  }
7528
8151
  }
7529
8152
  if (usedFolders.size === 0) {
@@ -7657,7 +8280,7 @@ function DocBlocksShell({
7657
8280
  const p = await createElectronFileSystemProvider(info.id, info.name, info.rootPath);
7658
8281
  setProvider(p);
7659
8282
  setActiveWorkspaceId(info.id);
7660
- setSelectedFile(null);
8283
+ adoptSelectedDocument(null);
7661
8284
  setSelectedFolder(null);
7662
8285
  setFolderEntries([]);
7663
8286
  } else {
@@ -7665,12 +8288,13 @@ function DocBlocksShell({
7665
8288
  const fsProvider = await createIndexedDbFileSystemProvider(defaultWs.id, defaultWs.name);
7666
8289
  setProvider(fsProvider);
7667
8290
  setActiveWorkspaceId(defaultWs.id);
7668
- setSelectedFile(null);
8291
+ adoptSelectedDocument(null);
7669
8292
  setSelectedFolder(null);
7670
8293
  setFolderEntries([]);
7671
8294
  }
7672
8295
  }, [
7673
8296
  activeWorkspaceId,
8297
+ adoptSelectedDocument,
7674
8298
  confirmAction,
7675
8299
  handleWorkspaceSelect,
7676
8300
  pinnedDocuments,
@@ -7807,6 +8431,7 @@ function DocBlocksShell({
7807
8431
  onPinnedDocumentDelete: handlePinnedDocumentDelete,
7808
8432
  onTogglePin: handleTogglePin,
7809
8433
  onSelect: handleSelect,
8434
+ actionsForEntry: outsideInActionsForEntry,
7810
8435
  onTreeMutation: handleTreeMutation,
7811
8436
  onTreeChange: handleTreeChange,
7812
8437
  onImportFiles: handleImportFiles,
@@ -7905,10 +8530,13 @@ function DocBlocksShell({
7905
8530
  EditorShell,
7906
8531
  {
7907
8532
  initialMarkdown: editorContent,
8533
+ readOnly: selectedOutsideIn !== null && !selectedOutsideInEditingEnabled,
7908
8534
  initialView,
7909
8535
  defaultViewportPreset: defaultPreviewViewportPreset,
7910
8536
  articleId: selectedFile,
7911
8537
  fileName: selectedFile,
8538
+ saveCoverImageOutput: saveRenderedImageOutput,
8539
+ saveDashboardImageOutput: saveRenderedImageOutput,
7912
8540
  onChange: handleEditorChange,
7913
8541
  onLinkClick: handleEditorLinkClick,
7914
8542
  colorScheme: resolvedTheme,
@@ -8079,6 +8707,9 @@ export {
8079
8707
  WorkspacePicker,
8080
8708
  WriteCanvasSettingsControls,
8081
8709
  buildExportFilename,
8710
+ createImageSaveOutput as createCoverImageSaveOutput,
8711
+ createImageSaveOutput as createDashboardImageSaveOutput,
8712
+ createImageSaveOutput,
8082
8713
  loadLastExportOptions,
8083
8714
  resolveWriteCanvasFonts,
8084
8715
  runExport,