@bendyline/docblocks-react 2.2.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  ExportDialog
3
- } from "./chunk-3CZ27FOA.js";
3
+ } from "./chunk-MHTQORMH.js";
4
4
  import "./chunk-YBEYTVU2.js";
5
5
  import "./chunk-LG6HAWCK.js";
6
6
  import "./chunk-EBWYGTN7.js";
@@ -26,7 +26,7 @@ import { getThemeSummaries } from "@bendyline/squisq/schemas";
26
26
  import { parseMarkdown } from "@bendyline/squisq/markdown";
27
27
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
28
28
  var ExportDialog = lazy(
29
- () => import("./ExportDialog-VYATD5UC.js").then((module) => ({ default: module.ExportDialog }))
29
+ () => import("./ExportDialog-HPFPXELL.js").then((module) => ({ default: module.ExportDialog }))
30
30
  );
31
31
  var ShareDialog = lazy(
32
32
  () => import("./ShareDialog-PAMOQQFB.js").then((module) => ({ default: module.ShareDialog }))
@@ -225,10 +225,6 @@ function ExportToolbarControls({
225
225
  },
226
226
  [markdownSource, videoModules]
227
227
  );
228
- const handleOpenAnimatedGifFromDialog = useCallback(() => {
229
- setDialogOpen(false);
230
- void handleOpenVideoModal("gif");
231
- }, [handleOpenVideoModal]);
232
228
  const handleCloseVideoModal = useCallback(() => {
233
229
  setVideoModalOpen(false);
234
230
  setVideoDoc(null);
@@ -438,7 +434,6 @@ function ExportToolbarControls({
438
434
  } : void 0,
439
435
  onExport: handleExport,
440
436
  onOptionsChange: destinationAdapter ? handleOptionsChange : void 0,
441
- onAnimatedGifExport: showAnimatedGifExport ? handleOpenAnimatedGifFromDialog : void 0,
442
437
  onClose: handleCloseDialog
443
438
  }
444
439
  ) }),
@@ -76,7 +76,6 @@ function ExportDialog({
76
76
  destination,
77
77
  onExport,
78
78
  onOptionsChange,
79
- onAnimatedGifExport,
80
79
  onClose
81
80
  }) {
82
81
  const [format, setFormat] = useState(initial.format);
@@ -327,23 +326,8 @@ function ExportDialog({
327
326
  }
328
327
  )
329
328
  ] }),
330
- onAnimatedGifExport && /* @__PURE__ */ jsxs("div", { className: "db-export-field", children: [
331
- /* @__PURE__ */ jsx("span", { className: "db-export-label", children: "Animated media" }),
332
- /* @__PURE__ */ jsx(
333
- "button",
334
- {
335
- type: "button",
336
- className: "db-export-btn db-export-btn--secondary",
337
- onClick: onAnimatedGifExport,
338
- children: "Animated GIF..."
339
- }
340
- ),
341
- /* @__PURE__ */ jsx("span", { className: "db-export-hint", children: "Opens timing, orientation, captions, looping, and color controls." })
342
- ] }),
343
329
  /* @__PURE__ */ jsxs("p", { className: "db-export-more-formats", children: [
344
- "Need spreadsheets, ",
345
- onAnimatedGifExport ? "" : "animated media, ",
346
- "portable bundles, or automation?",
330
+ "Need spreadsheets, animated media, portable bundles, or automation?",
347
331
  " ",
348
332
  /* @__PURE__ */ jsx("a", { href: "https://docblocks.com/cli/", target: "_blank", rel: "noopener noreferrer", children: "More formats with the DocBlocks CLI" }),
349
333
  "."
@@ -81,8 +81,6 @@ interface ExportDialogProps {
81
81
  onExport: (options: ExportOptions) => void;
82
82
  /** Called whenever the currently selected options change. */
83
83
  onOptionsChange?: (options: ExportOptions) => void;
84
- /** Opens the richer animated-GIF export flow when the host supports ffmpeg.wasm. */
85
- onAnimatedGifExport?: () => void;
86
84
  /** Called when the dialog is dismissed. */
87
85
  onClose: () => void;
88
86
  }
@@ -95,7 +93,7 @@ interface ExportDestinationControl {
95
93
  /** A host-supplied validation error for the current destination value. */
96
94
  error?: string | null;
97
95
  }
98
- declare function ExportDialog({ initial, exporting, error, destination, onExport, onOptionsChange, onAnimatedGifExport, onClose, }: ExportDialogProps): react_jsx_runtime.JSX.Element;
96
+ declare function ExportDialog({ initial, exporting, error, destination, onExport, onOptionsChange, onClose, }: ExportDialogProps): react_jsx_runtime.JSX.Element;
99
97
 
100
98
  /** Keep a user-edited target name while switching the selected export format. */
101
99
  declare function updateExportTargetExtension(targetPath: string, suggestedFilename: string): string;
@@ -3,7 +3,7 @@ import {
3
3
  } from "../chunk-BI7NVU6T.js";
4
4
  import {
5
5
  ExportDialog
6
- } from "../chunk-3CZ27FOA.js";
6
+ } from "../chunk-MHTQORMH.js";
7
7
  import {
8
8
  buildExportFilename,
9
9
  runExport
package/dist/index.d.ts CHANGED
@@ -219,7 +219,7 @@ interface DocBlocksShellProps {
219
219
  /**
220
220
  * True when a new deploy is waiting (browser PWA hosts: a fresh service
221
221
  * worker finished installing). The shell shows an "Update available"
222
- * status-bar notice that opens a Reload/Later prompt.
222
+ * status-bar notice; clicking it opens a nearby Reload/Later prompt.
223
223
  */
224
224
  updateAvailable?: boolean;
225
225
  /** Activates the waiting update and reloads onto the new version. */
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ import {
29
29
  } from "./chunk-BPGEBGAN.js";
30
30
  import {
31
31
  ExportDialog
32
- } from "./chunk-3CZ27FOA.js";
32
+ } from "./chunk-MHTQORMH.js";
33
33
  import {
34
34
  buildExportFilename,
35
35
  runExport
@@ -1704,7 +1704,7 @@ import {
1704
1704
  DocumentCommitConflictError,
1705
1705
  DocumentSessionConflictError
1706
1706
  } from "@bendyline/docblocks/document";
1707
- import { isElectronHost as isElectronHost2, getDocBlocksHost } from "@bendyline/docblocks/host";
1707
+ import { isElectronHost as isElectronHost3, getDocBlocksHost } from "@bendyline/docblocks/host";
1708
1708
  import {
1709
1709
  parseSharedDocumentHash
1710
1710
  } from "@bendyline/docblocks/share";
@@ -1723,6 +1723,7 @@ import {
1723
1723
 
1724
1724
  // src/AppMenu/AppMenu.tsx
1725
1725
  import { useState as useState5, useCallback as useCallback5, useRef as useRef5, useEffect as useEffect5 } from "react";
1726
+ import { isElectronHost as isElectronHost2 } from "@bendyline/docblocks/host";
1726
1727
  import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1727
1728
  function formatBytes(bytes) {
1728
1729
  if (!Number.isFinite(bytes) || bytes < 0) return "\u2014";
@@ -1757,6 +1758,7 @@ function AppMenu({
1757
1758
  const [showAbout, setShowAbout] = useState5(false);
1758
1759
  const [showSettings, setShowSettings] = useState5(false);
1759
1760
  const [requestingPersistentStorage, setRequestingPersistentStorage] = useState5(false);
1761
+ const moreInformationUrl = isElectronHost2() ? "https://docblocks.com/desktop/" : "https://docblocks.com/web/";
1760
1762
  const menuRef = useRef5(null);
1761
1763
  const [storageEstimate, setStorageEstimate] = useState5(null);
1762
1764
  useEffect5(() => {
@@ -1968,6 +1970,22 @@ function AppMenu({
1968
1970
  /* @__PURE__ */ jsx5("strong", { children: "DocBlocks" }),
1969
1971
  " is a local-first Markdown document editor. Your files stay under your control."
1970
1972
  ] }),
1973
+ /* @__PURE__ */ jsxs5("aside", { className: "db-about-beta", children: [
1974
+ /* @__PURE__ */ jsx5("strong", { children: "Beta Software." }),
1975
+ " We're still working through initial hiccups and issues. Please bear with us, make sure you keep backups, and",
1976
+ " ",
1977
+ /* @__PURE__ */ jsx5(
1978
+ "a",
1979
+ {
1980
+ href: "https://github.com/bendyline/docblocks/issues/new",
1981
+ target: "_blank",
1982
+ rel: "noopener noreferrer",
1983
+ children: "open issues"
1984
+ }
1985
+ ),
1986
+ " ",
1987
+ "where you find them. Thanks!"
1988
+ ] }),
1971
1989
  appVersion && /* @__PURE__ */ jsxs5("p", { className: "db-about-version", children: [
1972
1990
  /* @__PURE__ */ jsx5("span", { children: "Version" }),
1973
1991
  /* @__PURE__ */ jsx5("code", { "aria-label": `DocBlocks version ${appVersion}`, children: appVersion })
@@ -1987,6 +2005,8 @@ function AppMenu({
1987
2005
  "."
1988
2006
  ] }),
1989
2007
  /* @__PURE__ */ jsxs5("p", { className: "db-dialog-links", children: [
2008
+ /* @__PURE__ */ jsx5("a", { href: moreInformationUrl, target: "_blank", rel: "noopener noreferrer", children: "More information..." }),
2009
+ /* @__PURE__ */ jsx5("span", { className: "db-dialog-sep", children: "\xB7" }),
1990
2010
  /* @__PURE__ */ jsx5(
1991
2011
  "a",
1992
2012
  {
@@ -2260,7 +2280,7 @@ function useDocumentSession(autoSaveDelayMs = 500) {
2260
2280
  import { lazy, Suspense } from "react";
2261
2281
  import { jsx as jsx8 } from "react/jsx-runtime";
2262
2282
  var ExportToolbarControlsImplementation = lazy(
2263
- () => import("./ExportToolbarControls-C46GMPKC.js").then((module) => ({
2283
+ () => import("./ExportToolbarControls-2VB43ITO.js").then((module) => ({
2264
2284
  default: module.ExportToolbarControls
2265
2285
  }))
2266
2286
  );
@@ -2719,6 +2739,63 @@ async function decodeDbkWorkspace(bytes, options) {
2719
2739
  return snapshot;
2720
2740
  }
2721
2741
 
2742
+ // src/DocBlocksShell/editor-link-navigation.ts
2743
+ import { HOST_WIRE_LIMITS, isBoundedString, parseExternalHttpUrl } from "@bendyline/docblocks/host";
2744
+ var URI_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*:/;
2745
+ var MARKDOWN_EXTENSION = /\.(?:md|markdown|mdown)$/iu;
2746
+ function resolveShellEditorLinkTarget(href, documentPath) {
2747
+ if (!isBoundedString(href, HOST_WIRE_LIMITS.urlCharacters, 1)) return null;
2748
+ if (href.startsWith("#")) return { kind: "fragment" };
2749
+ const externalUrl = parseExternalHttpUrl(href);
2750
+ if (externalUrl) return { kind: "external", url: externalUrl };
2751
+ if (documentPath === null || href.startsWith("//") || URI_SCHEME.test(href)) return null;
2752
+ const splitAt = href.search(/[?#]/u);
2753
+ const encodedPath = splitAt === -1 ? href : href.slice(0, splitAt);
2754
+ if (!encodedPath) return null;
2755
+ let decodedPath;
2756
+ try {
2757
+ decodedPath = decodeURIComponent(encodedPath);
2758
+ } catch {
2759
+ return null;
2760
+ }
2761
+ if (decodedPath.includes("\\") || hasControlCharacter(decodedPath)) return null;
2762
+ const documentSegments = normalizeDocumentPath(documentPath);
2763
+ if (!documentSegments) return null;
2764
+ const targetSegments = decodedPath.startsWith("/") ? [] : documentSegments.slice(0, -1);
2765
+ for (const segment of decodedPath.split("/")) {
2766
+ if (segment === "" || segment === ".") continue;
2767
+ if (segment === "..") {
2768
+ if (targetSegments.length === 0) return null;
2769
+ targetSegments.pop();
2770
+ continue;
2771
+ }
2772
+ if (segment.length > 255) return null;
2773
+ targetSegments.push(segment);
2774
+ }
2775
+ const path = targetSegments.join("/");
2776
+ if (!path || path.length > HOST_WIRE_LIMITS.pathCharacters || !MARKDOWN_EXTENSION.test(path)) {
2777
+ return null;
2778
+ }
2779
+ return { kind: "workspace", path };
2780
+ }
2781
+ function normalizeDocumentPath(path) {
2782
+ if (!isBoundedString(path, HOST_WIRE_LIMITS.pathCharacters, 1)) return null;
2783
+ const segments = [];
2784
+ for (const segment of path.replace(/\\/g, "/").split("/")) {
2785
+ if (segment === "" || segment === ".") continue;
2786
+ if (segment === ".." || segment.length > 255 || hasControlCharacter(segment)) return null;
2787
+ segments.push(segment);
2788
+ }
2789
+ return segments.length > 0 ? segments : null;
2790
+ }
2791
+ function hasControlCharacter(value) {
2792
+ for (let index = 0; index < value.length; index += 1) {
2793
+ const code = value.charCodeAt(index);
2794
+ if (code <= 31 || code === 127) return true;
2795
+ }
2796
+ return false;
2797
+ }
2798
+
2722
2799
  // src/DocBlocksShell/import-files.ts
2723
2800
  import { FsError as FsError3 } from "@bendyline/docblocks/filesystem";
2724
2801
 
@@ -3214,7 +3291,7 @@ function UpdateAvailableNotice({
3214
3291
  blocked = false,
3215
3292
  statusBarVisible
3216
3293
  }) {
3217
- const [promptOpen, setPromptOpen] = useState13(true);
3294
+ const [promptOpen, setPromptOpen] = useState13(false);
3218
3295
  if (!available || !onApplyUpdate) return null;
3219
3296
  const promptVisible = promptOpen && !blocked;
3220
3297
  return /* @__PURE__ */ jsxs10(Fragment6, { children: [
@@ -3232,13 +3309,21 @@ function UpdateAvailableNotice({
3232
3309
  children: "Update available"
3233
3310
  }
3234
3311
  ),
3235
- promptVisible && /* @__PURE__ */ jsxs10("div", { id: UPDATE_PROMPT_ID, className: "db-update-banner", role: "alert", children: [
3236
- /* @__PURE__ */ jsx11("span", { children: "A new version of DocBlocks is available. Reload to update the editor and site pages." }),
3237
- /* @__PURE__ */ jsxs10("div", { className: "db-update-banner-actions", children: [
3238
- /* @__PURE__ */ jsx11("button", { type: "button", onClick: onApplyUpdate, disabled: blocked, children: "Reload" }),
3239
- /* @__PURE__ */ jsx11("button", { type: "button", onClick: () => setPromptOpen(false), children: "Later" })
3240
- ] })
3241
- ] })
3312
+ promptVisible && /* @__PURE__ */ jsxs10(
3313
+ "div",
3314
+ {
3315
+ id: UPDATE_PROMPT_ID,
3316
+ className: `db-update-banner${statusBarVisible ? "" : " db-update-banner--floating"}`,
3317
+ role: "alert",
3318
+ children: [
3319
+ /* @__PURE__ */ jsx11("span", { children: "A new version of DocBlocks is available. Reload to update the editor and site pages." }),
3320
+ /* @__PURE__ */ jsxs10("div", { className: "db-update-banner-actions", children: [
3321
+ /* @__PURE__ */ jsx11("button", { type: "button", onClick: onApplyUpdate, disabled: blocked, children: "Reload" }),
3322
+ /* @__PURE__ */ jsx11("button", { type: "button", onClick: () => setPromptOpen(false), children: "Later" })
3323
+ ] })
3324
+ ]
3325
+ }
3326
+ )
3242
3327
  ] });
3243
3328
  }
3244
3329
 
@@ -3549,6 +3634,91 @@ async function copyTransientWorkspaceContents(source, destination) {
3549
3634
  }
3550
3635
  }
3551
3636
 
3637
+ // src/DocBlocksShell/welcome-document.ts
3638
+ var WELCOME_DOCUMENT_PATH = "/aboutDocBlocks.md";
3639
+ var WELCOME_DOCUMENT_CONTENT = [
3640
+ "---",
3641
+ "squisq-theme: warm-earth",
3642
+ "squisq-cover-slide: false",
3643
+ "---",
3644
+ "",
3645
+ '# DocBlocks: one Markdown file, many finished forms {[title subtitle="Local-first writing for pages, documents, slideshows, and video"]}',
3646
+ "",
3647
+ "Write in plain Markdown. Shape it visually. Keep it portable.",
3648
+ "",
3649
+ "## A document editor that never asks you to stop owning the document {[factCard]}",
3650
+ "",
3651
+ "DocBlocks pairs a friendly visual writing surface with the durable source format you already know. There is no account to create, no proprietary document to rescue later, and no requirement to send your work to a hosted editor.",
3652
+ "",
3653
+ "## One source, four ways to publish {[diagram]}",
3654
+ "",
3655
+ "```diagram",
3656
+ "+------------+",
3657
+ "| Markdown |",
3658
+ "+-----+------+",
3659
+ " |",
3660
+ " +---+------+------------+-----------+",
3661
+ " | | | |",
3662
+ " v v v v",
3663
+ "+-------+ +---------+ +------------+ +-------+",
3664
+ "| Pages | |Documents| | Slideshows | | Video |",
3665
+ "+-------+ +---------+ +------------+ +-------+",
3666
+ "```",
3667
+ "",
3668
+ "## Write visually, then look under the hood whenever you like {[content]}",
3669
+ "",
3670
+ "Use the rich editor for headings, lists, tables, links, images, media, and everyday formatting. Switch to raw Markdown for exact control. Add themes, transform styles, and squiggly-square section annotations when a passage deserves its own visual treatment. The readable `.md` source stays at the center of the workflow.",
3671
+ "",
3672
+ "## Preview the finished idea before you export it {[list]}",
3673
+ "",
3674
+ "- **Document** \u2014 Read the work as a polished, continuous document.",
3675
+ "- **Page** \u2014 See how it behaves as a web page.",
3676
+ "- **Slideshow** \u2014 Step through headings as designed presentation blocks.",
3677
+ "- **Video** \u2014 Rehearse timing, motion, narration, and media in a player-ready view.",
3678
+ "",
3679
+ '## Local or installed: choose the workspace that fits {[twoColumn header="Work where your files make sense" left="Web|Browser-local workspaces or folders you explicitly grant" right="Desktop|Real folders, native menus, file opening, and updates"]}',
3680
+ "",
3681
+ '## In the project or in the pipeline {[twoColumn header="The same Markdown keeps moving" left="VS Code|Rich editing beside tabs, source control, and the project tree" right="CLI + MCP|Build, serve, convert, render, inspect, validate, and automate"]}',
3682
+ "",
3683
+ "## The quiet conveniences are there, too {[list]}",
3684
+ "",
3685
+ "- **Workspaces and file navigation** keep related documents organized without hiding their source.",
3686
+ "- **Automatic saving** protects the active revision while still surfacing conflicts or save errors.",
3687
+ "- **Optional version history** keeps plain sibling revisions when you want a trail of changes.",
3688
+ "- **Copy-by-link sharing** creates a bounded Markdown copy that can open directly in a chosen Play view.",
3689
+ "- **Offline-ready, local-first storage** keeps browser work on this device and native work inside folders you choose.",
3690
+ "",
3691
+ "## Export without rebuilding the document {[list]}",
3692
+ "",
3693
+ "- Create **Word documents and PDFs** for review, printing, and fixed-layout sharing.",
3694
+ "- Create **PowerPoint decks** from the same annotated sections you preview as a slideshow.",
3695
+ "- Publish **HTML pages, packaged HTML, and EPUB e-books**.",
3696
+ "- Keep portable **Markdown or DBK bundles** with their media assets.",
3697
+ "- Use the linked CLI registry for **spreadsheets, MP4 video, animated GIF, batch conversion, and more**.",
3698
+ "",
3699
+ "## Local-first is concrete, not decorative {[quote]}",
3700
+ "",
3701
+ "> Your browser workspace stays on this device. Native folders are opened only when you choose them. Your Markdown remains useful in DocBlocks, Git, a terminal, another editor, or an AI-assisted workflow.",
3702
+ "",
3703
+ "## Agents get document tools, not a blank check {[factCard]}",
3704
+ "",
3705
+ "The local MCP server can inspect, validate, preview, compare, and convert documents. Results remain temporary session artifacts until they are deliberately saved, and filesystem access begins with explicit roots rather than assumed authority.",
3706
+ "",
3707
+ "## Start with three small moves {[list]}",
3708
+ "",
3709
+ "1. Create a document with **New file** in the file pane.",
3710
+ "2. Write visually or in Markdown source; switch views whenever it helps.",
3711
+ "3. Open **Play** or **Export** when the idea is ready to become something else.",
3712
+ "",
3713
+ "## Explore DocBlocks {[list]}",
3714
+ "",
3715
+ "- [Desktop app](https://docblocks.com/desktop/) \u2014 macOS, Windows, and Linux.",
3716
+ "- [VS Code extension](https://docblocks.com/vscode/) \u2014 rich Markdown editing inside a workspace.",
3717
+ "- [CLI and MCP server](https://docblocks.com/cli/) \u2014 conversion, rendering, and agent workflows.",
3718
+ "- [Supported formats](https://docblocks.com/formats/) \u2014 current import and export directions.",
3719
+ "- [Documentation](https://docblocks.com/docs/) \u2014 guides for using and extending DocBlocks."
3720
+ ].join("\n");
3721
+
3552
3722
  // src/DocBlocksShell/DocBlocksShell.tsx
3553
3723
  import { Fragment as Fragment7, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
3554
3724
  var editorShellModulePromise = null;
@@ -3870,7 +4040,7 @@ function DocBlocksShell({
3870
4040
  saveThemePreference(pref);
3871
4041
  }, []);
3872
4042
  useEffect14(() => {
3873
- if (isElectronHost2() || typeof document === "undefined") return;
4043
+ if (isElectronHost3() || typeof document === "undefined") return;
3874
4044
  const metas = document.querySelectorAll('meta[name="theme-color"]');
3875
4045
  metas.forEach((meta) => {
3876
4046
  meta.content = DB_CHROME_COLORS[resolvedTheme];
@@ -3899,8 +4069,8 @@ function DocBlocksShell({
3899
4069
  const [sidebarWidth, setSidebarWidth] = useState14(loadSidebarWidth);
3900
4070
  const [compactLayout, setCompactLayout] = useState14(false);
3901
4071
  const effectiveCompact = isMobile || compactLayout;
3902
- const showBrowserStorageWarning = !isElectronHost2();
3903
- const appVersion = isElectronHost2() ? `${getDocBlocksHost().env.appVersion} desktop` : issueReportVersion ?? "web";
4072
+ const showBrowserStorageWarning = !isElectronHost3();
4073
+ const appVersion = isElectronHost3() ? `${getDocBlocksHost().env.appVersion} desktop` : issueReportVersion ?? "web";
3904
4074
  const issueReportUrl = buildIssueReportUrl({
3905
4075
  reportedAt: /* @__PURE__ */ new Date(),
3906
4076
  version: appVersion,
@@ -4014,7 +4184,7 @@ function DocBlocksShell({
4014
4184
  return;
4015
4185
  }
4016
4186
  let cancelled = false;
4017
- const electron = isElectronHost2();
4187
+ const electron = isElectronHost3();
4018
4188
  void listWorkspaces2().then((workspaces) => {
4019
4189
  if (cancelled) return;
4020
4190
  setTransientMoveDestinations(
@@ -4057,7 +4227,7 @@ function DocBlocksShell({
4057
4227
  const [selectedFile, setSelectedFile] = useState14(null);
4058
4228
  useDocumentTitle(selectedFile, homeDocumentTitle, homeDocumentPath);
4059
4229
  const exportDestinationAdapter = useMemo3(() => {
4060
- if (!isElectronHost2() || !activeWorkspaceId || !selectedFile) return void 0;
4230
+ if (!isElectronHost3() || !activeWorkspaceId || !selectedFile) return void 0;
4061
4231
  const documentId = JSON.stringify([activeWorkspaceId, selectedFile]);
4062
4232
  const host = getDocBlocksHost().exports;
4063
4233
  return {
@@ -4098,7 +4268,7 @@ function DocBlocksShell({
4098
4268
  const baseTarget = createFileSystemDocumentTarget(fsProvider, filePath);
4099
4269
  const originKind = transient?.descriptor.origin?.kind;
4100
4270
  const needsElectronHost = originKind === "loose-file" || originKind === "dbk";
4101
- if (!transient?.descriptor.origin || needsElectronHost && !isElectronHost2()) {
4271
+ if (!transient?.descriptor.origin || needsElectronHost && !isElectronHost3()) {
4102
4272
  return {
4103
4273
  key: baseTarget.key,
4104
4274
  async commit(request) {
@@ -4426,39 +4596,8 @@ function DocBlocksShell({
4426
4596
  return;
4427
4597
  }
4428
4598
  if (entries.length > 0) return;
4429
- const welcomePath = "/aboutDocBlocks.md";
4430
- const welcomeContent = [
4431
- "# DocBlocks: the local-first Markdown editor",
4432
- "",
4433
- "Your markdown can do anything.",
4434
- "",
4435
- "DocBlocks is a free, local-first Markdown editor for creating, organizing, and publishing documents without sending your writing to a server. What you write here can become a Word or PDF document, a slide deck, a web page, or a video.",
4436
- "",
4437
- "Simple to write. Beautiful wherever it goes.",
4438
- "",
4439
- "## Features",
4440
- "",
4441
- "- **Rich Markdown editing** \u2014 Write visually or switch to raw Markdown source at any time. Section annotations can change how individual blocks are presented.",
4442
- "- **Workspaces** \u2014 Organize documents into separate browser workspaces or folders on your device.",
4443
- "- **Useful everywhere** \u2014 Export Word, PowerPoint, PDF, HTML, and Markdown in the app, with EPUB and additional formats available through the CLI.",
4444
- "- **Playback and video** \u2014 Preview a document as a rich presentation and export it as an MP4 video.",
4445
- "- **Private by default** \u2014 No ads, accounts, or tracking. Your browser workspace stays on this device.",
4446
- "",
4447
- "## Getting Started",
4448
- "",
4449
- "1. Create a document with the **New file** button in the file pane.",
4450
- "2. Start writing in Markdown \u2014 the editor supports headings, lists, links, images, and more.",
4451
- "3. Your work is saved automatically.",
4452
- "",
4453
- "Built with [squisq](https://github.com/bendyline/squisq) by [Bendyline](https://bendyline.com).",
4454
- "",
4455
- "## Explore DocBlocks",
4456
- "",
4457
- "- [Desktop app](https://docblocks.com/desktop/) for macOS, Windows, and Linux",
4458
- "- [VS Code extension](https://docblocks.com/vscode/) for rich editing inside your workspace",
4459
- "- [CLI and MCP server](https://docblocks.com/cli/) for conversion and agent workflows",
4460
- "- [Supported formats](https://docblocks.com/formats/) and [documentation](https://docblocks.com/docs/)"
4461
- ].join("\n");
4599
+ const welcomePath = WELCOME_DOCUMENT_PATH;
4600
+ const welcomeContent = WELCOME_DOCUMENT_CONTENT;
4462
4601
  let seededContent = welcomeContent;
4463
4602
  try {
4464
4603
  await writeProviderText(fs, welcomePath, welcomeContent, "create");
@@ -4510,7 +4649,7 @@ function DocBlocksShell({
4510
4649
  let cancelled = false;
4511
4650
  const isCurrent = () => !cancelled && requestId === navigationRequestRef.current;
4512
4651
  void (async () => {
4513
- const electron = isElectronHost2();
4652
+ const electron = isElectronHost3();
4514
4653
  let workspaceIdRemap = {};
4515
4654
  if (electron) {
4516
4655
  try {
@@ -4758,7 +4897,7 @@ function DocBlocksShell({
4758
4897
  };
4759
4898
  }, [showBrowserStorageWarning]);
4760
4899
  useEffect14(() => {
4761
- if (isElectronHost2() || typeof window === "undefined") return;
4900
+ if (isElectronHost3() || typeof window === "undefined") return;
4762
4901
  const onBeforeInstallPrompt = (e) => {
4763
4902
  e.preventDefault();
4764
4903
  setInstallPromptEvent(e);
@@ -5047,7 +5186,7 @@ function DocBlocksShell({
5047
5186
  };
5048
5187
  }, [documentSession, documentSnapshot.status, documentSnapshot.targetKey]);
5049
5188
  useEffect14(() => {
5050
- if (!isElectronHost2()) return;
5189
+ if (!isElectronHost3()) return;
5051
5190
  const lifecycle = getDocBlocksHost().lifecycle;
5052
5191
  const stopPrepare = lifecycle.onPrepareClose(async (request) => {
5053
5192
  preparedCloseRequestRef.current = request.requestId;
@@ -5177,7 +5316,7 @@ function DocBlocksShell({
5177
5316
  void touchWorkspace2(destination.id).catch(() => void 0);
5178
5317
  if (sourceTargetKey) pendingDbkConflictsRef.current.delete(sourceTargetKey);
5179
5318
  const origin = transient.descriptor.origin;
5180
- if (isElectronHost2() && origin && (origin.kind === "loose-file" || origin.kind === "dbk")) {
5319
+ if (isElectronHost3() && origin && (origin.kind === "loose-file" || origin.kind === "dbk")) {
5181
5320
  try {
5182
5321
  await getDocBlocksHost().external.revoke(origin.resourceId);
5183
5322
  } catch {
@@ -5214,7 +5353,7 @@ function DocBlocksShell({
5214
5353
  const handleOpenFolder = useCallback14(async () => {
5215
5354
  const requestId = ++navigationRequestRef.current;
5216
5355
  try {
5217
- if (isElectronHost2()) {
5356
+ if (isElectronHost3()) {
5218
5357
  const info = await getDocBlocksHost().workspaces.pickFolder();
5219
5358
  if (!info) return;
5220
5359
  if (requestId !== navigationRequestRef.current) return;
@@ -5415,14 +5554,14 @@ function DocBlocksShell({
5415
5554
  void handleNewFile();
5416
5555
  }, [provider, handleNewFile]);
5417
5556
  const handleRevealWorkspace = useCallback14(async () => {
5418
- if (!isElectronHost2() || !activeWorkspaceId) return;
5557
+ if (!isElectronHost3() || !activeWorkspaceId) return;
5419
5558
  const ws = await getWorkspace(activeWorkspaceId);
5420
5559
  if (ws?.type === "electron-native" && ws.rootPath) {
5421
5560
  await getDocBlocksHost().shell.revealInFolder(ws.id);
5422
5561
  }
5423
5562
  }, [activeWorkspaceId]);
5424
5563
  useEffect14(() => {
5425
- if (!isElectronHost2()) return;
5564
+ if (!isElectronHost3()) return;
5426
5565
  const host = getDocBlocksHost();
5427
5566
  return host.onMenuCommand((cmd) => {
5428
5567
  switch (cmd) {
@@ -5542,6 +5681,56 @@ function DocBlocksShell({
5542
5681
  showToast
5543
5682
  ]
5544
5683
  );
5684
+ const handleEditorLinkClick = useCallback14(
5685
+ (href) => {
5686
+ const target = resolveShellEditorLinkTarget(href, selectedFile);
5687
+ if (!target) {
5688
+ showToast(
5689
+ "error",
5690
+ "DocBlocks only opens HTTP(S) links or Markdown files inside this workspace."
5691
+ );
5692
+ return true;
5693
+ }
5694
+ if (target.kind === "fragment") return false;
5695
+ if (target.kind === "external") {
5696
+ if (isElectronHost3()) {
5697
+ void getDocBlocksHost().shell.openExternal(target.url).catch((error) => {
5698
+ showToast(
5699
+ "error",
5700
+ error instanceof Error ? error.message : "Could not open the external link."
5701
+ );
5702
+ });
5703
+ } else if (typeof window !== "undefined") {
5704
+ try {
5705
+ window.open(target.url, "_blank", "noopener,noreferrer");
5706
+ } catch (error) {
5707
+ showToast(
5708
+ "error",
5709
+ error instanceof Error ? error.message : "Could not open the external link."
5710
+ );
5711
+ }
5712
+ }
5713
+ return true;
5714
+ }
5715
+ void (async () => {
5716
+ if (!provider) return;
5717
+ try {
5718
+ if (!await providerEntryExists(provider, target.path)) {
5719
+ showToast("error", "The linked Markdown file was not found in this workspace.");
5720
+ return;
5721
+ }
5722
+ await handleSelect(target.path, "file");
5723
+ } catch (error) {
5724
+ showToast(
5725
+ "error",
5726
+ error instanceof Error ? error.message : "Could not open the linked Markdown file."
5727
+ );
5728
+ }
5729
+ })();
5730
+ return true;
5731
+ },
5732
+ [handleSelect, provider, selectedFile, showToast]
5733
+ );
5545
5734
  const handleTreeMutation = useCallback14(
5546
5735
  async (change, mutate) => {
5547
5736
  if (!provider || !activeWorkspaceId || !selectedFile) {
@@ -5741,7 +5930,7 @@ function DocBlocksShell({
5741
5930
  [adoptTransientWorkspace]
5742
5931
  );
5743
5932
  useEffect14(() => {
5744
- if (!isElectronHost2()) return;
5933
+ if (!isElectronHost3()) return;
5745
5934
  const host = getDocBlocksHost();
5746
5935
  return host.onOpenRequest((req) => {
5747
5936
  const requestId = ++navigationRequestRef.current;
@@ -5807,7 +5996,7 @@ function DocBlocksShell({
5807
5996
  [adoptTransientWorkspace]
5808
5997
  );
5809
5998
  useEffect14(() => {
5810
- if (isElectronHost2() || typeof window === "undefined") return;
5999
+ if (isElectronHost3() || typeof window === "undefined") return;
5811
6000
  const launchQueue = window.launchQueue;
5812
6001
  if (!launchQueue) return;
5813
6002
  launchQueue.setConsumer((params) => {
@@ -5852,7 +6041,7 @@ function DocBlocksShell({
5852
6041
  import("@bendyline/squisq/storage"),
5853
6042
  import("@bendyline/squisq-formats/container")
5854
6043
  ]);
5855
- const electron = isElectronHost2();
6044
+ const electron = isElectronHost3();
5856
6045
  const all = await listWorkspaces2();
5857
6046
  const candidates = all.filter(
5858
6047
  (w) => electron ? w.type === "electron-native" || w.type === "transient" : w.type !== "electron-native"
@@ -5994,7 +6183,7 @@ function DocBlocksShell({
5994
6183
  }
5995
6184
  } else if (ws?.type === "transient") {
5996
6185
  const origin = ws.origin;
5997
- if (isElectronHost2() && origin && (origin.kind === "loose-file" || origin.kind === "dbk")) {
6186
+ if (isElectronHost3() && origin && (origin.kind === "loose-file" || origin.kind === "dbk")) {
5998
6187
  try {
5999
6188
  await getDocBlocksHost().external.revoke(origin.resourceId);
6000
6189
  } catch {
@@ -6007,7 +6196,7 @@ function DocBlocksShell({
6007
6196
  }
6008
6197
  await removeWorkspace(activeWorkspaceId);
6009
6198
  if (requestId !== navigationRequestRef.current) return;
6010
- const electron = isElectronHost2();
6199
+ const electron = isElectronHost3();
6011
6200
  const remaining = (await listWorkspaces2()).filter(
6012
6201
  (w) => electron ? w.type === "electron-native" : w.type !== "electron-native"
6013
6202
  );
@@ -6268,6 +6457,7 @@ function DocBlocksShell({
6268
6457
  articleId: selectedFile,
6269
6458
  fileName: selectedFile,
6270
6459
  onChange: handleEditorChange,
6460
+ onLinkClick: handleEditorLinkClick,
6271
6461
  colorScheme: resolvedTheme,
6272
6462
  writeCanvasSettings,
6273
6463
  height: "100%",
@@ -6382,7 +6572,7 @@ function DocBlocksShell({
6382
6572
  children: "New folder"
6383
6573
  }
6384
6574
  ),
6385
- (isElectronHost2() || typeof globalThis.showDirectoryPicker === "function") && /* @__PURE__ */ jsx12(
6575
+ (isElectronHost3() || typeof globalThis.showDirectoryPicker === "function") && /* @__PURE__ */ jsx12(
6386
6576
  "button",
6387
6577
  {
6388
6578
  type: "button",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/docblocks-react",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "React components for DocBlocks — file explorer, workspace picker, and editor shell",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -65,21 +65,21 @@
65
65
  "react-dom": "^18.0.0 || ^19.0.0"
66
66
  },
67
67
  "dependencies": {
68
- "@bendyline/docblocks": "2.2.0",
69
- "@bendyline/squisq": "2.3.2",
70
- "@bendyline/squisq-editor-react": "2.3.3",
71
- "@bendyline/squisq-formats": "2.3.2",
72
- "@bendyline/squisq-react": "2.3.2",
73
- "@bendyline/squisq-video": "2.2.2",
74
- "@bendyline/squisq-video-react": "2.2.2"
68
+ "@bendyline/docblocks": "2.2.1",
69
+ "@bendyline/squisq": "2.3.3",
70
+ "@bendyline/squisq-editor-react": "2.3.4",
71
+ "@bendyline/squisq-formats": "2.3.3",
72
+ "@bendyline/squisq-react": "2.3.3",
73
+ "@bendyline/squisq-video": "2.2.3",
74
+ "@bendyline/squisq-video-react": "2.2.3"
75
75
  },
76
76
  "devDependencies": {
77
77
  "@happy-dom/global-registrator": "20.9.0",
78
78
  "@types/react": "18.3.28",
79
79
  "@types/react-dom": "18.3.7",
80
80
  "happy-dom": "20.9.0",
81
- "monaco-editor": "0.50.0",
82
81
  "mocha": "11.3.0",
82
+ "monaco-editor": "0.50.0",
83
83
  "react": "18.3.1",
84
84
  "react-dom": "18.3.1",
85
85
  "tsup": "8.5.1",
@@ -1600,6 +1600,21 @@ body.db-resizing-sidebar * {
1600
1600
  gap: 6px;
1601
1601
  }
1602
1602
 
1603
+ .db-about-beta {
1604
+ margin-bottom: 10px;
1605
+ padding: 10px 12px;
1606
+ border: 1px solid var(--db-accent);
1607
+ border-radius: 6px;
1608
+ background: var(--db-accent-soft-08);
1609
+ color: var(--db-text-secondary);
1610
+ font-size: 12px;
1611
+ line-height: 1.5;
1612
+ }
1613
+
1614
+ .db-about-beta strong {
1615
+ color: var(--db-accent-deep);
1616
+ }
1617
+
1603
1618
  .db-about-version {
1604
1619
  display: flex;
1605
1620
  align-items: center;
@@ -4335,24 +4350,26 @@ html:has(.db-shell[data-theme='dark'])
4335
4350
  box-shadow: 0 2px 8px var(--db-shadow);
4336
4351
  }
4337
4352
 
4338
- /* The expanded Reload/Later prompt uses the same top-center card pattern as
4339
- the document-conflict card, accent-toned instead of warning. */
4353
+ /* The expanded Reload/Later prompt sits just above its status-bar trigger. */
4340
4354
  .db-update-banner {
4341
- position: fixed;
4342
- top: 16px;
4343
- left: 50%;
4355
+ position: absolute;
4356
+ right: 8px;
4357
+ bottom: 30px;
4344
4358
  z-index: 210;
4345
4359
  display: flex;
4346
4360
  gap: 16px;
4347
4361
  align-items: center;
4348
- max-width: min(720px, calc(100vw - 32px));
4362
+ max-width: min(720px, calc(100% - 16px));
4349
4363
  padding: 10px 12px;
4350
4364
  color: var(--db-text);
4351
4365
  background: var(--db-bg-elevated, var(--db-bg));
4352
4366
  border: 1px solid var(--db-accent);
4353
4367
  border-radius: 8px;
4354
4368
  box-shadow: 0 6px 20px var(--db-shadow);
4355
- transform: translateX(-50%);
4369
+ }
4370
+
4371
+ .db-update-banner--floating {
4372
+ bottom: 48px;
4356
4373
  }
4357
4374
 
4358
4375
  .db-update-banner-actions {
@@ -5033,15 +5050,15 @@ button.db-git-file-row:hover {
5033
5050
  padding-bottom: 2px;
5034
5051
  }
5035
5052
 
5036
- /* The native 32px caption band leaves the labels slightly above its visual
5037
- center. Move only the text down; the tab hit area and active underline
5038
- stay anchored to the titlebar edges. */
5053
+ /* Keep the compact labels on the same visual center as the action glyphs.
5054
+ Move only the text; the tab hit area and active underline stay anchored
5055
+ to the titlebar edges. */
5039
5056
  .db-shell .squisq-toolbar-view-tab-label {
5040
- transform: translateY(4px);
5057
+ transform: translateY(1px);
5041
5058
  }
5042
5059
 
5043
5060
  .db-shell .squisq-use-mode-trigger {
5044
- padding-bottom: 7px;
5061
+ padding-bottom: 10px;
5045
5062
  }
5046
5063
 
5047
5064
  /* The 32px caption row leaves little room around Squisq's 28px action