@bendyline/squisq-editor-react 1.5.3 → 1.6.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.
- package/README.md +57 -10
- package/dist/index.d.ts +1114 -52
- package/dist/index.js +18446 -8050
- package/dist/index.js.map +1 -1
- package/dist/styles/fa-brands-400-AHOAZHCU.woff2 +0 -0
- package/dist/styles/fa-regular-400-VRZYIBIZ.woff2 +0 -0
- package/dist/styles/fa-solid-900-MDEYK55F.woff2 +0 -0
- package/dist/styles/fa-v4compatibility-ETEVP6IB.woff2 +0 -0
- package/dist/styles/index.css +13867 -0
- package/package.json +15 -7
- package/src/BlockCardView.tsx +121 -0
- package/src/BlockPreviewPanel.tsx +69 -0
- package/src/BlockPropertiesPopover.tsx +191 -0
- package/src/EditorContext.tsx +165 -16
- package/src/EditorShell.tsx +265 -144
- package/src/FolderView.tsx +131 -0
- package/src/Icon.tsx +26 -0
- package/src/ImageEditor.tsx +69 -22
- package/src/OutlinePanel.tsx +64 -7
- package/src/PlainHtmlPreview.tsx +30 -3
- package/src/PreviewControls.tsx +491 -122
- package/src/PreviewPanel.tsx +15 -9
- package/src/RawEditor.tsx +179 -17
- package/src/RecorderEntry.tsx +9 -16
- package/src/TemplateAnnotation.ts +44 -0
- package/src/TemplatePicker.tsx +329 -54
- package/src/ThemeCustomizerPanel.tsx +30 -336
- package/src/ThemePicker.tsx +112 -3
- package/src/TimelineBlockPreview.tsx +37 -0
- package/src/TimelineTrack.tsx +671 -0
- package/src/Toolbar.tsx +544 -180
- package/src/Tooltip.tsx +22 -4
- package/src/TransitionPicker.tsx +351 -0
- package/src/VersionHistoryPanel.tsx +4 -16
- package/src/ViewMenuPanel.tsx +17 -14
- package/src/WysiwygEditor.tsx +161 -65
- package/src/__tests__/blockProperties.test.ts +92 -0
- package/src/__tests__/blockRange.test.ts +105 -0
- package/src/__tests__/buildPreviewDocTransition.test.ts +73 -0
- package/src/__tests__/codeContextSectionView.test.tsx +95 -0
- package/src/__tests__/codeContextZoneManager.test.ts +127 -0
- package/src/__tests__/createShapeLayer.test.ts +46 -0
- package/src/__tests__/diffContextSections.test.ts +39 -0
- package/src/__tests__/drawingShapeRoundTrip.test.ts +49 -0
- package/src/__tests__/editorShellCodeContext.test.tsx +86 -0
- package/src/__tests__/editorShellProps.test.tsx +96 -0
- package/src/__tests__/embeddedMedia.test.ts +48 -0
- package/src/__tests__/headingTransition.test.ts +138 -0
- package/src/__tests__/layoutChildRoundTrip.test.ts +71 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +10 -8
- package/src/__tests__/previewControls.test.tsx +70 -0
- package/src/__tests__/recorderMediaInsert.test.ts +86 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +18 -0
- package/src/__tests__/templatePickerMetadata.test.ts +32 -0
- package/src/__tests__/timelineSource.test.ts +134 -0
- package/src/__tests__/tiptapBridge.test.ts +92 -0
- package/src/__tests__/tiptapBridgeConformance.test.ts +47 -0
- package/src/__tests__/tooltip.test.tsx +72 -0
- package/src/__tests__/transitionCatalog.test.ts +64 -0
- package/src/__tests__/useBlockNavigator.test.tsx +67 -0
- package/src/__tests__/useJsonEditorTokens.test.ts +59 -0
- package/src/__tests__/useMediaRecorder.test.ts +41 -0
- package/src/__tests__/useTimelineClock.test.ts +21 -0
- package/src/blockProperties.ts +88 -0
- package/src/blockRange.ts +132 -0
- package/src/buildPreviewDoc.ts +98 -9
- package/src/codeContext/CodeContextSectionView.tsx +124 -0
- package/src/codeContext/CodeContextZoneManager.ts +149 -0
- package/src/codeContext/CodeContextZones.tsx +121 -0
- package/src/codeContext/diffContextSections.ts +38 -0
- package/src/codeContext/types.ts +75 -0
- package/src/customTemplates/AddBin.tsx +126 -0
- package/src/customTemplates/CustomLayoutManager.tsx +233 -0
- package/src/customTemplates/CustomTemplateContext.tsx +182 -0
- package/src/customTemplates/LayerToolbar.tsx +580 -0
- package/src/customTemplates/ShapeGlyph.tsx +47 -0
- package/src/customTemplates/TemplateDesigner.tsx +430 -0
- package/src/customTemplates/__tests__/library.test.ts +88 -0
- package/src/customTemplates/__tests__/normalizePositions.test.ts +109 -0
- package/src/customTemplates/__tests__/shapeDefs.test.ts +49 -0
- package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +95 -0
- package/src/customTemplates/designer.css +673 -0
- package/src/customTemplates/index.ts +31 -0
- package/src/customTemplates/library.ts +97 -0
- package/src/customTemplates/normalizePositions.ts +75 -0
- package/src/customTemplates/shapeDefs.ts +131 -0
- package/src/customTemplates/thumbnail.tsx +63 -0
- package/src/customTemplates/tokenDefs.ts +60 -0
- package/src/customTemplates/useDocCustomTemplates.ts +52 -0
- package/src/customTemplates/useMemoryLayerAdapter.ts +123 -0
- package/src/customThemes/CustomThemeContext.tsx +179 -0
- package/src/customThemes/CustomThemeDialog.tsx +286 -0
- package/src/customThemes/__tests__/CustomThemeContext.test.tsx +64 -0
- package/src/customThemes/__tests__/CustomThemeDialog.test.tsx +47 -0
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +51 -0
- package/src/customThemes/customThemeLibrary.ts +97 -0
- package/src/customThemes/index.ts +31 -0
- package/src/customThemes/themeControls.tsx +229 -0
- package/src/customThemes/themeDraft.ts +272 -0
- package/src/customThemes/useDocCustomThemes.ts +49 -0
- package/src/diagram/DiagramCanvas.tsx +240 -0
- package/src/diagram/DiagramExtension.ts +209 -0
- package/src/diagram/DiagramMaximizedOverlay.tsx +46 -0
- package/src/diagram/DiagramWidget.tsx +270 -0
- package/src/diagram/diagramCommands.ts +604 -0
- package/src/diagram/diagramConstants.ts +17 -0
- package/src/diagram/useDiagramData.ts +126 -0
- package/src/embeddedMedia.ts +78 -0
- package/src/frontmatter.ts +29 -0
- package/src/headingTransition.ts +231 -0
- package/src/imageEditor/CanvasSurface.tsx +383 -88
- package/src/imageEditor/PropertiesPanel.tsx +47 -1
- package/src/imageEditor/Toolbar.tsx +229 -16
- package/src/imageEditor/createShapeLayer.ts +280 -0
- package/src/imageEditor/icons.tsx +34 -114
- package/src/imageEditor/image-editor.css +54 -5
- package/src/imageEditor/state.ts +23 -3
- package/src/index.ts +109 -1
- package/src/jsonEditor/useJsonEditorTokens.ts +13 -43
- package/src/recorder/RecorderModal.tsx +120 -53
- package/src/recorder/RecorderPanel.tsx +2 -26
- package/src/recorder/hooks/useMediaRecorder.ts +17 -11
- package/src/recorder/insertMediaBlock.ts +30 -0
- package/src/resolveBlockVisual.ts +33 -0
- package/src/scene/Scene.tsx +540 -0
- package/src/scene/SceneBlockExtension.ts +198 -0
- package/src/scene/SceneBlockToolbar.tsx +201 -0
- package/src/scene/SceneBlockWidget.tsx +434 -0
- package/src/scene/ScenePropsBar.tsx +85 -0
- package/src/scene/SceneSelection.tsx +107 -0
- package/src/scene/SceneViewport.tsx +102 -0
- package/src/scene/ShapePalette.tsx +181 -0
- package/src/scene/__tests__/DiagramAdapter.test.ts +56 -0
- package/src/scene/__tests__/bezierEdit.test.ts +85 -0
- package/src/scene/__tests__/blockLayers.test.ts +57 -0
- package/src/scene/__tests__/shapeLayers.test.ts +106 -0
- package/src/scene/__tests__/useSceneHitTest.test.ts +90 -0
- package/src/scene/__tests__/useScenePanZoom.test.ts +103 -0
- package/src/scene/adapters/DiagramAdapter.ts +168 -0
- package/src/scene/adapters/DrawingAdapter.ts +415 -0
- package/src/scene/adapters/LayoutAdapter.ts +310 -0
- package/src/scene/adapters/blockLayers.ts +159 -0
- package/src/scene/commands/SceneCommand.ts +70 -0
- package/src/scene/commands/drawingCommands.ts +318 -0
- package/src/scene/commands/layoutCommands.ts +301 -0
- package/src/scene/hooks/useSceneHitTest.ts +105 -0
- package/src/scene/hooks/useScenePanZoom.ts +147 -0
- package/src/scene/hooks/useSceneSelection.ts +62 -0
- package/src/scene/index.ts +95 -0
- package/src/scene/layers/DiagramEdges.tsx +127 -0
- package/src/scene/layers/edgeGeometry.ts +77 -0
- package/src/scene/layers/nodeCard.tsx +145 -0
- package/src/scene/layers/renderLayer.tsx +70 -0
- package/src/scene/layers/shapeLayers.ts +201 -0
- package/src/scene/paths/bezierEdit.ts +208 -0
- package/src/scene/scene.css +649 -0
- package/src/scene/text/SceneTextOverlay.tsx +161 -0
- package/src/scene/text/sceneTextChannel.ts +40 -0
- package/src/scene/text/sceneTextConfig.ts +27 -0
- package/src/scene/text/sceneTiptap.ts +36 -0
- package/src/scene/text/useSceneTextEditing.ts +39 -0
- package/src/scene/tools/ConnectTool.ts +111 -0
- package/src/scene/tools/DrawingConnectTool.ts +161 -0
- package/src/scene/tools/PathTool.ts +158 -0
- package/src/scene/tools/PlaceTool.ts +47 -0
- package/src/scene/tools/SceneTool.ts +75 -0
- package/src/scene/tools/SelectTool.ts +284 -0
- package/src/scene/tools/ShapeTool.ts +144 -0
- package/src/scene/tools/TextTool.ts +72 -0
- package/src/scene/tools/TokenTool.ts +95 -0
- package/src/scene/tools/createDrawShapeTool.ts +82 -0
- package/src/styles/code-context.css +155 -0
- package/src/styles/diagram.css +183 -0
- package/src/styles/editor.css +1708 -150
- package/src/styles/folder-view.css +210 -0
- package/src/styles/image-edit-affordance.css +2 -2
- package/src/styles/index.css +5 -0
- package/src/timelineSource.ts +244 -0
- package/src/tiptapBridge.ts +115 -34
- package/src/tooltipPlacement.ts +13 -0
- package/src/transitionCatalog.ts +159 -0
- package/src/types/monaco-shims.d.ts +10 -0
- package/src/useBlockNavigator.ts +153 -0
- package/src/useMonacoLoader.ts +23 -1
- package/src/useTimelineClock.ts +76 -0
- package/src/utils/dropUtils.ts +1 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One entry in a {@link FolderView} — a file or subfolder. Host-defined
|
|
5
|
+
* `path` is opaque to this component: it's used as the React key and
|
|
6
|
+
* handed back verbatim to the open callbacks so the host can resolve it.
|
|
7
|
+
*/
|
|
8
|
+
export interface FolderEntry {
|
|
9
|
+
/** Display name (basename). */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Full host path. Opaque here; passed back on open. */
|
|
12
|
+
path: string;
|
|
13
|
+
isDirectory: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FolderViewProps {
|
|
17
|
+
/** Folder display name (basename) shown in the header. */
|
|
18
|
+
name: string;
|
|
19
|
+
/** Immediate children of the folder — subfolders and files. */
|
|
20
|
+
entries: FolderEntry[];
|
|
21
|
+
/** Open a file entry. The host decides what "open" means. */
|
|
22
|
+
onOpenFile: (entry: FolderEntry) => void;
|
|
23
|
+
/** Drill into a subfolder. */
|
|
24
|
+
onOpenFolder: (entry: FolderEntry) => void;
|
|
25
|
+
/** Primary action — create a new document in this folder. */
|
|
26
|
+
onNewDocument: () => void;
|
|
27
|
+
/** Secondary action — create a new subfolder. Omit to hide the button. */
|
|
28
|
+
onNewFolder?: () => void;
|
|
29
|
+
/** Color theme (default `'light'`). */
|
|
30
|
+
theme?: 'light' | 'dark';
|
|
31
|
+
/** CSS height for the container (default `'100%'`). */
|
|
32
|
+
height?: string;
|
|
33
|
+
/** Override the per-entry icon. Defaults to FontAwesome file/folder glyphs. */
|
|
34
|
+
iconFor?: (entry: FolderEntry) => ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* FolderView — a standalone folder browser surface.
|
|
39
|
+
*
|
|
40
|
+
* Lists a folder's files and subfolders plus a prominent "New document"
|
|
41
|
+
* action (and an optional "New folder"). It's the companion to
|
|
42
|
+
* {@link EditorShell}: where the shell edits a single document, this
|
|
43
|
+
* presents the directory around it.
|
|
44
|
+
*
|
|
45
|
+
* Like the shell, it's host-agnostic — the consumer supplies the
|
|
46
|
+
* `entries` and the open / new callbacks, and FolderView owns only the
|
|
47
|
+
* presentation and theming. It holds no state and reads nothing from
|
|
48
|
+
* disk, so it composes into any storage backend.
|
|
49
|
+
*/
|
|
50
|
+
export function FolderView({
|
|
51
|
+
name,
|
|
52
|
+
entries,
|
|
53
|
+
onOpenFile,
|
|
54
|
+
onOpenFolder,
|
|
55
|
+
onNewDocument,
|
|
56
|
+
onNewFolder,
|
|
57
|
+
theme = 'light',
|
|
58
|
+
height = '100%',
|
|
59
|
+
iconFor = defaultIconFor,
|
|
60
|
+
}: FolderViewProps) {
|
|
61
|
+
// Folders first, then files; each group alphabetical (case-insensitive).
|
|
62
|
+
const sorted = [...entries].sort((a, b) => {
|
|
63
|
+
if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1;
|
|
64
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="squisq-folder-view" data-theme={theme} style={{ height }}>
|
|
69
|
+
<div className="squisq-folder-view-header">
|
|
70
|
+
<i
|
|
71
|
+
className="fa-regular fa-folder-open squisq-folder-view-header-icon"
|
|
72
|
+
aria-hidden="true"
|
|
73
|
+
/>
|
|
74
|
+
<span className="squisq-folder-view-title">{name}</span>
|
|
75
|
+
<span className="squisq-folder-view-count">
|
|
76
|
+
{entries.length} {entries.length === 1 ? 'item' : 'items'}
|
|
77
|
+
</span>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div className="squisq-folder-view-body">
|
|
81
|
+
{sorted.length === 0 ? (
|
|
82
|
+
<p className="squisq-folder-view-empty">This folder is empty.</p>
|
|
83
|
+
) : (
|
|
84
|
+
<ul className="squisq-folder-view-list">
|
|
85
|
+
{sorted.map((entry) => (
|
|
86
|
+
<li key={entry.path}>
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
className="squisq-folder-view-item"
|
|
90
|
+
onClick={() => (entry.isDirectory ? onOpenFolder(entry) : onOpenFile(entry))}
|
|
91
|
+
>
|
|
92
|
+
<span className="squisq-folder-view-item-icon">{iconFor(entry)}</span>
|
|
93
|
+
<span className="squisq-folder-view-item-name">{entry.name}</span>
|
|
94
|
+
{entry.isDirectory && (
|
|
95
|
+
<i
|
|
96
|
+
className="fa-solid fa-chevron-right squisq-folder-view-item-chevron"
|
|
97
|
+
aria-hidden="true"
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
100
|
+
</button>
|
|
101
|
+
</li>
|
|
102
|
+
))}
|
|
103
|
+
</ul>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div className="squisq-folder-view-actions">
|
|
108
|
+
<button type="button" className="squisq-folder-view-new" onClick={onNewDocument}>
|
|
109
|
+
<i className="fa-solid fa-plus" aria-hidden="true" /> New document
|
|
110
|
+
</button>
|
|
111
|
+
{onNewFolder && (
|
|
112
|
+
<button type="button" className="squisq-folder-view-new-folder" onClick={onNewFolder}>
|
|
113
|
+
<i className="fa-regular fa-folder" aria-hidden="true" /> New folder
|
|
114
|
+
</button>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const IMAGE_RE = /\.(png|jpe?g|gif|svg|webp|bmp|avif)$/i;
|
|
122
|
+
|
|
123
|
+
function defaultIconFor(entry: FolderEntry): ReactNode {
|
|
124
|
+
if (entry.isDirectory) {
|
|
125
|
+
return <i className="fa-regular fa-folder fa-fw" aria-hidden="true" />;
|
|
126
|
+
}
|
|
127
|
+
if (IMAGE_RE.test(entry.name)) {
|
|
128
|
+
return <i className="fa-regular fa-file-image fa-fw" aria-hidden="true" />;
|
|
129
|
+
}
|
|
130
|
+
return <i className="fa-regular fa-file-lines fa-fw" aria-hidden="true" />;
|
|
131
|
+
}
|
package/src/Icon.tsx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Icon — renders a Font Awesome Free glyph via the webfont CSS that
|
|
3
|
+
* `styles/index.css` imports (`@fortawesome/fontawesome-free`).
|
|
4
|
+
*
|
|
5
|
+
* Icons are decorative: the surrounding control owns the accessible
|
|
6
|
+
* label (`aria-label` / `data-tooltip`), so the glyph is marked
|
|
7
|
+
* `aria-hidden`. Sizing and color flow from the parent's `font-size`
|
|
8
|
+
* and `color`, matching how the inline `{[icon]}` content nodes render
|
|
9
|
+
* (see `InlineIcon.ts`).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { CSSProperties } from 'react';
|
|
13
|
+
|
|
14
|
+
export interface IconProps {
|
|
15
|
+
/** Full Font Awesome class string, e.g. `"fa-solid fa-bold"`. */
|
|
16
|
+
icon: string;
|
|
17
|
+
/** Extra class names appended after the Font Awesome classes. */
|
|
18
|
+
className?: string;
|
|
19
|
+
style?: CSSProperties;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Icon({ icon, className, style }: IconProps) {
|
|
23
|
+
return (
|
|
24
|
+
<i className={className ? `${icon} ${className}` : icon} style={style} aria-hidden="true" />
|
|
25
|
+
);
|
|
26
|
+
}
|
package/src/ImageEditor.tsx
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* `exportImageEditDoc` from `@bendyline/squisq/imageEdit`.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { useCallback, useState } from 'react';
|
|
16
|
+
import { useCallback, useRef, useState } from 'react';
|
|
17
17
|
import type { ContentContainer } from '@bendyline/squisq/storage';
|
|
18
18
|
import { exportImageEditDoc, type ImageEditExportFormat } from '@bendyline/squisq/imageEdit';
|
|
19
19
|
import type { SurfaceScheme, Theme } from '@bendyline/squisq/schemas';
|
|
@@ -24,6 +24,13 @@ import { PropertiesPanel } from './imageEditor/PropertiesPanel.js';
|
|
|
24
24
|
import { Toolbar } from './imageEditor/Toolbar.js';
|
|
25
25
|
import { useImageEditor } from './imageEditor/useImageEditor.js';
|
|
26
26
|
import { useImageEditorTokens } from './imageEditor/useImageEditorTokens.js';
|
|
27
|
+
import {
|
|
28
|
+
createShapeLayer,
|
|
29
|
+
createLinearShapeLayer,
|
|
30
|
+
isLinearShapeKind,
|
|
31
|
+
} from './imageEditor/createShapeLayer.js';
|
|
32
|
+
|
|
33
|
+
const ZOOM_STEPS = [0.0625, 0.083, 0.125, 0.167, 0.25, 0.333, 0.5, 0.667, 1, 1.5, 2, 3, 4, 8, 16];
|
|
27
34
|
|
|
28
35
|
export interface ImageEditorProps {
|
|
29
36
|
/**
|
|
@@ -107,6 +114,33 @@ export function ImageEditor(props: ImageEditorProps) {
|
|
|
107
114
|
// re-lists without polling.
|
|
108
115
|
const [historyRefreshKey, setHistoryRefreshKey] = useState(0);
|
|
109
116
|
|
|
117
|
+
const surfaceRef = useRef<HTMLDivElement>(null);
|
|
118
|
+
const [zoom, setZoom] = useState(1.0);
|
|
119
|
+
|
|
120
|
+
const handleZoomIn = useCallback(() => {
|
|
121
|
+
setZoom((z) => ZOOM_STEPS.find((s) => s > z + 0.001) ?? 16);
|
|
122
|
+
}, []);
|
|
123
|
+
const handleZoomOut = useCallback(() => {
|
|
124
|
+
setZoom((z) => [...ZOOM_STEPS].reverse().find((s) => s < z - 0.001) ?? 0.0625);
|
|
125
|
+
}, []);
|
|
126
|
+
const handleZoomSet = useCallback((z: number) => {
|
|
127
|
+
setZoom(Math.max(0.0625, Math.min(16, z)));
|
|
128
|
+
}, []);
|
|
129
|
+
const handleZoom1to1 = useCallback(() => setZoom(1), []);
|
|
130
|
+
const handleZoomFit = useCallback(() => {
|
|
131
|
+
if (!state || !surfaceRef.current) return;
|
|
132
|
+
const { clientWidth, clientHeight } = surfaceRef.current;
|
|
133
|
+
const PADDING = 32;
|
|
134
|
+
const fitZ = Math.min(
|
|
135
|
+
(clientWidth - PADDING) / state.doc.canvas.width,
|
|
136
|
+
(clientHeight - PADDING) / state.doc.canvas.height,
|
|
137
|
+
);
|
|
138
|
+
setZoom(Math.max(0.0625, Math.min(16, fitZ)));
|
|
139
|
+
}, [state]);
|
|
140
|
+
|
|
141
|
+
// ID of a newly-created text layer that should immediately enter inline editing.
|
|
142
|
+
const [requestEditLayerId, setRequestEditLayerId] = useState<string | null>(null);
|
|
143
|
+
|
|
110
144
|
const handleExport = useCallback(
|
|
111
145
|
async (format: ImageEditExportFormat) => {
|
|
112
146
|
if (!state) return;
|
|
@@ -198,9 +232,11 @@ export function ImageEditor(props: ImageEditorProps) {
|
|
|
198
232
|
|
|
199
233
|
const handleCreateTextAt = useCallback(
|
|
200
234
|
(x: number, y: number) => {
|
|
235
|
+
const id = `layer-${Math.random().toString(36).slice(2, 10)}`;
|
|
201
236
|
dispatch({
|
|
202
237
|
type: 'add-layer',
|
|
203
238
|
layer: {
|
|
239
|
+
id,
|
|
204
240
|
type: 'text',
|
|
205
241
|
name: 'Text',
|
|
206
242
|
position: { x: Math.round(x), y: Math.round(y), width: 240, height: 48 },
|
|
@@ -211,35 +247,33 @@ export function ImageEditor(props: ImageEditorProps) {
|
|
|
211
247
|
},
|
|
212
248
|
});
|
|
213
249
|
dispatch({ type: 'set-tool', tool: 'select' });
|
|
250
|
+
setRequestEditLayerId(id);
|
|
214
251
|
},
|
|
215
252
|
[dispatch],
|
|
216
253
|
);
|
|
217
254
|
|
|
255
|
+
const shapeKind = state?.shapeKind ?? 'rectangle';
|
|
256
|
+
const shapeDragDraw = isLinearShapeKind(shapeKind);
|
|
257
|
+
|
|
218
258
|
const handleCreateShapeAt = useCallback(
|
|
219
259
|
(x: number, y: number) => {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
stroke: '#1a4d80',
|
|
235
|
-
strokeWidth: 2,
|
|
236
|
-
borderRadius: 8,
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
});
|
|
260
|
+
const layer = createShapeLayer(shapeKind, x, y);
|
|
261
|
+
const id = `layer-${Math.random().toString(36).slice(2, 10)}`;
|
|
262
|
+
dispatch({ type: 'add-layer', layer: { ...layer, id } });
|
|
263
|
+
dispatch({ type: 'set-tool', tool: 'select' });
|
|
264
|
+
if (layer.type === 'text') {
|
|
265
|
+
setRequestEditLayerId(id);
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
[dispatch, shapeKind],
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const handleCreateShapeFromPoints = useCallback(
|
|
272
|
+
(x1: number, y1: number, x2: number, y2: number) => {
|
|
273
|
+
dispatch({ type: 'add-layer', layer: createLinearShapeLayer(shapeKind, x1, y1, x2, y2) });
|
|
240
274
|
dispatch({ type: 'set-tool', tool: 'select' });
|
|
241
275
|
},
|
|
242
|
-
[dispatch],
|
|
276
|
+
[dispatch, shapeKind],
|
|
243
277
|
);
|
|
244
278
|
|
|
245
279
|
if (error) {
|
|
@@ -275,6 +309,7 @@ export function ImageEditor(props: ImageEditorProps) {
|
|
|
275
309
|
<Toolbar
|
|
276
310
|
doc={state.doc}
|
|
277
311
|
tool={state.tool}
|
|
312
|
+
shapeKind={state.shapeKind}
|
|
278
313
|
dispatch={dispatch}
|
|
279
314
|
uploadAsset={uploadAsset}
|
|
280
315
|
onExport={handleExport}
|
|
@@ -296,6 +331,12 @@ export function ImageEditor(props: ImageEditorProps) {
|
|
|
296
331
|
/>
|
|
297
332
|
) : null
|
|
298
333
|
}
|
|
334
|
+
zoom={zoom}
|
|
335
|
+
onZoomIn={handleZoomIn}
|
|
336
|
+
onZoomOut={handleZoomOut}
|
|
337
|
+
onZoomSet={handleZoomSet}
|
|
338
|
+
onZoomFit={handleZoomFit}
|
|
339
|
+
onZoom1to1={handleZoom1to1}
|
|
299
340
|
/>
|
|
300
341
|
<div className="squisq-image-editor-body">
|
|
301
342
|
<div className="squisq-image-editor-center">
|
|
@@ -307,6 +348,12 @@ export function ImageEditor(props: ImageEditorProps) {
|
|
|
307
348
|
dispatch={dispatch}
|
|
308
349
|
onCreateTextAt={handleCreateTextAt}
|
|
309
350
|
onCreateShapeAt={handleCreateShapeAt}
|
|
351
|
+
onCreateShapeFromPoints={handleCreateShapeFromPoints}
|
|
352
|
+
shapeDragDraw={shapeDragDraw}
|
|
353
|
+
zoom={zoom}
|
|
354
|
+
onSetZoom={handleZoomSet}
|
|
355
|
+
surfaceRef={surfaceRef}
|
|
356
|
+
requestEditLayerId={requestEditLayerId}
|
|
310
357
|
/>
|
|
311
358
|
</div>
|
|
312
359
|
<div className="squisq-image-editor-side">
|
package/src/OutlinePanel.tsx
CHANGED
|
@@ -17,18 +17,69 @@ import { templateLabel } from './TemplatePicker';
|
|
|
17
17
|
import { useHeadingLayout } from './useHeadingLayout';
|
|
18
18
|
import { usePreviewSettingsOptional } from './PreviewControls';
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Responsive default width for the outline pane, used when no fixed `width`
|
|
22
|
+
* (or `--squisq-outline-width`) is supplied: never narrower than 260px, grows
|
|
23
|
+
* with the viewport so it stretches out to fill horizontal space when there's
|
|
24
|
+
* room, and caps at 460px so it never dominates the editor. A viewport unit
|
|
25
|
+
* (rather than
|
|
26
|
+
* a container unit) keeps the pane and the toolbar's view-tabs — which read the
|
|
27
|
+
* same value — resolving to an identical width, so their right edges stay
|
|
28
|
+
* aligned.
|
|
29
|
+
*/
|
|
30
|
+
export const OUTLINE_RESPONSIVE_WIDTH = 'clamp(260px, 30vw, 460px)';
|
|
31
|
+
|
|
20
32
|
export interface OutlinePanelProps {
|
|
21
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Fixed width of the pane in pixels. When omitted, the pane sizes
|
|
35
|
+
* responsively from `--squisq-outline-width` (falling back to
|
|
36
|
+
* {@link OUTLINE_RESPONSIVE_WIDTH}) so it stretches on wider screens.
|
|
37
|
+
*/
|
|
22
38
|
width?: number;
|
|
23
39
|
/** Optional CSS class for the outer container. */
|
|
24
40
|
className?: string;
|
|
25
41
|
}
|
|
26
42
|
|
|
27
|
-
export function OutlinePanel({ width
|
|
28
|
-
const {
|
|
43
|
+
export function OutlinePanel({ width, className }: OutlinePanelProps) {
|
|
44
|
+
const {
|
|
45
|
+
doc,
|
|
46
|
+
markdownSource,
|
|
47
|
+
setMarkdownSource,
|
|
48
|
+
layoutMode,
|
|
49
|
+
goToBlockByLine,
|
|
50
|
+
activeBlockStartLine,
|
|
51
|
+
} = useEditorContext();
|
|
29
52
|
const paneRef = useRef<HTMLElement | null>(null);
|
|
30
53
|
const { scrollToBlock } = useHeadingLayout(paneRef);
|
|
31
|
-
const
|
|
54
|
+
const cursorActiveId = useActiveOutlineBlockId();
|
|
55
|
+
|
|
56
|
+
// In block-at-a-time mode there's no live cursor across the whole document
|
|
57
|
+
// (only the active block is mounted), so the highlight follows the card:
|
|
58
|
+
// match the block whose heading begins on the active slice's source line.
|
|
59
|
+
const blockModeActiveId = useMemo(() => {
|
|
60
|
+
if (layoutMode !== 'block' || activeBlockStartLine == null || !doc) return null;
|
|
61
|
+
const match = flattenBlocks(doc.blocks).find(
|
|
62
|
+
(b) => b.sourceHeading?.position?.start.line === activeBlockStartLine,
|
|
63
|
+
);
|
|
64
|
+
return match?.id ?? null;
|
|
65
|
+
}, [layoutMode, activeBlockStartLine, doc]);
|
|
66
|
+
const activeBlockId = layoutMode === 'block' ? blockModeActiveId : cursorActiveId;
|
|
67
|
+
|
|
68
|
+
// Clicking a row jumps the card to that block in block mode, or scrolls the
|
|
69
|
+
// editor to it in document mode.
|
|
70
|
+
const handleSelect = useCallback(
|
|
71
|
+
(block: Block) => {
|
|
72
|
+
if (layoutMode === 'block') {
|
|
73
|
+
const line = block.sourceHeading?.position?.start.line;
|
|
74
|
+
if (typeof line === 'number') {
|
|
75
|
+
goToBlockByLine(line);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
scrollToBlock(block);
|
|
80
|
+
},
|
|
81
|
+
[layoutMode, goToBlockByLine, scrollToBlock],
|
|
82
|
+
);
|
|
32
83
|
|
|
33
84
|
// Promote / demote the row's heading by rewriting just the `#` prefix
|
|
34
85
|
// on the heading line. Falls through when the new depth would leave the
|
|
@@ -53,9 +104,15 @@ export function OutlinePanel({ width = 240, className }: OutlinePanelProps) {
|
|
|
53
104
|
const accentColor = previewSettings?.activeTheme?.colors?.primary;
|
|
54
105
|
|
|
55
106
|
const isEmpty = !doc || doc.blocks.length === 0 || !hasAnyHeading(doc.blocks);
|
|
107
|
+
// Fixed px when a width is supplied; otherwise size from the shared
|
|
108
|
+
// `--squisq-outline-width` variable (the shell sets it, and the toolbar's
|
|
109
|
+
// view-tabs read the same value so their right edges stay aligned). The
|
|
110
|
+
// clamp fallback lets a standalone OutlinePanel stretch on wide screens too.
|
|
111
|
+
const basis =
|
|
112
|
+
width != null ? `${width}px` : `var(--squisq-outline-width, ${OUTLINE_RESPONSIVE_WIDTH})`;
|
|
56
113
|
const paneStyle: CSSProperties = {
|
|
57
|
-
width:
|
|
58
|
-
flex: `0 0 ${
|
|
114
|
+
width: basis,
|
|
115
|
+
flex: `0 0 ${basis}`,
|
|
59
116
|
overflow: 'auto',
|
|
60
117
|
...(accentColor
|
|
61
118
|
? ({ ['--squisq-outline-accent' as string]: accentColor } as CSSProperties)
|
|
@@ -81,7 +138,7 @@ export function OutlinePanel({ width = 240, className }: OutlinePanelProps) {
|
|
|
81
138
|
key={b.id}
|
|
82
139
|
block={b}
|
|
83
140
|
activeBlockId={activeBlockId}
|
|
84
|
-
onSelect={
|
|
141
|
+
onSelect={handleSelect}
|
|
85
142
|
onChangeLevel={changeHeadingLevel}
|
|
86
143
|
/>
|
|
87
144
|
))}
|
package/src/PlainHtmlPreview.tsx
CHANGED
|
@@ -21,10 +21,25 @@ import type { CSSProperties } from 'react';
|
|
|
21
21
|
import { parseMarkdown } from '@bendyline/squisq/markdown';
|
|
22
22
|
import type { MarkdownDocument, HtmlNode } from '@bendyline/squisq/markdown';
|
|
23
23
|
import type { MediaProvider, Theme } from '@bendyline/squisq/schemas';
|
|
24
|
-
import { markdownDocToPlainHtml } from '@bendyline/squisq-formats/html';
|
|
25
24
|
import { normalizeMalformedAssetUrl } from './utils/normalizeMalformedAssetUrl';
|
|
26
25
|
import { collectInlineFontAwesomeCss } from './utils/collectInlineFontAwesomeCss';
|
|
27
26
|
|
|
27
|
+
// formats/html is ~50 KB and only needed for the plain-HTML preview pane.
|
|
28
|
+
// Lazy-load on first render so it doesn't sit in the editor's main chunk.
|
|
29
|
+
type RenderFn = typeof import('@bendyline/squisq-formats/html').markdownDocToPlainHtml;
|
|
30
|
+
let cachedRender: RenderFn | null = null;
|
|
31
|
+
let cachedRenderPromise: Promise<RenderFn> | null = null;
|
|
32
|
+
function loadRenderFn(): Promise<RenderFn> {
|
|
33
|
+
if (cachedRender) return Promise.resolve(cachedRender);
|
|
34
|
+
if (!cachedRenderPromise) {
|
|
35
|
+
cachedRenderPromise = import('@bendyline/squisq-formats/html').then((m) => {
|
|
36
|
+
cachedRender = m.markdownDocToPlainHtml;
|
|
37
|
+
return cachedRender;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return cachedRenderPromise;
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
export interface PlainHtmlPreviewProps {
|
|
29
44
|
/** Raw markdown source. */
|
|
30
45
|
markdown: string;
|
|
@@ -129,9 +144,21 @@ export function PlainHtmlPreview({
|
|
|
129
144
|
// fetch under `allow-same-origin`.
|
|
130
145
|
const iconsCss = useMemo(() => collectInlineFontAwesomeCss(), []);
|
|
131
146
|
|
|
147
|
+
const [renderFn, setRenderFn] = useState<RenderFn | null>(() => cachedRender);
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (renderFn) return;
|
|
150
|
+
let cancelled = false;
|
|
151
|
+
loadRenderFn().then((fn) => {
|
|
152
|
+
if (!cancelled) setRenderFn(() => fn);
|
|
153
|
+
});
|
|
154
|
+
return () => {
|
|
155
|
+
cancelled = true;
|
|
156
|
+
};
|
|
157
|
+
}, [renderFn]);
|
|
158
|
+
|
|
132
159
|
const html = useMemo(
|
|
133
|
-
() =>
|
|
134
|
-
[mdDoc, title, mergedImages, theme, iconsCss],
|
|
160
|
+
() => (renderFn ? renderFn(mdDoc, { title, images: mergedImages, theme, iconsCss }) : ''),
|
|
161
|
+
[renderFn, mdDoc, title, mergedImages, theme, iconsCss],
|
|
135
162
|
);
|
|
136
163
|
|
|
137
164
|
return (
|