@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.
- package/README.md +23 -14
- package/dist/DocBlocksShell/DocBlocksShell.d.ts +2 -2
- package/dist/DocBlocksShell/DocBlocksShell.d.ts.map +1 -1
- package/dist/DocBlocksShell/DocBlocksShell.js +61 -29
- package/dist/DocBlocksShell/DocBlocksShell.js.map +1 -1
- package/dist/Export/ExportDialog.d.ts.map +1 -1
- package/dist/Export/ExportDialog.js +20 -3
- package/dist/Export/ExportDialog.js.map +1 -1
- package/dist/Export/ExportToolbarControls.d.ts.map +1 -1
- package/dist/Export/ExportToolbarControls.js +66 -16
- package/dist/Export/ExportToolbarControls.js.map +1 -1
- package/dist/Export/run-export.d.ts.map +1 -1
- package/dist/Export/run-export.js +28 -12
- package/dist/Export/run-export.js.map +1 -1
- package/dist/Export/transform-summaries.d.ts +7 -0
- package/dist/Export/transform-summaries.d.ts.map +1 -0
- package/dist/Export/transform-summaries.js +6 -0
- package/dist/Export/transform-summaries.js.map +1 -0
- package/dist/monaco-slim.d.ts +19 -0
- package/dist/monaco-slim.d.ts.map +1 -0
- package/dist/monaco-slim.js +19 -0
- package/dist/monaco-slim.js.map +1 -0
- package/package.json +12 -10
- package/src/DocBlocksShell/DocBlocksShell.tsx +85 -38
- package/src/Export/ExportDialog.tsx +28 -7
- package/src/Export/ExportToolbarControls.tsx +107 -19
- package/src/Export/run-export.ts +30 -18
- package/src/Export/transform-summaries.ts +14 -0
- package/src/monaco-slim.ts +20 -0
- package/src/styles/docblocks.css +75 -0
|
@@ -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
|
-
|
|
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?:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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’re watching this welcome doc in <strong>Play</strong> view —
|
|
1713
|
+
it’s a regular markdown file, and so is everything you’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
|
+
×
|
|
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.
|
|
317
|
-
<option
|
|
318
|
-
|
|
319
|
-
|
|
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 =
|
|
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 &&
|
|
221
|
-
<
|
|
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
|
+
×
|
|
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
|
}
|
package/src/Export/run-export.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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';
|