@bendyline/squisq-editor-react 1.6.2 → 2.0.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 +12 -3
- package/dist/index.d.ts +1275 -183
- package/dist/index.js +20345 -12777
- package/dist/index.js.map +1 -1
- package/dist/monaco.d.ts +1 -0
- package/dist/monaco.js +4 -0
- package/dist/monaco.js.map +1 -0
- package/dist/styles/index.css +1607 -241
- package/package.json +9 -5
- package/src/BlockPropertiesPopover.tsx +30 -15
- package/src/DocumentSettingsDialog.tsx +39 -33
- package/src/EditorContext.tsx +199 -42
- package/src/EditorShell.tsx +349 -261
- package/src/ImageEditor.tsx +22 -1
- package/src/InlinePreviewGutter.tsx +16 -8
- package/src/MediaBin.tsx +107 -14
- package/src/OutlinePanel.tsx +202 -7
- package/src/PlainHtmlPreview.tsx +40 -1
- package/src/PreviewControls.tsx +362 -89
- package/src/PreviewPanel.tsx +312 -78
- package/src/RawEditor.tsx +13 -2
- package/src/RecorderEntry.tsx +2 -0
- package/src/StatusBar.tsx +5 -2
- package/src/TemplateAnnotation.ts +12 -3
- package/src/TemplatePicker.tsx +75 -21
- package/src/ThemeCustomizerPanel.tsx +8 -0
- package/src/ThemePicker.tsx +10 -5
- package/src/TimelineBlockPreview.tsx +1 -1
- package/src/Toolbar.tsx +357 -51
- package/src/TransitionPicker.tsx +36 -4
- package/src/ViewMenuPanel.tsx +12 -14
- package/src/WysiwygEditor.tsx +124 -91
- package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +33 -0
- package/src/__tests__/blockTagActivity.test.ts +183 -0
- package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +75 -0
- package/src/__tests__/buildPreviewDocContent.test.ts +48 -0
- package/src/__tests__/buildPreviewDocTransition.test.ts +21 -1
- package/src/__tests__/canvasSurfaceTextEditing.test.tsx +60 -0
- package/src/__tests__/codeContextSectionView.test.tsx +8 -6
- package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
- package/src/__tests__/documentSettingsDialog.test.tsx +51 -1
- package/src/__tests__/editorScrollHandoff.test.tsx +102 -0
- package/src/__tests__/editorScrollSync.test.ts +65 -0
- package/src/__tests__/editorShellProps.test.tsx +123 -1
- package/src/__tests__/emojiPicker.test.tsx +2 -42
- package/src/__tests__/headingTransition.test.ts +0 -12
- package/src/__tests__/imageEditAffordance.test.tsx +5 -1
- package/src/__tests__/imageEditModalContract.test.tsx +119 -0
- package/src/__tests__/imageEditorShell.test.tsx +15 -1
- package/src/__tests__/jsonEditor.test.tsx +56 -1
- package/src/__tests__/layersPanel.test.tsx +75 -5
- package/src/__tests__/majorApiTypes.test.ts +26 -0
- package/src/__tests__/mediaAttachmentFlow.test.ts +2 -2
- package/src/__tests__/mediaBinDrop.test.tsx +90 -0
- package/src/__tests__/narrationSave.test.ts +148 -0
- package/src/__tests__/outlinePanel.test.tsx +93 -3
- package/src/__tests__/outlineSource.test.ts +217 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +17 -1
- package/src/__tests__/presentationMode.test.tsx +290 -0
- package/src/__tests__/previewControls.test.tsx +382 -5
- package/src/__tests__/previewPanelAudioFailure.test.tsx +46 -0
- package/src/__tests__/previewPanelPresentation.test.tsx +126 -0
- package/src/__tests__/previewPanelTransforms.test.tsx +117 -0
- package/src/__tests__/rawEditorModelIsolation.test.ts +19 -0
- package/src/__tests__/recorderTheme.test.tsx +42 -0
- package/src/__tests__/resolveBlockVisual.test.ts +43 -0
- package/src/__tests__/selectionConversions.test.ts +80 -0
- package/src/__tests__/statusBar.test.tsx +27 -0
- package/src/__tests__/teleprompter.test.tsx +190 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +26 -0
- package/src/__tests__/templatePickerPortal.test.tsx +62 -0
- package/src/__tests__/tiptapBridge.test.ts +71 -7
- package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
- package/src/__tests__/tiptapImageRoundTrip.test.ts +1 -1
- package/src/__tests__/toolbarSelectionConversion.test.tsx +164 -0
- package/src/__tests__/transformStyleId.test.ts +13 -0
- package/src/__tests__/useImageEditor.test.tsx +67 -0
- package/src/__tests__/useMediaRecorder.test.ts +75 -0
- package/src/__tests__/viewMenuPanel.test.tsx +130 -0
- package/src/__tests__/wysiwygImageUpload.test.ts +69 -0
- package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +56 -0
- package/src/asciiDiagram/AsciiDiagramExtension.ts +343 -0
- package/src/asciiDiagram/AsciiDiagramWidget.tsx +254 -0
- package/src/asciiDiagram/RepairableDiagramExtension.ts +200 -0
- package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +145 -0
- package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +159 -0
- package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +284 -0
- package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
- package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
- package/src/asciiDiagram/asciiDiagramCommands.ts +174 -0
- package/src/asciiDiagram/asciiDiagramData.ts +149 -0
- package/src/asciiDiagram/asciiDiagramOps.ts +238 -0
- package/src/asciiDiagram/asciiPaste.ts +19 -0
- package/src/blockTagActivity.ts +233 -0
- package/src/buildDocumentPreviewMarkdown.ts +168 -0
- package/src/buildPreviewDoc.ts +19 -12
- package/src/codeContext/types.ts +1 -1
- package/src/customTemplates/CustomLayoutManager.tsx +3 -2
- package/src/customTemplates/CustomTemplateContext.tsx +6 -0
- package/src/customTemplates/TemplateDesigner.tsx +7 -4
- package/src/customTemplates/__tests__/library.test.ts +8 -0
- package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +13 -0
- package/src/customTemplates/designer.css +158 -99
- package/src/customTemplates/library.ts +16 -3
- package/src/customTemplates/useMemoryLayerAdapter.ts +7 -1
- package/src/customThemes/CustomThemeContext.tsx +6 -0
- package/src/customThemes/CustomThemeDialog.tsx +35 -5
- package/src/customThemes/ImportThemeSection.tsx +178 -0
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +9 -0
- package/src/customThemes/__tests__/importThemeSection.test.tsx +192 -0
- package/src/customThemes/customThemeLibrary.ts +7 -4
- package/src/customThemes/index.ts +7 -1
- package/src/customThemes/themeDraft.ts +23 -7
- package/src/diagram/DiagramCanvas.tsx +25 -6
- package/src/diagram/types.ts +35 -0
- package/src/editorScrollSync.ts +73 -0
- package/src/emojiData.ts +3 -23
- package/src/frontmatterSettings.ts +23 -0
- package/src/headingTransition.ts +0 -15
- package/src/imageEditor/CanvasSurface.tsx +29 -23
- package/src/imageEditor/LayersPanel.tsx +78 -3
- package/src/imageEditor/Toolbar.tsx +5 -1
- package/src/imageEditor/icons.tsx +4 -0
- package/src/imageEditor/image-editor.css +75 -0
- package/src/imageEditor/layers/SelectionHandles.tsx +1 -1
- package/src/imageEditor/useImageEditor.ts +71 -14
- package/src/index.ts +198 -20
- package/src/jsonEditor/JsonEditorContext.tsx +12 -6
- package/src/jsonEditor/RenderNode.tsx +24 -3
- package/src/jsonEditor/editors.tsx +86 -19
- package/src/mediaEntries.ts +12 -0
- package/src/monaco.ts +35 -0
- package/src/monacoWorkers.ts +89 -0
- package/src/outlineSource.ts +171 -0
- package/src/presentation/PresentationMode.tsx +596 -0
- package/src/rawEditorIsolation.ts +18 -0
- package/src/recorder/RecorderButton.tsx +9 -1
- package/src/recorder/RecorderModal.tsx +84 -41
- package/src/recorder/RecorderPanel.tsx +9 -1
- package/src/recorder/hooks/useMediaRecorder.ts +97 -53
- package/src/resolveBlockVisual.ts +10 -4
- package/src/scene/Scene.tsx +48 -18
- package/src/scene/SceneBlockExtension.ts +17 -10
- package/src/scene/SceneBlockWidget.tsx +5 -1
- package/src/scene/SceneViewport.tsx +7 -3
- package/src/scene/ShapePalette.tsx +17 -2
- package/src/scene/__tests__/DiagramAdapter.test.ts +86 -0
- package/src/scene/__tests__/SceneBlockExtension.test.ts +33 -0
- package/src/scene/__tests__/sceneIsolation.test.tsx +119 -0
- package/src/scene/adapters/DiagramAdapter.ts +12 -101
- package/src/scene/adapters/DrawingAdapter.ts +6 -1
- package/src/scene/adapters/LayoutAdapter.ts +3 -0
- package/src/scene/commands/SceneCommand.ts +17 -3
- package/src/scene/index.ts +1 -6
- package/src/scene/layers/DiagramEdges.tsx +21 -7
- package/src/scene/layers/edgeGeometry.ts +9 -1
- package/src/scene/layers/nodeCard.tsx +27 -8
- package/src/scene/scene.css +5 -2
- package/src/scene/text/SceneTextOverlay.tsx +5 -5
- package/src/scene/text/sceneTextChannel.ts +26 -20
- package/src/scene/text/sceneTextConfig.ts +4 -0
- package/src/scene/tools/ConnectTool.ts +13 -4
- package/src/scene/tools/SceneTool.ts +8 -0
- package/src/scene/tools/SelectTool.ts +39 -23
- package/src/scene/tools/ShapeTool.ts +2 -3
- package/src/selectionConversions.ts +155 -0
- package/src/styles/ascii-diagram.css +79 -0
- package/src/styles/ascii-timeline.css +596 -0
- package/src/styles/editor.css +736 -120
- package/src/styles/index.css +3 -0
- package/src/styles/tree-view.css +189 -0
- package/src/teleprompter/TeleprompterControls.tsx +218 -0
- package/src/teleprompter/TeleprompterSelfView.tsx +22 -0
- package/src/teleprompter/TeleprompterSurface.tsx +267 -0
- package/src/teleprompter/TeleprompterView.tsx +338 -0
- package/src/teleprompter/canvasRenderer.ts +161 -0
- package/src/teleprompter/floatingWindow.ts +352 -0
- package/src/teleprompter/index.ts +61 -0
- package/src/teleprompter/pcmWorklet.ts +62 -0
- package/src/teleprompter/recording/insertPreamble.ts +80 -0
- package/src/teleprompter/recording/narrationSave.ts +134 -0
- package/src/teleprompter/recording/useNarrationRecorder.ts +367 -0
- package/src/teleprompter/scrollModel.ts +85 -0
- package/src/teleprompter/teleprompterTheme.ts +303 -0
- package/src/teleprompter/types.ts +38 -0
- package/src/teleprompter/useFloatingWindow.ts +74 -0
- package/src/teleprompter/useMicAnalysis.ts +211 -0
- package/src/teleprompter/useTeleprompter.ts +421 -0
- package/src/templateContentPreviewResolver.ts +12 -5
- package/src/timeline/TimelineEditorWidget.tsx +861 -0
- package/src/timeline/TimelineViewExtension.ts +252 -0
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +414 -0
- package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
- package/src/timeline/__tests__/timelineCommands.test.ts +359 -0
- package/src/timeline/__tests__/timelineOps.test.ts +277 -0
- package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
- package/src/timeline/timelineCommands.ts +421 -0
- package/src/timeline/timelineData.ts +54 -0
- package/src/timeline/timelineOps.ts +381 -0
- package/src/timeline/timelinePaste.ts +8 -0
- package/src/tiptapBridge.ts +47 -21
- package/src/transformStyleId.ts +17 -0
- package/src/treeview/TreeOutlineWidget.tsx +390 -0
- package/src/treeview/TreeViewExtension.ts +250 -0
- package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +156 -0
- package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
- package/src/treeview/__tests__/treeOps.test.ts +177 -0
- package/src/treeview/__tests__/treePaste.test.ts +40 -0
- package/src/treeview/__tests__/treeViewCommands.test.ts +179 -0
- package/src/treeview/treeOps.ts +231 -0
- package/src/treeview/treePaste.ts +13 -0
- package/src/treeview/treeViewCommands.ts +99 -0
- package/src/treeview/treeViewData.ts +41 -0
- package/src/useMonacoLoader.ts +11 -34
- package/src/wysiwygImageUpload.ts +113 -0
- package/src/diagram/DiagramExtension.ts +0 -209
- package/src/diagram/DiagramWidget.tsx +0 -272
- package/src/diagram/useDiagramData.ts +0 -126
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* doc parse.
|
|
13
13
|
*
|
|
14
14
|
* Visual conventions match `VideoExportModal` from `@bendyline/squisq-
|
|
15
|
-
* video-react
|
|
15
|
+
* video-react`, with inline theme tokens so the body-level portal follows
|
|
16
|
+
* the editor's light/dark chrome scheme.
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import { useCallback, useEffect, useRef, useState, type CSSProperties } from 'react';
|
|
@@ -25,6 +26,8 @@ import { buildTimingJson, encodeTimingJson, timingPathFor } from './timingJson.j
|
|
|
25
26
|
|
|
26
27
|
// ── Types ──────────────────────────────────────────────────────────
|
|
27
28
|
|
|
29
|
+
export type RecorderColorScheme = 'light' | 'dark';
|
|
30
|
+
|
|
28
31
|
export interface RecorderModalProps {
|
|
29
32
|
/** Required — recordings are written here. */
|
|
30
33
|
mediaProvider: MediaProvider;
|
|
@@ -37,6 +40,8 @@ export interface RecorderModalProps {
|
|
|
37
40
|
container?: ContentContainer | null;
|
|
38
41
|
/** Initial capture source. Defaults to `'mic'` (narration). */
|
|
39
42
|
initialMode?: RecorderSource;
|
|
43
|
+
/** Light/dark chrome scheme. Defaults to `'light'`. */
|
|
44
|
+
colorScheme?: RecorderColorScheme;
|
|
40
45
|
/** Called after the modal is dismissed (save or cancel). */
|
|
41
46
|
onClose: () => void;
|
|
42
47
|
/**
|
|
@@ -77,9 +82,34 @@ const overlayStyle: CSSProperties = {
|
|
|
77
82
|
zIndex: 10000,
|
|
78
83
|
};
|
|
79
84
|
|
|
85
|
+
/**
|
|
86
|
+
* The recorder is normally portaled to `document.body`, outside the editor
|
|
87
|
+
* shell's CSS-variable scope. Copy the scheme onto a fresh scope and provide
|
|
88
|
+
* inline fallbacks so the standalone RecorderModal/RecorderButton exports are
|
|
89
|
+
* themed even when a host has not loaded the editor stylesheet.
|
|
90
|
+
*/
|
|
91
|
+
function recorderThemeStyle(colorScheme: RecorderColorScheme): CSSProperties {
|
|
92
|
+
const dark = colorScheme === 'dark';
|
|
93
|
+
return {
|
|
94
|
+
colorScheme,
|
|
95
|
+
'--squisq-recorder-surface': `var(--squisq-bg, ${dark ? '#1f2937' : '#fffdf7'})`,
|
|
96
|
+
'--squisq-recorder-input': `var(--squisq-input-bg, ${dark ? '#374151' : '#fff'})`,
|
|
97
|
+
'--squisq-recorder-border': `var(--squisq-border, ${dark ? '#4b5563' : '#c9b98a'})`,
|
|
98
|
+
'--squisq-recorder-text': `var(--squisq-text, ${dark ? '#e5e7eb' : '#4a3c1f'})`,
|
|
99
|
+
'--squisq-recorder-muted': `var(--squisq-text-muted, ${dark ? '#9ca3af' : '#5a4a2a'})`,
|
|
100
|
+
'--squisq-recorder-accent': 'var(--squisq-accent, #8b6914)',
|
|
101
|
+
'--squisq-recorder-accent-text': '#fff',
|
|
102
|
+
'--squisq-recorder-danger': dark ? '#dc4c4c' : '#b33a3a',
|
|
103
|
+
'--squisq-recorder-danger-border': dark ? '#ef6a6a' : '#902929',
|
|
104
|
+
'--squisq-recorder-error-bg': dark ? '#3f151b' : '#fceeee',
|
|
105
|
+
'--squisq-recorder-error-border': dark ? '#7f1d1d' : '#d88a8a',
|
|
106
|
+
'--squisq-recorder-error-text': dark ? '#fecdd3' : '#8c2a2a',
|
|
107
|
+
} as CSSProperties;
|
|
108
|
+
}
|
|
109
|
+
|
|
80
110
|
const modalStyle: CSSProperties = {
|
|
81
|
-
background: '
|
|
82
|
-
border: '1px solid
|
|
111
|
+
background: 'var(--squisq-recorder-surface)',
|
|
112
|
+
border: '1px solid var(--squisq-recorder-border)',
|
|
83
113
|
borderRadius: 0,
|
|
84
114
|
padding: '24px 28px',
|
|
85
115
|
width: 'min(560px, calc(100vw - 48px))',
|
|
@@ -87,14 +117,14 @@ const modalStyle: CSSProperties = {
|
|
|
87
117
|
overflowY: 'auto',
|
|
88
118
|
boxShadow: '0 8px 32px rgba(0,0,0,0.18)',
|
|
89
119
|
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
90
|
-
color: '
|
|
120
|
+
color: 'var(--squisq-recorder-text)',
|
|
91
121
|
};
|
|
92
122
|
|
|
93
123
|
const titleStyle: CSSProperties = {
|
|
94
124
|
margin: '0 0 16px 0',
|
|
95
125
|
fontSize: 18,
|
|
96
126
|
fontWeight: 600,
|
|
97
|
-
color: '
|
|
127
|
+
color: 'var(--squisq-recorder-text)',
|
|
98
128
|
};
|
|
99
129
|
|
|
100
130
|
const labelStyle: CSSProperties = {
|
|
@@ -102,7 +132,7 @@ const labelStyle: CSSProperties = {
|
|
|
102
132
|
fontSize: 13,
|
|
103
133
|
fontWeight: 500,
|
|
104
134
|
marginBottom: 4,
|
|
105
|
-
color: '
|
|
135
|
+
color: 'var(--squisq-recorder-text)',
|
|
106
136
|
};
|
|
107
137
|
|
|
108
138
|
const inputStyle: CSSProperties = {
|
|
@@ -110,10 +140,10 @@ const inputStyle: CSSProperties = {
|
|
|
110
140
|
padding: '6px 8px',
|
|
111
141
|
fontSize: 13,
|
|
112
142
|
fontFamily: 'inherit',
|
|
113
|
-
border: '1px solid
|
|
143
|
+
border: '1px solid var(--squisq-recorder-border)',
|
|
114
144
|
borderRadius: 0,
|
|
115
|
-
background: '
|
|
116
|
-
color: '
|
|
145
|
+
background: 'var(--squisq-recorder-input)',
|
|
146
|
+
color: 'var(--squisq-recorder-text)',
|
|
117
147
|
marginBottom: 12,
|
|
118
148
|
boxSizing: 'border-box',
|
|
119
149
|
};
|
|
@@ -130,9 +160,9 @@ const btnPrimary: CSSProperties = {
|
|
|
130
160
|
fontFamily: 'inherit',
|
|
131
161
|
fontWeight: 500,
|
|
132
162
|
cursor: 'pointer',
|
|
133
|
-
background: '
|
|
134
|
-
color: '
|
|
135
|
-
border: '1px solid
|
|
163
|
+
background: 'var(--squisq-recorder-accent)',
|
|
164
|
+
color: 'var(--squisq-recorder-accent-text)',
|
|
165
|
+
border: '1px solid var(--squisq-recorder-accent)',
|
|
136
166
|
borderRadius: 0,
|
|
137
167
|
};
|
|
138
168
|
|
|
@@ -142,16 +172,16 @@ const btnSecondary: CSSProperties = {
|
|
|
142
172
|
fontFamily: 'inherit',
|
|
143
173
|
fontWeight: 500,
|
|
144
174
|
cursor: 'pointer',
|
|
145
|
-
background: '
|
|
146
|
-
color: '
|
|
147
|
-
border: '1px solid
|
|
175
|
+
background: 'var(--squisq-recorder-input)',
|
|
176
|
+
color: 'var(--squisq-recorder-text)',
|
|
177
|
+
border: '1px solid var(--squisq-recorder-border)',
|
|
148
178
|
borderRadius: 0,
|
|
149
179
|
};
|
|
150
180
|
|
|
151
181
|
const btnDanger: CSSProperties = {
|
|
152
182
|
...btnPrimary,
|
|
153
|
-
background: '
|
|
154
|
-
borderColor: '
|
|
183
|
+
background: 'var(--squisq-recorder-danger)',
|
|
184
|
+
borderColor: 'var(--squisq-recorder-danger-border)',
|
|
155
185
|
};
|
|
156
186
|
|
|
157
187
|
const toggleRowStyle: CSSProperties = {
|
|
@@ -166,17 +196,17 @@ const toggleBase: CSSProperties = {
|
|
|
166
196
|
fontFamily: 'inherit',
|
|
167
197
|
cursor: 'pointer',
|
|
168
198
|
background: 'transparent',
|
|
169
|
-
color: '
|
|
170
|
-
border: '1px solid
|
|
199
|
+
color: 'var(--squisq-recorder-text)',
|
|
200
|
+
border: '1px solid var(--squisq-recorder-border)',
|
|
171
201
|
borderRadius: 999,
|
|
172
202
|
};
|
|
173
203
|
|
|
174
204
|
const toggleActive: CSSProperties = {
|
|
175
205
|
...toggleBase,
|
|
176
|
-
color: '
|
|
206
|
+
color: 'var(--squisq-recorder-accent-text)',
|
|
177
207
|
fontWeight: 600,
|
|
178
|
-
background: '
|
|
179
|
-
borderColor: '
|
|
208
|
+
background: 'var(--squisq-recorder-accent)',
|
|
209
|
+
borderColor: 'var(--squisq-recorder-accent)',
|
|
180
210
|
};
|
|
181
211
|
|
|
182
212
|
const previewBoxStyle: CSSProperties = {
|
|
@@ -196,21 +226,21 @@ const previewBoxStyle: CSSProperties = {
|
|
|
196
226
|
const audioMeterStyle: CSSProperties = {
|
|
197
227
|
width: '100%',
|
|
198
228
|
height: 56,
|
|
199
|
-
background: '
|
|
200
|
-
border: '1px solid
|
|
229
|
+
background: 'var(--squisq-recorder-input)',
|
|
230
|
+
border: '1px solid var(--squisq-recorder-border)',
|
|
201
231
|
marginBottom: 12,
|
|
202
232
|
display: 'flex',
|
|
203
233
|
alignItems: 'center',
|
|
204
234
|
justifyContent: 'center',
|
|
205
|
-
color: '
|
|
235
|
+
color: 'var(--squisq-recorder-muted)',
|
|
206
236
|
fontSize: 13,
|
|
207
237
|
fontVariantNumeric: 'tabular-nums',
|
|
208
238
|
};
|
|
209
239
|
|
|
210
240
|
const errorStyle: CSSProperties = {
|
|
211
|
-
background: '
|
|
212
|
-
border: '1px solid
|
|
213
|
-
color: '
|
|
241
|
+
background: 'var(--squisq-recorder-error-bg)',
|
|
242
|
+
border: '1px solid var(--squisq-recorder-error-border)',
|
|
243
|
+
color: 'var(--squisq-recorder-error-text)',
|
|
214
244
|
padding: '8px 10px',
|
|
215
245
|
fontSize: 13,
|
|
216
246
|
marginBottom: 12,
|
|
@@ -223,6 +253,20 @@ const buttonRowStyle: CSSProperties = {
|
|
|
223
253
|
marginTop: 8,
|
|
224
254
|
};
|
|
225
255
|
|
|
256
|
+
const summaryStyle: CSSProperties = {
|
|
257
|
+
margin: '0 0 12px 0',
|
|
258
|
+
fontSize: 12,
|
|
259
|
+
color: 'var(--squisq-recorder-muted)',
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const recordingStatusStyle: CSSProperties = {
|
|
263
|
+
fontSize: 13,
|
|
264
|
+
fontVariantNumeric: 'tabular-nums',
|
|
265
|
+
marginBottom: 12,
|
|
266
|
+
color: 'var(--squisq-recorder-accent)',
|
|
267
|
+
fontWeight: 600,
|
|
268
|
+
};
|
|
269
|
+
|
|
226
270
|
// ── Helpers ────────────────────────────────────────────────────────
|
|
227
271
|
|
|
228
272
|
function formatDurationMs(ms: number): string {
|
|
@@ -293,6 +337,7 @@ export function RecorderModal({
|
|
|
293
337
|
mediaProvider,
|
|
294
338
|
container = null,
|
|
295
339
|
initialMode = 'mic',
|
|
340
|
+
colorScheme = 'light',
|
|
296
341
|
onClose,
|
|
297
342
|
onSave,
|
|
298
343
|
}: RecorderModalProps) {
|
|
@@ -471,7 +516,14 @@ export function RecorderModal({
|
|
|
471
516
|
};
|
|
472
517
|
|
|
473
518
|
return (
|
|
474
|
-
<div
|
|
519
|
+
<div
|
|
520
|
+
className="squisq-editor-shell squisq-recorder-overlay"
|
|
521
|
+
data-theme={colorScheme}
|
|
522
|
+
style={{ ...overlayStyle, ...recorderThemeStyle(colorScheme) }}
|
|
523
|
+
role="dialog"
|
|
524
|
+
aria-modal="true"
|
|
525
|
+
aria-label="Record media"
|
|
526
|
+
>
|
|
475
527
|
<div style={modalStyle} onClick={(e) => e.stopPropagation()}>
|
|
476
528
|
<h2 style={titleStyle}>Record media</h2>
|
|
477
529
|
|
|
@@ -493,9 +545,7 @@ export function RecorderModal({
|
|
|
493
545
|
})}
|
|
494
546
|
</div>
|
|
495
547
|
|
|
496
|
-
<p style={{
|
|
497
|
-
{captureSummary(micOn, video)}
|
|
498
|
-
</p>
|
|
548
|
+
<p style={summaryStyle}>{captureSummary(micOn, video)}</p>
|
|
499
549
|
|
|
500
550
|
{recorder.error && <div style={errorStyle}>{recorder.error.message}</div>}
|
|
501
551
|
{saveError && <div style={errorStyle}>{saveError}</div>}
|
|
@@ -579,6 +629,7 @@ export function RecorderModal({
|
|
|
579
629
|
>
|
|
580
630
|
<input
|
|
581
631
|
type="checkbox"
|
|
632
|
+
style={{ accentColor: 'var(--squisq-recorder-accent)' }}
|
|
582
633
|
checked={includeSystemAudio}
|
|
583
634
|
onChange={(e) => setIncludeSystemAudio(e.target.checked)}
|
|
584
635
|
disabled={recorder.state === 'recording' || recorder.state === 'requesting'}
|
|
@@ -602,15 +653,7 @@ export function RecorderModal({
|
|
|
602
653
|
|
|
603
654
|
{/* Live duration during recording */}
|
|
604
655
|
{recorder.state === 'recording' && !isAudioOnly && (
|
|
605
|
-
<div
|
|
606
|
-
style={{
|
|
607
|
-
fontSize: 13,
|
|
608
|
-
fontVariantNumeric: 'tabular-nums',
|
|
609
|
-
marginBottom: 12,
|
|
610
|
-
color: '#8B6914',
|
|
611
|
-
fontWeight: 600,
|
|
612
|
-
}}
|
|
613
|
-
>
|
|
656
|
+
<div style={recordingStatusStyle}>
|
|
614
657
|
● Recording {formatDurationMs(recorder.durationMs)}
|
|
615
658
|
</div>
|
|
616
659
|
)}
|
|
@@ -12,7 +12,11 @@ import { useCallback, useState } from 'react';
|
|
|
12
12
|
import { createPortal } from 'react-dom';
|
|
13
13
|
import type { MediaProvider } from '@bendyline/squisq/schemas';
|
|
14
14
|
import type { ContentContainer } from '@bendyline/squisq/storage';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
RecorderModal,
|
|
17
|
+
type RecorderColorScheme,
|
|
18
|
+
type RecorderSaveResult,
|
|
19
|
+
} from './RecorderModal.js';
|
|
16
20
|
import { Icon } from '../Icon';
|
|
17
21
|
import type { RecorderSource } from './hooks/useMediaRecorder.js';
|
|
18
22
|
|
|
@@ -20,6 +24,8 @@ export interface RecorderPanelProps {
|
|
|
20
24
|
mediaProvider: MediaProvider;
|
|
21
25
|
container?: ContentContainer | null;
|
|
22
26
|
initialMode?: RecorderSource;
|
|
27
|
+
/** Light/dark chrome scheme copied onto the portaled modal. */
|
|
28
|
+
colorScheme?: RecorderColorScheme;
|
|
23
29
|
onSave?: (result: RecorderSaveResult) => void;
|
|
24
30
|
/** ARIA / tooltip label. Defaults to `'Record media'`. */
|
|
25
31
|
tooltip?: string;
|
|
@@ -31,6 +37,7 @@ export function RecorderPanel({
|
|
|
31
37
|
mediaProvider,
|
|
32
38
|
container = null,
|
|
33
39
|
initialMode = 'mic',
|
|
40
|
+
colorScheme = 'light',
|
|
34
41
|
onSave,
|
|
35
42
|
tooltip = 'Record media',
|
|
36
43
|
className,
|
|
@@ -57,6 +64,7 @@ export function RecorderPanel({
|
|
|
57
64
|
mediaProvider={mediaProvider}
|
|
58
65
|
container={container}
|
|
59
66
|
initialMode={initialMode}
|
|
67
|
+
colorScheme={colorScheme}
|
|
60
68
|
onClose={handleClose}
|
|
61
69
|
onSave={(result) => {
|
|
62
70
|
onSave?.(result);
|
|
@@ -173,6 +173,9 @@ export function useMediaRecorder(options: UseMediaRecorderOptions = {}): UseMedi
|
|
|
173
173
|
const startTimestampRef = useRef<number | null>(null);
|
|
174
174
|
const tickerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
175
175
|
const stopResolversRef = useRef<Array<(blob: Blob | null) => void>>([]);
|
|
176
|
+
const stopPromiseRef = useRef<Promise<Blob | null> | null>(null);
|
|
177
|
+
const requestPromiseRef = useRef<Promise<void> | null>(null);
|
|
178
|
+
const lifecycleRef = useRef(0);
|
|
176
179
|
|
|
177
180
|
// Stable copy of options for callbacks that read them late. Re-evaluated
|
|
178
181
|
// each render, but each callback closes over the ref so we don't have
|
|
@@ -222,9 +225,13 @@ export function useMediaRecorder(options: UseMediaRecorderOptions = {}): UseMedi
|
|
|
222
225
|
}, [clearTicker]);
|
|
223
226
|
|
|
224
227
|
const cancel = useCallback(() => {
|
|
228
|
+
lifecycleRef.current += 1;
|
|
225
229
|
const rec = recorderRef.current;
|
|
226
230
|
if (rec && rec.state !== 'inactive') {
|
|
227
231
|
try {
|
|
232
|
+
rec.ondataavailable = null;
|
|
233
|
+
rec.onstop = null;
|
|
234
|
+
rec.onerror = null;
|
|
228
235
|
rec.stop();
|
|
229
236
|
} catch {
|
|
230
237
|
// Ignore — we're tearing down anyway.
|
|
@@ -237,6 +244,8 @@ export function useMediaRecorder(options: UseMediaRecorderOptions = {}): UseMedi
|
|
|
237
244
|
startTimestampRef.current = null;
|
|
238
245
|
// Any in-flight stop() promises won't get a blob.
|
|
239
246
|
stopResolversRef.current.splice(0).forEach((resolve) => resolve(null));
|
|
247
|
+
stopPromiseRef.current = null;
|
|
248
|
+
requestPromiseRef.current = null;
|
|
240
249
|
setBlob(null);
|
|
241
250
|
setDurationMs(0);
|
|
242
251
|
setError(null);
|
|
@@ -244,66 +253,89 @@ export function useMediaRecorder(options: UseMediaRecorderOptions = {}): UseMedi
|
|
|
244
253
|
}, [clearTicker, releaseStream]);
|
|
245
254
|
|
|
246
255
|
const request = useCallback(async () => {
|
|
256
|
+
if (requestPromiseRef.current) return requestPromiseRef.current;
|
|
257
|
+
if (recorderRef.current?.stream.active) return;
|
|
247
258
|
if (!supportsMediaRecorder()) {
|
|
248
259
|
const err = new Error('MediaRecorder is not supported in this environment.');
|
|
249
260
|
setError(err);
|
|
250
261
|
setState('error');
|
|
251
262
|
throw err;
|
|
252
263
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
264
|
+
const lifecycle = ++lifecycleRef.current;
|
|
265
|
+
const requestPromise = (async () => {
|
|
266
|
+
setError(null);
|
|
267
|
+
setState('requesting');
|
|
268
|
+
let acquired: { stream: MediaStream; dispose: () => void } | null = null;
|
|
269
|
+
try {
|
|
270
|
+
const source = optionsRef.current.source ?? 'mic';
|
|
271
|
+
acquired = await acquireStream(source, optionsRef.current);
|
|
272
|
+
const { stream: nextStream, dispose } = acquired;
|
|
273
|
+
if (lifecycle !== lifecycleRef.current) {
|
|
274
|
+
nextStream.getTracks().forEach((track) => track.stop());
|
|
275
|
+
dispose();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const resolved = resolveFormat(captureKindFor(source), optionsRef.current.mimeType);
|
|
279
|
+
const recorderOptions: MediaRecorderOptions = {};
|
|
280
|
+
if (resolved.mimeType) recorderOptions.mimeType = resolved.mimeType;
|
|
281
|
+
if (optionsRef.current.bitsPerSecond) {
|
|
282
|
+
recorderOptions.bitsPerSecond = optionsRef.current.bitsPerSecond;
|
|
283
|
+
}
|
|
284
|
+
const recorder = new MediaRecorder(nextStream, recorderOptions);
|
|
269
285
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
286
|
+
recorder.ondataavailable = (e) => {
|
|
287
|
+
if (e.data && e.data.size > 0) chunksRef.current.push(e.data);
|
|
288
|
+
};
|
|
289
|
+
recorder.onstop = () => {
|
|
290
|
+
if (recorderRef.current !== recorder || lifecycle !== lifecycleRef.current) return;
|
|
291
|
+
// The recorded MIME type is authoritative once data is in hand —
|
|
292
|
+
// some browsers down-negotiate the format (e.g. drop codec hint).
|
|
293
|
+
const recordedType = recorder.mimeType || resolved.mimeType || 'application/octet-stream';
|
|
294
|
+
const finalBlob = new Blob(chunksRef.current, { type: recordedType });
|
|
295
|
+
chunksRef.current = [];
|
|
296
|
+
setBlob(finalBlob);
|
|
297
|
+
setState('stopped');
|
|
298
|
+
clearTicker();
|
|
299
|
+
stopResolversRef.current.splice(0).forEach((resolve) => resolve(finalBlob));
|
|
300
|
+
stopPromiseRef.current = null;
|
|
301
|
+
};
|
|
302
|
+
recorder.onerror = (event) => {
|
|
303
|
+
if (recorderRef.current !== recorder || lifecycle !== lifecycleRef.current) return;
|
|
304
|
+
const detail = (event as unknown as { error?: DOMException }).error;
|
|
305
|
+
const err = detail instanceof Error ? detail : new Error('Recorder error');
|
|
306
|
+
setError(err);
|
|
307
|
+
setState('error');
|
|
308
|
+
clearTicker();
|
|
309
|
+
stopResolversRef.current.splice(0).forEach((resolve) => resolve(null));
|
|
310
|
+
stopPromiseRef.current = null;
|
|
311
|
+
};
|
|
292
312
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
313
|
+
recorderRef.current = recorder;
|
|
314
|
+
disposeStreamRef.current = dispose;
|
|
315
|
+
setStream(nextStream);
|
|
316
|
+
setFormat(resolved);
|
|
317
|
+
setBlob(null);
|
|
318
|
+
setDurationMs(0);
|
|
319
|
+
setState('ready');
|
|
320
|
+
acquired = null;
|
|
321
|
+
} catch (err: unknown) {
|
|
322
|
+
if (acquired) {
|
|
323
|
+
acquired.stream.getTracks().forEach((track) => track.stop());
|
|
324
|
+
acquired.dispose();
|
|
325
|
+
}
|
|
326
|
+
const normalized = err instanceof Error ? err : new Error('Stream acquisition failed');
|
|
327
|
+
if (lifecycle === lifecycleRef.current) {
|
|
328
|
+
setError(normalized);
|
|
329
|
+
setState('error');
|
|
330
|
+
}
|
|
331
|
+
throw normalized;
|
|
332
|
+
} finally {
|
|
333
|
+
if (lifecycle === lifecycleRef.current) requestPromiseRef.current = null;
|
|
334
|
+
}
|
|
335
|
+
})();
|
|
336
|
+
requestPromiseRef.current = requestPromise;
|
|
337
|
+
return requestPromise;
|
|
338
|
+
}, [clearTicker]);
|
|
307
339
|
|
|
308
340
|
const start = useCallback(() => {
|
|
309
341
|
const rec = recorderRef.current;
|
|
@@ -329,12 +361,13 @@ export function useMediaRecorder(options: UseMediaRecorderOptions = {}): UseMedi
|
|
|
329
361
|
}, [clearTicker]);
|
|
330
362
|
|
|
331
363
|
const stop = useCallback((): Promise<Blob | null> => {
|
|
364
|
+
if (stopPromiseRef.current) return stopPromiseRef.current;
|
|
332
365
|
const rec = recorderRef.current;
|
|
333
366
|
if (!rec || rec.state === 'inactive') {
|
|
334
367
|
return Promise.resolve(blob);
|
|
335
368
|
}
|
|
336
369
|
setState('stopping');
|
|
337
|
-
|
|
370
|
+
const stopPromise = new Promise<Blob | null>((resolve) => {
|
|
338
371
|
stopResolversRef.current.push(resolve);
|
|
339
372
|
try {
|
|
340
373
|
rec.stop();
|
|
@@ -346,6 +379,11 @@ export function useMediaRecorder(options: UseMediaRecorderOptions = {}): UseMedi
|
|
|
346
379
|
stopResolversRef.current.splice(0).forEach((r) => r(null));
|
|
347
380
|
}
|
|
348
381
|
});
|
|
382
|
+
stopPromiseRef.current = stopPromise;
|
|
383
|
+
void stopPromise.finally(() => {
|
|
384
|
+
if (stopPromiseRef.current === stopPromise) stopPromiseRef.current = null;
|
|
385
|
+
});
|
|
386
|
+
return stopPromise;
|
|
349
387
|
}, [blob, clearTicker]);
|
|
350
388
|
|
|
351
389
|
// Final unmount cleanup — make sure we don't leak the camera light /
|
|
@@ -360,9 +398,14 @@ export function useMediaRecorder(options: UseMediaRecorderOptions = {}): UseMedi
|
|
|
360
398
|
useEffect(() => {
|
|
361
399
|
const pendingResolvers = stopResolversRef.current;
|
|
362
400
|
return () => {
|
|
401
|
+
lifecycleRef.current += 1;
|
|
402
|
+
requestPromiseRef.current = null;
|
|
363
403
|
const rec = recorderRef.current;
|
|
364
404
|
if (rec && rec.state !== 'inactive') {
|
|
365
405
|
try {
|
|
406
|
+
rec.ondataavailable = null;
|
|
407
|
+
rec.onstop = null;
|
|
408
|
+
rec.onerror = null;
|
|
366
409
|
rec.stop();
|
|
367
410
|
} catch {
|
|
368
411
|
// ignore
|
|
@@ -371,6 +414,7 @@ export function useMediaRecorder(options: UseMediaRecorderOptions = {}): UseMedi
|
|
|
371
414
|
releaseStream();
|
|
372
415
|
clearTicker();
|
|
373
416
|
pendingResolvers.splice(0).forEach((resolve) => resolve(null));
|
|
417
|
+
stopPromiseRef.current = null;
|
|
374
418
|
};
|
|
375
419
|
}, [releaseStream, clearTicker]);
|
|
376
420
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* resolveBlockVisual — resolves a doc block into a renderable card (with
|
|
3
|
-
* `layers`) via the shared preview pipeline
|
|
3
|
+
* `layers`) via the shared preview pipeline and canonical materializer.
|
|
4
4
|
*
|
|
5
5
|
* Kept in its own module (rather than alongside `BlockThumbnail` in
|
|
6
6
|
* `TimelineBlockPreview.tsx`) so that component file exports only components,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { Block, DocBlock, Doc, ViewportConfig, Theme } from '@bendyline/squisq/schemas';
|
|
11
|
-
import {
|
|
11
|
+
import { materializeBlockLayers, type MaterializeBlockLayersOptions } from '@bendyline/squisq/doc';
|
|
12
12
|
import { buildPreviewDoc } from './buildPreviewDoc';
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -24,8 +24,14 @@ export function resolveBlockVisual(
|
|
|
24
24
|
try {
|
|
25
25
|
const slide = buildPreviewDoc({ ...doc, blocks: [block] }).blocks[0] as DocBlock | undefined;
|
|
26
26
|
if (!slide) return null;
|
|
27
|
-
const ctx:
|
|
28
|
-
|
|
27
|
+
const ctx: MaterializeBlockLayersOptions = {
|
|
28
|
+
blockIndex: 0,
|
|
29
|
+
totalBlocks: 1,
|
|
30
|
+
theme,
|
|
31
|
+
viewport,
|
|
32
|
+
customTemplates: doc.customTemplates,
|
|
33
|
+
};
|
|
34
|
+
const { layers } = materializeBlockLayers(slide, ctx);
|
|
29
35
|
return { ...(slide as unknown as Block), layers };
|
|
30
36
|
} catch {
|
|
31
37
|
return null;
|