@bendyline/docblocks-react 1.1.1 → 1.1.2

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.
@@ -8,7 +8,7 @@
8
8
  import { useState, useCallback, useEffect, useRef } from 'react';
9
9
  import { EditorShell } from '@bendyline/squisq-editor-react';
10
10
  import type {
11
- EditorTheme,
11
+ EditorColorScheme,
12
12
  EditorView,
13
13
  ViewPreferences,
14
14
  DocumentLinkProvider,
@@ -62,7 +62,7 @@ import {
62
62
 
63
63
  export interface DocBlocksShellProps {
64
64
  /** Optional theme override. Omit or pass 'auto' to follow OS preference. */
65
- theme?: EditorTheme | 'auto';
65
+ theme?: EditorColorScheme | 'auto';
66
66
  /** Optional logo image URL for the app menu. */
67
67
  logoUrl?: string;
68
68
  /**
@@ -148,6 +148,27 @@ function loadLastState(): LastState | null {
148
148
  }
149
149
  }
150
150
 
151
+ /** One-time first-run callout shown over the welcome doc's Play view.
152
+ * Once the user starts writing, switches views themselves, or dismisses
153
+ * it, it never comes back — on any workspace. */
154
+ const WELCOME_GATEWAY_KEY = 'docblocks:welcomeGatewayDismissed';
155
+
156
+ function isWelcomeGatewayDismissed(): boolean {
157
+ try {
158
+ return localStorage.getItem(WELCOME_GATEWAY_KEY) === '1';
159
+ } catch {
160
+ return false;
161
+ }
162
+ }
163
+
164
+ function markWelcomeGatewayDismissed(): void {
165
+ try {
166
+ localStorage.setItem(WELCOME_GATEWAY_KEY, '1');
167
+ } catch {
168
+ // ignore quota errors
169
+ }
170
+ }
171
+
151
172
  const THEME_PREF_KEY = 'docblocks:themePreference';
152
173
 
153
174
  function loadThemePreference(): ThemePreference {
@@ -543,6 +564,8 @@ export function DocBlocksShell({
543
564
  const [editorKey, setEditorKey] = useState(0);
544
565
  const [explorerKey, setExplorerKey] = useState(0);
545
566
  const [initialView, setInitialView] = useState<EditorView>('wysiwyg');
567
+ // First-run gateway over the welcome doc's Play view — see WELCOME_GATEWAY_KEY.
568
+ const [showWelcomeGateway, setShowWelcomeGateway] = useState(false);
546
569
  /** Suppress popstate handling during programmatic navigation. */
547
570
  const skipPopState = useRef(false);
548
571
  const lastLocalSaveRef = useRef<{
@@ -670,6 +693,7 @@ export function DocBlocksShell({
670
693
  setExplorerKey((k) => k + 1);
671
694
  pushHash(fs.id, aboutPath);
672
695
  saveLastState({ workspaceId: fs.id, filePath: aboutPath, view: 'preview' });
696
+ if (!isWelcomeGatewayDismissed()) setShowWelcomeGateway(true);
673
697
  }
674
698
  return;
675
699
  }
@@ -709,10 +733,30 @@ export function DocBlocksShell({
709
733
  setExplorerKey((k) => k + 1);
710
734
  pushHash(fs.id, welcomePath);
711
735
  saveLastState({ workspaceId: fs.id, filePath: welcomePath, view: 'preview' });
736
+ if (!isWelcomeGatewayDismissed()) setShowWelcomeGateway(true);
712
737
  },
713
738
  [pushHash],
714
739
  );
715
740
 
741
+ /** Hide the welcome gateway and never show it again. Safe to call from
742
+ * paths where it may not be showing — only persists when it was. */
743
+ const closeWelcomeGateway = useCallback(() => {
744
+ setShowWelcomeGateway((showing) => {
745
+ if (showing) markWelcomeGatewayDismissed();
746
+ return false;
747
+ });
748
+ }, []);
749
+
750
+ /** Gateway CTA — flip the welcome doc from Play into the editor. */
751
+ const handleStartWriting = useCallback(() => {
752
+ closeWelcomeGateway();
753
+ setInitialView('wysiwyg');
754
+ setEditorKey((k) => k + 1);
755
+ if (activeWorkspaceId && selectedFile) {
756
+ saveLastState({ workspaceId: activeWorkspaceId, filePath: selectedFile, view: 'wysiwyg' });
757
+ }
758
+ }, [closeWelcomeGateway, activeWorkspaceId, selectedFile]);
759
+
716
760
  // Initialise workspace on mount — restore from hash or last-used
717
761
  useEffect(() => {
718
762
  (async () => {
@@ -843,6 +887,10 @@ export function DocBlocksShell({
843
887
  const target = (e.target as HTMLElement).closest?.('[data-view]');
844
888
  if (target) {
845
889
  const view = target.getAttribute('data-view') as EditorView;
890
+ if (view) {
891
+ // The user found the view tabs on their own — the gateway's job is done.
892
+ closeWelcomeGateway();
893
+ }
846
894
  if (view && activeWorkspaceId && selectedFile) {
847
895
  saveLastState({ workspaceId: activeWorkspaceId, filePath: selectedFile, view });
848
896
  }
@@ -850,7 +898,7 @@ export function DocBlocksShell({
850
898
  };
851
899
  window.addEventListener('click', handler, true);
852
900
  return () => window.removeEventListener('click', handler, true);
853
- }, [activeWorkspaceId, selectedFile]);
901
+ }, [activeWorkspaceId, selectedFile, closeWelcomeGateway]);
854
902
 
855
903
  const handleAutoSaved = useCallback((filePath: string, savedContent: string) => {
856
904
  lastLocalSaveRef.current = {
@@ -1097,10 +1145,11 @@ export function DocBlocksShell({
1097
1145
  setInitialView('wysiwyg');
1098
1146
  setEditorKey((k) => k + 1);
1099
1147
  setExplorerKey((k) => k + 1);
1148
+ closeWelcomeGateway();
1100
1149
  if (activeWorkspaceId) {
1101
1150
  pushHash(activeWorkspaceId, '/' + filename);
1102
1151
  }
1103
- }, [provider, activeWorkspaceId, pushHash]);
1152
+ }, [provider, activeWorkspaceId, pushHash, closeWelcomeGateway]);
1104
1153
 
1105
1154
  const handleRevealWorkspace = useCallback(async () => {
1106
1155
  if (!isElectronHost() || !activeWorkspaceId) return;
@@ -1150,37 +1199,7 @@ export function DocBlocksShell({
1150
1199
  if (!isElectronHost()) return;
1151
1200
  const host = getDocBlocksHost();
1152
1201
  return host.onOpenRequest(async (req) => {
1153
- if (req.filePath) {
1154
- const workspaces = (await listWorkspaces()).filter(
1155
- (w) => w.type === 'electron-native' && w.rootPath,
1156
- );
1157
- const match = workspaces.find(
1158
- (w) => req.filePath!.startsWith((w.rootPath ?? '') + '/') || req.filePath === w.rootPath,
1159
- );
1160
- if (match && match.rootPath) {
1161
- const rel = '/' + req.filePath.slice(match.rootPath.length).replace(/^\/+/, '');
1162
- await openFromIds(match.id, rel, true);
1163
- }
1164
- } else if (req.url) {
1165
- try {
1166
- const u = new URL(req.url);
1167
- const path = u.searchParams.get('path');
1168
- if (path) {
1169
- const workspaces = (await listWorkspaces()).filter(
1170
- (w) => w.type === 'electron-native' && w.rootPath,
1171
- );
1172
- const match = workspaces.find(
1173
- (w) => path.startsWith((w.rootPath ?? '') + '/') || path === w.rootPath,
1174
- );
1175
- if (match && match.rootPath) {
1176
- const rel = '/' + path.slice(match.rootPath.length).replace(/^\/+/, '');
1177
- await openFromIds(match.id, rel, true);
1178
- }
1179
- }
1180
- } catch {
1181
- // bad URL, ignore
1182
- }
1183
- }
1202
+ await openFromIds(req.workspaceId, req.path, true);
1184
1203
  });
1185
1204
  }, [openFromIds]);
1186
1205
 
@@ -1204,12 +1223,13 @@ export function DocBlocksShell({
1204
1223
  setEditorContent(content ?? '');
1205
1224
  setInitialView('wysiwyg');
1206
1225
  setEditorKey((k) => k + 1);
1226
+ closeWelcomeGateway();
1207
1227
  pushHash(activeWorkspaceId, path);
1208
1228
  saveLastState({ workspaceId: activeWorkspaceId, filePath: path, view: 'wysiwyg' });
1209
1229
  if (effectiveCompact) setMobileShowEditor(true);
1210
1230
  }
1211
1231
  },
1212
- [provider, activeWorkspaceId, pushHash, effectiveCompact],
1232
+ [provider, activeWorkspaceId, pushHash, effectiveCompact, closeWelcomeGateway],
1213
1233
  );
1214
1234
 
1215
1235
  const handleTreeChange = useCallback(async () => {
@@ -1609,7 +1629,15 @@ export function DocBlocksShell({
1609
1629
 
1610
1630
  {/* Editor area — hidden in compact layout when the sidebar is showing. */}
1611
1631
  {(!effectiveCompact || mobileShowEditor) && (
1612
- <div style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
1632
+ <div
1633
+ style={{
1634
+ flex: 1,
1635
+ overflow: 'hidden',
1636
+ display: 'flex',
1637
+ flexDirection: 'column',
1638
+ position: 'relative',
1639
+ }}
1640
+ >
1613
1641
  {selectedFile && mediaProvider ? (
1614
1642
  <MediaContext.Provider value={mediaProvider}>
1615
1643
  <EditorShell
@@ -1619,7 +1647,7 @@ export function DocBlocksShell({
1619
1647
  articleId={selectedFile}
1620
1648
  fileName={selectedFile}
1621
1649
  onChange={handleEditorChange}
1622
- theme={resolvedTheme}
1650
+ colorScheme={resolvedTheme}
1623
1651
  height="100%"
1624
1652
  outlineWidth={280}
1625
1653
  mediaProvider={mediaProvider}
@@ -1678,6 +1706,25 @@ export function DocBlocksShell({
1678
1706
  </>
1679
1707
  }
1680
1708
  />
1709
+ {showWelcomeGateway && (
1710
+ <div className="db-welcome-gateway" role="note" aria-label="Welcome tip">
1711
+ <span className="db-welcome-gateway-text">
1712
+ You&rsquo;re watching this welcome doc in <strong>Play</strong> view —
1713
+ it&rsquo;s a regular markdown file, and so is everything you&rsquo;ll write.
1714
+ </span>
1715
+ <button className="db-welcome-gateway-cta" onClick={handleStartWriting}>
1716
+ Start writing
1717
+ </button>
1718
+ <button
1719
+ className="db-welcome-gateway-dismiss"
1720
+ onClick={closeWelcomeGateway}
1721
+ aria-label="Dismiss welcome tip"
1722
+ title="Dismiss"
1723
+ >
1724
+ &times;
1725
+ </button>
1726
+ </div>
1727
+ )}
1681
1728
  </MediaContext.Provider>
1682
1729
  ) : selectedFolder ? (
1683
1730
  <div className="db-folder-view">
@@ -4,9 +4,9 @@
4
4
 
5
5
  import { useState, useCallback, useEffect, useRef } from 'react';
6
6
  import { getThemeSummaries } from '@bendyline/squisq/schemas';
7
- import { getTransformStyleSummaries } from '@bendyline/squisq/transform';
8
7
  import type { ExportFormat, ExportOptions, HtmlBundle, HtmlStyle } from './export-options.js';
9
8
  import { FORMAT_LABELS, saveExportOptions } from './export-options.js';
9
+ import { loadTransformStyleSummaries, type ExportSummaryOption } from './transform-summaries.js';
10
10
 
11
11
  export interface ExportDialogProps {
12
12
  /** Initial options (pre-populated from last export). */
@@ -105,10 +105,10 @@ export function ExportDialog({ initial, exporting, onExport, onClose }: ExportDi
105
105
  const [htmlBundle, setHtmlBundle] = useState<HtmlBundle>(initial.htmlBundle);
106
106
  const [includeLinkedDocs, setIncludeLinkedDocs] = useState<boolean>(initial.includeLinkedDocs);
107
107
  const [entryAsIndex, setEntryAsIndex] = useState<boolean>(initial.entryAsIndex);
108
+ const [transforms, setTransforms] = useState<ExportSummaryOption[]>([]);
108
109
  const dialogRef = useRef<HTMLDivElement>(null);
109
110
 
110
111
  const themes = getThemeSummaries();
111
- const transforms = getTransformStyleSummaries();
112
112
 
113
113
  // Both HTML styles honor themes now: rendered HTML through the
114
114
  // SquisqPlayer's theme system, plain HTML through squisq's
@@ -139,6 +139,23 @@ export function ExportDialog({ initial, exporting, onExport, onClose }: ExportDi
139
139
  // inert control.
140
140
  const showEntryAsIndex = showHtmlOptions && (htmlBundle === 'single' || includeLinkedDocs);
141
141
 
142
+ useEffect(() => {
143
+ if (!showTransform) return;
144
+ let cancelled = false;
145
+
146
+ loadTransformStyleSummaries()
147
+ .then((nextTransforms) => {
148
+ if (!cancelled) setTransforms(nextTransforms);
149
+ })
150
+ .catch(() => {
151
+ if (!cancelled) setTransforms([]);
152
+ });
153
+
154
+ return () => {
155
+ cancelled = true;
156
+ };
157
+ }, [showTransform]);
158
+
142
159
  const handleExport = useCallback(() => {
143
160
  const opts: ExportOptions = {
144
161
  format,
@@ -313,11 +330,15 @@ export function ExportDialog({ initial, exporting, onExport, onClose }: ExportDi
313
330
  value={transformStyle}
314
331
  onChange={(e) => setTransformStyle(e.target.value)}
315
332
  >
316
- {transforms.map((t) => (
317
- <option key={t.id} value={t.id}>
318
- {t.name}
319
- </option>
320
- ))}
333
+ {transforms.length === 0 ? (
334
+ <option value={transformStyle}>Loading...</option>
335
+ ) : (
336
+ transforms.map((t) => (
337
+ <option key={t.id} value={t.id}>
338
+ {t.name}
339
+ </option>
340
+ ))
341
+ )}
321
342
  </select>
322
343
  <span className="db-export-hint">
323
344
  {transforms.find((t) => t.id === transformStyle)?.description}
@@ -7,15 +7,12 @@
7
7
  * Must be rendered inside <EditorProvider> so useEditorContext() works.
8
8
  */
9
9
 
10
- import { useState, useCallback, useEffect, useRef, useMemo } from 'react';
10
+ import { useState, useCallback, useEffect, useRef, useMemo, type ComponentType } from 'react';
11
11
  import { useEditorContext } from '@bendyline/squisq-editor-react';
12
12
  import { getThemeSummaries } from '@bendyline/squisq/schemas';
13
- import { getTransformStyleSummaries } from '@bendyline/squisq/transform';
14
13
  import { parseMarkdown } from '@bendyline/squisq/markdown';
15
- import { markdownToDoc } from '@bendyline/squisq/doc';
16
- import { VideoExportModal } from '@bendyline/squisq-video-react';
17
- import { PLAYER_BUNDLE } from '@bendyline/squisq-react/standalone-source';
18
14
  import type { ContentContainer } from '@bendyline/squisq/storage';
15
+ import type { VideoExportModalProps } from '@bendyline/squisq-video-react';
19
16
  import type { ExportOptions } from './export-options.js';
20
17
  import {
21
18
  DEFAULT_OPTIONS,
@@ -25,6 +22,7 @@ import {
25
22
  } from './export-options.js';
26
23
  import { ExportDialog } from './ExportDialog.js';
27
24
  import { runExport } from './run-export.js';
25
+ import { loadTransformStyleSummaries, type ExportSummaryOption } from './transform-summaries.js';
28
26
 
29
27
  export interface ExportToolbarControlsProps {
30
28
  /** Currently selected file path — used to derive the download filename. */
@@ -33,8 +31,31 @@ export interface ExportToolbarControlsProps {
33
31
  mediaContainer?: ContentContainer | null;
34
32
  }
35
33
 
34
+ type ParsedMarkdown = ReturnType<typeof parseMarkdown>;
35
+
36
+ interface VideoExportModules {
37
+ Modal: ComponentType<VideoExportModalProps>;
38
+ markdownToDoc: (doc: ParsedMarkdown) => VideoExportModalProps['doc'];
39
+ playerScript: string;
40
+ }
41
+
42
+ let videoExportModulesPromise: Promise<VideoExportModules> | null = null;
43
+
44
+ function loadVideoExportModules(): Promise<VideoExportModules> {
45
+ videoExportModulesPromise ??= Promise.all([
46
+ import('@bendyline/squisq/doc'),
47
+ import('@bendyline/squisq-video-react'),
48
+ import('@bendyline/squisq-react/standalone-source'),
49
+ ]).then(([docModule, videoModule, playerModule]) => ({
50
+ Modal: videoModule.VideoExportModal,
51
+ markdownToDoc: docModule.markdownToDoc,
52
+ playerScript: playerModule.PLAYER_BUNDLE,
53
+ }));
54
+ return videoExportModulesPromise;
55
+ }
56
+
36
57
  /** Build the quick-export label from saved options. */
37
- function quickLabel(opts: ExportOptions): string {
58
+ function quickLabel(opts: ExportOptions, transformSummaries: ExportSummaryOption[]): string {
38
59
  const baseExt = FORMAT_EXTENSIONS[opts.format].toUpperCase().replace('.', '');
39
60
  // Recursive HTML always emits a ZIP (multi-doc tree), regardless of
40
61
  // the saved `htmlBundle` value. Applies to both plain and rendered
@@ -58,7 +79,7 @@ function quickLabel(opts: ExportOptions): string {
58
79
  if (theme) parts.push(theme.name);
59
80
  }
60
81
  if (opts.format === 'pptx' && opts.transformStyle) {
61
- const transform = getTransformStyleSummaries().find((t) => t.id === opts.transformStyle);
82
+ const transform = transformSummaries.find((t) => t.id === opts.transformStyle);
62
83
  if (transform) parts.push(transform.name);
63
84
  }
64
85
 
@@ -76,6 +97,11 @@ export function ExportToolbarControls({
76
97
  const [menuOpen, setMenuOpen] = useState(false);
77
98
  const [dialogOpen, setDialogOpen] = useState(false);
78
99
  const [videoModalOpen, setVideoModalOpen] = useState(false);
100
+ const [videoLoading, setVideoLoading] = useState(false);
101
+ const [videoLoadError, setVideoLoadError] = useState<string | null>(null);
102
+ const [videoModules, setVideoModules] = useState<VideoExportModules | null>(null);
103
+ const [videoDoc, setVideoDoc] = useState<VideoExportModalProps['doc'] | null>(null);
104
+ const [transformSummaries, setTransformSummaries] = useState<ExportSummaryOption[]>([]);
79
105
  const [exporting, setExporting] = useState(false);
80
106
  const menuRef = useRef<HTMLDivElement>(null);
81
107
 
@@ -108,13 +134,6 @@ export function ExportToolbarControls({
108
134
  return docThemeId ? { ...base, themeId: docThemeId } : base;
109
135
  }, [lastOptions, docThemeId]);
110
136
 
111
- /** Build a Doc from the current markdown for video export. */
112
- const doc = useMemo(() => {
113
- if (!videoModalOpen) return null;
114
- const mdDoc = parseMarkdown(markdownSource);
115
- return markdownToDoc(mdDoc);
116
- }, [videoModalOpen, markdownSource]);
117
-
118
137
  // Close menu on outside click
119
138
  useEffect(() => {
120
139
  if (!menuOpen) return;
@@ -127,6 +146,30 @@ export function ExportToolbarControls({
127
146
  return () => document.removeEventListener('pointerdown', onPointerDown);
128
147
  }, [menuOpen]);
129
148
 
149
+ useEffect(() => {
150
+ if (
151
+ !menuOpen ||
152
+ lastOptions?.format !== 'pptx' ||
153
+ !lastOptions.transformStyle ||
154
+ transformSummaries.length > 0
155
+ ) {
156
+ return;
157
+ }
158
+
159
+ let cancelled = false;
160
+ loadTransformStyleSummaries()
161
+ .then((nextSummaries) => {
162
+ if (!cancelled) setTransformSummaries(nextSummaries);
163
+ })
164
+ .catch(() => {
165
+ if (!cancelled) setTransformSummaries([]);
166
+ });
167
+
168
+ return () => {
169
+ cancelled = true;
170
+ };
171
+ }, [lastOptions?.format, lastOptions?.transformStyle, menuOpen, transformSummaries.length]);
172
+
130
173
  const handleToggleMenu = useCallback(() => {
131
174
  setMenuOpen((prev) => !prev);
132
175
  }, []);
@@ -140,13 +183,27 @@ export function ExportToolbarControls({
140
183
  setDialogOpen(false);
141
184
  }, []);
142
185
 
143
- const handleOpenVideoModal = useCallback(() => {
186
+ const handleOpenVideoModal = useCallback(async () => {
144
187
  setMenuOpen(false);
145
188
  setVideoModalOpen(true);
146
- }, []);
189
+ setVideoLoading(true);
190
+ setVideoLoadError(null);
191
+
192
+ try {
193
+ const modules = videoModules ?? (await loadVideoExportModules());
194
+ setVideoModules(modules);
195
+ setVideoDoc(modules.markdownToDoc(parseMarkdown(markdownSource)));
196
+ } catch {
197
+ setVideoLoadError('Video export could not be loaded.');
198
+ } finally {
199
+ setVideoLoading(false);
200
+ }
201
+ }, [markdownSource, videoModules]);
147
202
 
148
203
  const handleCloseVideoModal = useCallback(() => {
149
204
  setVideoModalOpen(false);
205
+ setVideoDoc(null);
206
+ setVideoLoadError(null);
150
207
  }, []);
151
208
 
152
209
  const handleExport = useCallback(
@@ -174,6 +231,8 @@ export function ExportToolbarControls({
174
231
  }
175
232
  }, [lastOptions, markdownSource, selectedFile, mediaContainer]);
176
233
 
234
+ const LoadedVideoExportModal = videoModules?.Modal;
235
+
177
236
  return (
178
237
  <>
179
238
  <div className="db-toolbar-menu" ref={menuRef}>
@@ -194,7 +253,7 @@ export function ExportToolbarControls({
194
253
  onClick={handleQuickExport}
195
254
  disabled={exporting}
196
255
  >
197
- {quickLabel(lastOptions)}
256
+ {quickLabel(lastOptions, transformSummaries)}
198
257
  </button>
199
258
  )}
200
259
  <button className="db-toolbar-menu-item" onClick={handleOpenDialog}>
@@ -217,9 +276,38 @@ export function ExportToolbarControls({
217
276
  />
218
277
  )}
219
278
 
220
- {videoModalOpen && doc && (
221
- <VideoExportModal doc={doc} playerScript={PLAYER_BUNDLE} onClose={handleCloseVideoModal} />
279
+ {videoModalOpen && (videoLoading || videoLoadError) && (
280
+ <div className="db-dialog-overlay">
281
+ <div className="db-dialog">
282
+ <div className="db-dialog-header">
283
+ <h2 className="db-dialog-title">Export Video</h2>
284
+ <button
285
+ className="db-dialog-close"
286
+ onClick={handleCloseVideoModal}
287
+ aria-label="Close"
288
+ >
289
+ &times;
290
+ </button>
291
+ </div>
292
+ <div className="db-dialog-body">
293
+ <p className="db-export-hint">{videoLoadError ?? 'Loading...'}</p>
294
+ </div>
295
+ </div>
296
+ </div>
222
297
  )}
298
+
299
+ {videoModalOpen &&
300
+ videoModules &&
301
+ LoadedVideoExportModal &&
302
+ videoDoc &&
303
+ !videoLoading &&
304
+ !videoLoadError && (
305
+ <LoadedVideoExportModal
306
+ doc={videoDoc}
307
+ playerScript={videoModules.playerScript}
308
+ onClose={handleCloseVideoModal}
309
+ />
310
+ )}
223
311
  </>
224
312
  );
225
313
  }
@@ -8,23 +8,7 @@ import type {
8
8
  MarkdownInlineNode,
9
9
  } from '@bendyline/squisq/markdown';
10
10
  import { parseMarkdown } from '@bendyline/squisq/markdown';
11
- import { markdownToDoc } from '@bendyline/squisq/doc';
12
11
  import type { Doc } from '@bendyline/squisq/schemas';
13
- import { applyTransform } from '@bendyline/squisq/transform';
14
- import { markdownDocToDocx } from '@bendyline/squisq-formats/docx';
15
- import { markdownDocToPdf } from '@bendyline/squisq-formats/pdf';
16
- import { docToPptx } from '@bendyline/squisq-formats/pptx';
17
- import {
18
- docToHtml,
19
- docToHtmlZip,
20
- collectImagePaths,
21
- markdownDocsToPlainHtmlBundle,
22
- markdownDocsToHtmlBundle,
23
- markdownDocToPlainHtml,
24
- } from '@bendyline/squisq-formats/html';
25
- import { containerToZip } from '@bendyline/squisq-formats/container';
26
- import { MemoryContentContainer } from '@bendyline/squisq/storage';
27
- import { PLAYER_BUNDLE } from '@bendyline/squisq-react/standalone-source';
28
12
  import type { ContentContainer } from '@bendyline/squisq/storage';
29
13
  import type { ExportOptions, ExportFormat } from './export-options.js';
30
14
  import { FORMAT_EXTENSIONS } from './export-options.js';
@@ -76,6 +60,7 @@ export async function runExport(
76
60
  const doc = parseMarkdown(markdown);
77
61
 
78
62
  if (options.format === 'docx') {
63
+ const { markdownDocToDocx } = await import('@bendyline/squisq-formats/docx');
79
64
  const images = mediaContainer ? await resolveImages(doc, mediaContainer) : undefined;
80
65
  const buf = await markdownDocToDocx(doc, { themeId, images });
81
66
  downloadBlob(new Blob([buf], { type: MIME_TYPES.docx }), filename);
@@ -83,6 +68,7 @@ export async function runExport(
83
68
  }
84
69
 
85
70
  if (options.format === 'pdf') {
71
+ const { markdownDocToPdf } = await import('@bendyline/squisq-formats/pdf');
86
72
  const buf = await markdownDocToPdf(doc, {
87
73
  themeId,
88
74
  pageSize: options.pageSize,
@@ -92,7 +78,12 @@ export async function runExport(
92
78
  }
93
79
 
94
80
  if (options.format === 'pptx') {
95
- // Use the full transform pipeline: markdown Doc transform PPTX
81
+ const [{ markdownToDoc }, { applyTransform }, { docToPptx }] = await Promise.all([
82
+ import('@bendyline/squisq/doc'),
83
+ import('@bendyline/squisq/transform'),
84
+ import('@bendyline/squisq-formats/pptx'),
85
+ ]);
86
+ // Use the full transform pipeline: markdown -> Doc -> transform -> PPTX
96
87
  const baseDoc = markdownToDoc(doc);
97
88
  const transformed = applyTransform(baseDoc, options.transformStyle);
98
89
  const enrichedDoc = transformed.doc;
@@ -136,6 +127,10 @@ async function runHtmlExport(
136
127
  if (options.includeLinkedDocs && mediaContainer && selectedFile) {
137
128
  const entryPath = basenameForBundle(selectedFile);
138
129
  if (options.htmlStyle === 'rendered') {
130
+ const [{ markdownDocsToHtmlBundle }, { PLAYER_BUNDLE }] = await Promise.all([
131
+ import('@bendyline/squisq-formats/html'),
132
+ import('@bendyline/squisq-react/standalone-source'),
133
+ ]);
139
134
  const blob = await markdownDocsToHtmlBundle({
140
135
  entryPath,
141
136
  readDocument: (path) => readDocumentFromContainer(mediaContainer, path),
@@ -149,6 +144,7 @@ async function runHtmlExport(
149
144
  downloadBlob(blob, zipName);
150
145
  return;
151
146
  }
147
+ const { markdownDocsToPlainHtmlBundle } = await import('@bendyline/squisq-formats/html');
152
148
  const blob = await markdownDocsToPlainHtmlBundle({
153
149
  entryPath,
154
150
  readDocument: (path) => readDocumentFromContainer(mediaContainer, path),
@@ -162,11 +158,19 @@ async function runHtmlExport(
162
158
  }
163
159
 
164
160
  if (options.htmlStyle === 'rendered') {
161
+ const [{ markdownToDoc }, { docToHtml, docToHtmlZip, collectImagePaths }, { PLAYER_BUNDLE }] =
162
+ await Promise.all([
163
+ import('@bendyline/squisq/doc'),
164
+ import('@bendyline/squisq-formats/html'),
165
+ import('@bendyline/squisq-react/standalone-source'),
166
+ ]);
165
167
  const mdDoc = parseMarkdown(markdown);
166
168
  const baseDoc = markdownToDoc(mdDoc);
167
169
  if (themeId) baseDoc.themeId = themeId;
168
170
 
169
- const images = mediaContainer ? await resolveDocImages(baseDoc, mediaContainer) : undefined;
171
+ const images = mediaContainer
172
+ ? await resolveDocImages(baseDoc, mediaContainer, collectImagePaths)
173
+ : undefined;
170
174
 
171
175
  if (options.htmlBundle === 'zip') {
172
176
  const blob = await docToHtmlZip(baseDoc, {
@@ -195,6 +199,12 @@ async function runHtmlExport(
195
199
  const localImages = referencedImages.filter((u) => !isExternalUrl(u));
196
200
 
197
201
  if (options.htmlBundle === 'zip' && mediaContainer && localImages.length > 0) {
202
+ const [{ markdownDocToPlainHtml }, { MemoryContentContainer }, { containerToZip }] =
203
+ await Promise.all([
204
+ import('@bendyline/squisq-formats/html'),
205
+ import('@bendyline/squisq/storage'),
206
+ import('@bendyline/squisq-formats/container'),
207
+ ]);
198
208
  // Mirror the markdown's relative paths inside the zip so <img src="..."> still resolves.
199
209
  const html = markdownDocToPlainHtml(mdDoc, { title: baseName, themeId });
200
210
  const container = new MemoryContentContainer();
@@ -210,6 +220,7 @@ async function runHtmlExport(
210
220
  return;
211
221
  }
212
222
 
223
+ const { markdownDocToPlainHtml } = await import('@bendyline/squisq-formats/html');
213
224
  // Single-file plain HTML — embed local images as base64 data URIs.
214
225
  const inlineMap = new Map<string, string>();
215
226
  if (mediaContainer) {
@@ -325,6 +336,7 @@ async function readDocumentFromContainer(
325
336
  async function resolveDocImages(
326
337
  doc: Doc,
327
338
  container: ContentContainer,
339
+ collectImagePaths: (doc: Doc) => Iterable<string>,
328
340
  ): Promise<Map<string, ArrayBuffer>> {
329
341
  const paths = collectImagePaths(doc);
330
342
  const map = new Map<string, ArrayBuffer>();
@@ -0,0 +1,14 @@
1
+ export interface ExportSummaryOption {
2
+ id: string;
3
+ name: string;
4
+ description?: string;
5
+ }
6
+
7
+ let transformStyleSummariesPromise: Promise<ExportSummaryOption[]> | null = null;
8
+
9
+ export function loadTransformStyleSummaries(): Promise<ExportSummaryOption[]> {
10
+ transformStyleSummariesPromise ??= import('@bendyline/squisq/transform').then(
11
+ ({ getTransformStyleSummaries }) => getTransformStyleSummaries(),
12
+ );
13
+ return transformStyleSummariesPromise;
14
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Slim Monaco editor bundle for DocBlocks browser surfaces.
3
+ *
4
+ * Squisq's raw editor lazy-loads `monaco-editor/esm/vs/editor/editor.main.js`
5
+ * so syntax highlighting works in raw mode. Alias both that subpath and the
6
+ * bare `monaco-editor` specifier to this file to keep the lazy chunk focused
7
+ * on markdown plus the languages users most often place in fenced blocks.
8
+ */
9
+
10
+ export * from 'monaco-editor/esm/vs/editor/editor.api';
11
+
12
+ import 'monaco-editor/esm/vs/basic-languages/markdown/markdown.contribution';
13
+ import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution';
14
+ import 'monaco-editor/esm/vs/basic-languages/typescript/typescript.contribution';
15
+ import 'monaco-editor/esm/vs/basic-languages/html/html.contribution';
16
+ import 'monaco-editor/esm/vs/basic-languages/css/css.contribution';
17
+ import 'monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution';
18
+ import 'monaco-editor/esm/vs/basic-languages/python/python.contribution';
19
+ import 'monaco-editor/esm/vs/basic-languages/shell/shell.contribution';
20
+ import 'monaco-editor/esm/vs/basic-languages/xml/xml.contribution';