@bendyline/squisq-editor-react 1.6.2 → 2.0.0
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 +1229 -179
- package/dist/index.js +18017 -11231
- 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 +1471 -245
- package/package.json +9 -5
- package/src/BlockPropertiesPopover.tsx +30 -15
- package/src/DocumentSettingsDialog.tsx +13 -21
- package/src/EditorContext.tsx +199 -42
- package/src/EditorShell.tsx +348 -260
- 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 +309 -69
- package/src/PreviewPanel.tsx +312 -78
- package/src/RawEditor.tsx +13 -2
- 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 +202 -41
- 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__/customLayoutManagerTheme.test.tsx +34 -0
- package/src/__tests__/documentSettingsDialog.test.tsx +29 -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__/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 +218 -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__/resolveBlockVisual.test.ts +43 -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 +23 -0
- package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -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 +224 -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 +227 -0
- package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
- package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
- package/src/asciiDiagram/asciiDiagramCommands.ts +148 -0
- package/src/asciiDiagram/asciiDiagramData.ts +116 -0
- package/src/asciiDiagram/asciiDiagramOps.ts +219 -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/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/designer.css +158 -99
- package/src/customTemplates/library.ts +16 -3
- 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 +9 -4
- package/src/diagram/types.ts +35 -0
- package/src/editorScrollSync.ts +73 -0
- package/src/emojiData.ts +3 -23
- 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 +191 -19
- 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/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 +93 -0
- package/src/scene/adapters/DiagramAdapter.ts +12 -101
- package/src/scene/commands/SceneCommand.ts +4 -1
- 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 +34 -18
- package/src/scene/tools/ShapeTool.ts +2 -3
- package/src/styles/ascii-diagram.css +79 -0
- package/src/styles/ascii-timeline.css +499 -0
- package/src/styles/editor.css +706 -120
- package/src/styles/index.css +3 -0
- package/src/styles/tree-view.css +139 -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 +702 -0
- package/src/timeline/TimelineViewExtension.ts +252 -0
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +355 -0
- package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
- package/src/timeline/__tests__/timelineCommands.test.ts +327 -0
- package/src/timeline/__tests__/timelineOps.test.ts +222 -0
- package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
- package/src/timeline/timelineCommands.ts +367 -0
- package/src/timeline/timelineData.ts +54 -0
- package/src/timeline/timelineOps.ts +277 -0
- package/src/timeline/timelinePaste.ts +8 -0
- package/src/tiptapBridge.ts +24 -16
- package/src/transformStyleId.ts +17 -0
- package/src/treeview/TreeOutlineWidget.tsx +240 -0
- package/src/treeview/TreeViewExtension.ts +250 -0
- package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
- package/src/treeview/__tests__/treeOps.test.ts +125 -0
- package/src/treeview/__tests__/treePaste.test.ts +40 -0
- package/src/treeview/__tests__/treeViewCommands.test.ts +163 -0
- package/src/treeview/treeOps.ts +172 -0
- package/src/treeview/treePaste.ts +13 -0
- package/src/treeview/treeViewCommands.ts +94 -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
package/src/PlainHtmlPreview.tsx
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* it can fetch.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
19
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
20
20
|
import type { CSSProperties } from 'react';
|
|
21
21
|
import { parseMarkdown } from '@bendyline/squisq/markdown';
|
|
22
22
|
import type { MarkdownDocument, HtmlNode } from '@bendyline/squisq/markdown';
|
|
@@ -68,6 +68,8 @@ export interface PlainHtmlPreviewProps {
|
|
|
68
68
|
theme?: Theme;
|
|
69
69
|
className?: string;
|
|
70
70
|
style?: CSSProperties;
|
|
71
|
+
/** Let unmodified Up/Down arrows scroll the preview without requiring iframe focus. */
|
|
72
|
+
globalKeyboardShortcuts?: boolean;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
const IFRAME_STYLE: CSSProperties = {
|
|
@@ -87,7 +89,9 @@ export function PlainHtmlPreview({
|
|
|
87
89
|
theme,
|
|
88
90
|
className,
|
|
89
91
|
style,
|
|
92
|
+
globalKeyboardShortcuts = false,
|
|
90
93
|
}: PlainHtmlPreviewProps) {
|
|
94
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
91
95
|
const mdDoc = useMemo<MarkdownDocument>(() => parseMarkdown(markdown), [markdown]);
|
|
92
96
|
|
|
93
97
|
// Resolve any relative image URLs the doc references. Blob URLs are
|
|
@@ -161,8 +165,43 @@ export function PlainHtmlPreview({
|
|
|
161
165
|
[renderFn, mdDoc, title, mergedImages, theme, iconsCss],
|
|
162
166
|
);
|
|
163
167
|
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (!globalKeyboardShortcuts) return;
|
|
170
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
171
|
+
if (
|
|
172
|
+
event.defaultPrevented ||
|
|
173
|
+
event.altKey ||
|
|
174
|
+
event.ctrlKey ||
|
|
175
|
+
event.metaKey ||
|
|
176
|
+
event.shiftKey ||
|
|
177
|
+
(event.key !== 'ArrowDown' && event.key !== 'ArrowUp')
|
|
178
|
+
) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
182
|
+
if (
|
|
183
|
+
target?.closest(
|
|
184
|
+
'input, textarea, select, [contenteditable]:not([contenteditable="false"]), [role="textbox"], [role="combobox"], [role="listbox"], [role="menu"], [role="dialog"], [aria-modal="true"], .monaco-editor',
|
|
185
|
+
)
|
|
186
|
+
) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const frameWindow = iframeRef.current?.contentWindow;
|
|
190
|
+
if (!frameWindow) return;
|
|
191
|
+
event.preventDefault();
|
|
192
|
+
const distance = Math.max(64, Math.round(iframeRef.current?.clientHeight ?? 0) * 0.12);
|
|
193
|
+
frameWindow.scrollBy({
|
|
194
|
+
top: event.key === 'ArrowDown' ? distance : -distance,
|
|
195
|
+
behavior: 'smooth',
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
199
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
200
|
+
}, [globalKeyboardShortcuts]);
|
|
201
|
+
|
|
164
202
|
return (
|
|
165
203
|
<iframe
|
|
204
|
+
ref={iframeRef}
|
|
166
205
|
className={className}
|
|
167
206
|
data-testid="plain-html-preview"
|
|
168
207
|
title={title ?? 'HTML preview'}
|
package/src/PreviewControls.tsx
CHANGED
|
@@ -17,10 +17,12 @@ import {
|
|
|
17
17
|
useState,
|
|
18
18
|
useMemo,
|
|
19
19
|
useEffect,
|
|
20
|
+
useId,
|
|
20
21
|
useLayoutEffect,
|
|
21
22
|
useRef,
|
|
22
23
|
} from 'react';
|
|
23
24
|
import type { ReactNode } from 'react';
|
|
25
|
+
import { createPortal } from 'react-dom';
|
|
24
26
|
import type { DisplayMode, CaptionStyle } from '@bendyline/squisq-react';
|
|
25
27
|
import type { ViewportPreset, ViewportConfig } from '@bendyline/squisq/schemas';
|
|
26
28
|
import { VIEWPORT_PRESETS, getThemeSummaries } from '@bendyline/squisq/schemas';
|
|
@@ -28,15 +30,23 @@ import type { Theme } from '@bendyline/squisq/schemas';
|
|
|
28
30
|
import { ThemePicker } from './ThemePicker';
|
|
29
31
|
import { getTransformStyleSummaries } from '@bendyline/squisq/transform';
|
|
30
32
|
import type { Doc } from '@bendyline/squisq/schemas';
|
|
31
|
-
import { setFrontmatterValues } from '@bendyline/squisq/markdown';
|
|
33
|
+
import { readFrontmatterThemeId, setFrontmatterValues } from '@bendyline/squisq/markdown';
|
|
32
34
|
import {
|
|
33
35
|
resolveThemeForDoc,
|
|
34
36
|
writeCustomThemesToFrontmatter,
|
|
37
|
+
writeCustomTemplatesToFrontmatter,
|
|
35
38
|
FRONTMATTER_CUSTOM_THEMES_KEY,
|
|
39
|
+
FRONTMATTER_CUSTOM_TEMPLATES_KEY,
|
|
36
40
|
} from '@bendyline/squisq/doc';
|
|
37
41
|
import { useEditorContext } from './EditorContext';
|
|
38
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
useCustomThemes,
|
|
44
|
+
CustomThemeDialog,
|
|
45
|
+
type ThemeSaveTarget,
|
|
46
|
+
type ThemeSaveExtras,
|
|
47
|
+
} from './customThemes';
|
|
39
48
|
import { Icon } from './Icon';
|
|
49
|
+
import { resolvePersistedTransformStyleId } from './transformStyleId';
|
|
40
50
|
|
|
41
51
|
// ── Context ──────────────────────────────────────────────────────
|
|
42
52
|
|
|
@@ -81,7 +91,7 @@ export interface PreviewSettings {
|
|
|
81
91
|
export interface ThemeDesignerConfig {
|
|
82
92
|
value: Theme | null;
|
|
83
93
|
onChange: (theme: Theme) => void;
|
|
84
|
-
onSave: (theme: Theme, target: ThemeSaveTarget) => void;
|
|
94
|
+
onSave: (theme: Theme, target: ThemeSaveTarget, extras?: ThemeSaveExtras) => void;
|
|
85
95
|
onClose: () => void;
|
|
86
96
|
}
|
|
87
97
|
|
|
@@ -127,8 +137,9 @@ function resolveRenderAs(value: unknown): ViewportPreset | null {
|
|
|
127
137
|
function resolveDisplayMode(value: unknown): DisplayMode | null {
|
|
128
138
|
if (typeof value !== 'string') return null;
|
|
129
139
|
const v = value.trim().toLowerCase();
|
|
130
|
-
if (v === 'video' || v === 'slideshow' || v === 'linear') return v;
|
|
140
|
+
if (v === 'video' || v === 'slideshow' || v === 'linear' || v === 'narrate') return v;
|
|
131
141
|
if (v === 'slides' || v === 'presentation' || v === 'deck') return 'slideshow';
|
|
142
|
+
if (v === 'teleprompter' || v === 'prompter') return 'narrate';
|
|
132
143
|
// Frontmatter uses product-facing names: Document is the plain text/HTML
|
|
133
144
|
// preview, Page is the styled Squisq page view. The raw DisplayMode values
|
|
134
145
|
// are older and remain stable for the public React API.
|
|
@@ -155,17 +166,6 @@ function resolveFrontmatterTheme(value: unknown, customIds?: Set<string>): strin
|
|
|
155
166
|
return null;
|
|
156
167
|
}
|
|
157
168
|
|
|
158
|
-
const VALID_TRANSFORM_IDS = new Set(getTransformStyleSummaries().map((s) => s.id));
|
|
159
|
-
|
|
160
|
-
function resolveFrontmatterTransform(value: unknown): string | null {
|
|
161
|
-
if (typeof value !== 'string') return null;
|
|
162
|
-
const v = value.trim().toLowerCase();
|
|
163
|
-
if (VALID_TRANSFORM_IDS.has(v)) return v;
|
|
164
|
-
const normalized = v.replace(/\s+/g, '-');
|
|
165
|
-
if (VALID_TRANSFORM_IDS.has(normalized)) return normalized;
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
169
|
function resolveFrontmatterCaptionMode(value: unknown): CaptionMode | null {
|
|
170
170
|
if (typeof value !== 'string') return null;
|
|
171
171
|
const v = value.trim().toLowerCase();
|
|
@@ -202,7 +202,7 @@ export interface PreviewSettingsProviderProps {
|
|
|
202
202
|
* keys are the canonical names; the legacy keys are still read so existing
|
|
203
203
|
* documents keep working. Persistence (writes) uses only the squisq names. */
|
|
204
204
|
const FM_KEYS = {
|
|
205
|
-
theme: { canonical: 'squisq-theme', legacy: 'theme' as const },
|
|
205
|
+
theme: { canonical: 'squisq-theme', legacy: ['themeId', 'theme'] as const },
|
|
206
206
|
transform: { canonical: 'squisq-transform', legacy: 'transform-style' as const },
|
|
207
207
|
captions: { canonical: 'squisq-captions', legacy: 'caption-style' as const },
|
|
208
208
|
coverSlide: { canonical: 'squisq-cover-slide', legacy: 'cover-slide' as const },
|
|
@@ -223,7 +223,7 @@ export function PreviewSettingsProvider({
|
|
|
223
223
|
themeOverride,
|
|
224
224
|
}: PreviewSettingsProviderProps) {
|
|
225
225
|
const frontmatter = doc?.frontmatter;
|
|
226
|
-
const { markdownSource, setMarkdownSource } = useEditorContext();
|
|
226
|
+
const { markdownSource, setMarkdownSource, allowNarrate } = useEditorContext();
|
|
227
227
|
|
|
228
228
|
const persistFrontmatter = useCallback(
|
|
229
229
|
(updates: Record<string, string | null>) => {
|
|
@@ -245,35 +245,40 @@ export function PreviewSettingsProvider({
|
|
|
245
245
|
const activePreset = selectedPreset ?? fmPreset ?? 'landscape';
|
|
246
246
|
const activeViewport = VIEWPORT_PRESETS[activePreset];
|
|
247
247
|
|
|
248
|
-
// Display mode
|
|
248
|
+
// Display mode. A frontmatter-forced `narrate` clamps back to video when
|
|
249
|
+
// the host disabled the mode, so hostile frontmatter can't turn it on.
|
|
249
250
|
const fmMode = useMemo(() => resolveDisplayMode(frontmatter?.['display-mode']), [frontmatter]);
|
|
250
251
|
const [selectedDisplayMode, setSelectedDisplayMode] = useState<DisplayMode | null>(null);
|
|
251
252
|
useEffect(() => setSelectedDisplayMode(null), [fmMode]);
|
|
252
|
-
const
|
|
253
|
+
const requestedDisplayMode = selectedDisplayMode ?? fmMode ?? 'slideshow';
|
|
254
|
+
const activeDisplayMode =
|
|
255
|
+
requestedDisplayMode === 'narrate' && !allowNarrate ? 'video' : requestedDisplayMode;
|
|
253
256
|
|
|
254
257
|
// Custom themes (doc + browser library). `useCustomThemes` returns null when
|
|
255
|
-
// no provider is mounted;
|
|
258
|
+
// no provider is mounted; document-scoped themes still remain available.
|
|
256
259
|
const custom = useCustomThemes();
|
|
257
|
-
const
|
|
260
|
+
const docThemes = useMemo(
|
|
261
|
+
() => custom?.docThemes ?? doc?.customThemes ?? [],
|
|
262
|
+
[custom, doc?.customThemes],
|
|
263
|
+
);
|
|
264
|
+
const customThemes = useMemo(() => custom?.allThemes ?? docThemes, [custom, docThemes]);
|
|
258
265
|
const customIds = useMemo(() => new Set(customThemes.map((t) => t.id)), [customThemes]);
|
|
259
266
|
|
|
260
|
-
// Theme — persisted to `squisq-theme`
|
|
267
|
+
// Theme — persisted to `squisq-theme`; `themeId` / `theme` remain readable.
|
|
261
268
|
const fmTheme = useMemo(
|
|
262
|
-
() =>
|
|
263
|
-
resolveFrontmatterTheme(
|
|
264
|
-
readFrontmatterKey(frontmatter, FM_KEYS.theme.canonical, FM_KEYS.theme.legacy),
|
|
265
|
-
customIds,
|
|
266
|
-
),
|
|
269
|
+
() => resolveFrontmatterTheme(readFrontmatterThemeId(frontmatter), customIds),
|
|
267
270
|
[frontmatter, customIds],
|
|
268
271
|
);
|
|
269
272
|
const [selectedThemeId, setSelectedThemeId] = useState<string | null>(null);
|
|
270
273
|
useEffect(() => setSelectedThemeId(null), [fmTheme]);
|
|
271
274
|
const resolvedThemeId = selectedThemeId ?? fmTheme ?? 'standard';
|
|
272
|
-
// Doc
|
|
273
|
-
//
|
|
275
|
+
// Doc themes precede browser-library themes in `allThemes`; choosing a
|
|
276
|
+
// library-only entry copies it into the document below for portable export.
|
|
274
277
|
const resolvedTheme = useMemo(
|
|
275
|
-
() =>
|
|
276
|
-
|
|
278
|
+
() =>
|
|
279
|
+
customThemes.find((theme) => theme.id === resolvedThemeId) ??
|
|
280
|
+
resolveThemeForDoc(doc, resolvedThemeId),
|
|
281
|
+
[customThemes, doc, resolvedThemeId],
|
|
277
282
|
);
|
|
278
283
|
|
|
279
284
|
// In-progress theme from the designer dialog; previews live without mutating
|
|
@@ -291,9 +296,21 @@ export function PreviewSettingsProvider({
|
|
|
291
296
|
const handleSetThemeId = useCallback(
|
|
292
297
|
(id: string | null) => {
|
|
293
298
|
setSelectedThemeId(id);
|
|
294
|
-
if (id
|
|
299
|
+
if (id === null) return;
|
|
300
|
+
const selectedCustom = customThemes.find((theme) => theme.id === id);
|
|
301
|
+
const alreadyDocScoped = docThemes.some((theme) => theme.id === id);
|
|
302
|
+
const updates: Record<string, string | null> = {
|
|
303
|
+
[FM_KEYS.theme.canonical]: id,
|
|
304
|
+
[FM_KEYS.theme.legacy[0]]: null,
|
|
305
|
+
[FM_KEYS.theme.legacy[1]]: null,
|
|
306
|
+
};
|
|
307
|
+
if (selectedCustom && !alreadyDocScoped) {
|
|
308
|
+
updates[FRONTMATTER_CUSTOM_THEMES_KEY] =
|
|
309
|
+
writeCustomThemesToFrontmatter([...docThemes, selectedCustom]) ?? null;
|
|
310
|
+
}
|
|
311
|
+
persistFrontmatter(updates);
|
|
295
312
|
},
|
|
296
|
-
[persistFrontmatter],
|
|
313
|
+
[customThemes, docThemes, persistFrontmatter],
|
|
297
314
|
);
|
|
298
315
|
|
|
299
316
|
const openThemeDesigner = useCallback((theme: Theme | null) => {
|
|
@@ -305,27 +322,38 @@ export function PreviewSettingsProvider({
|
|
|
305
322
|
setPreviewTheme(null);
|
|
306
323
|
}, []);
|
|
307
324
|
const handleDesignerSave = useCallback(
|
|
308
|
-
(theme: Theme, target: ThemeSaveTarget) => {
|
|
325
|
+
(theme: Theme, target: ThemeSaveTarget, extras?: ThemeSaveExtras) => {
|
|
309
326
|
if (target === 'library') {
|
|
327
|
+
// Imported slide layouts are doc-scoped in v1; the dialog hints at this.
|
|
310
328
|
custom?.upsertLibraryTheme(theme);
|
|
311
329
|
} else {
|
|
312
|
-
// Write the theme payload
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
330
|
+
// Write the theme payload, its selection, AND any imported layout
|
|
331
|
+
// templates in a SINGLE frontmatter update. Separate
|
|
332
|
+
// `setMarkdownSource` calls would each derive from the same stale
|
|
333
|
+
// source, so later writes would clobber earlier ones.
|
|
316
334
|
const docThemes = custom?.docThemes ?? [];
|
|
317
335
|
const idx = docThemes.findIndex((t) => t.id === theme.id);
|
|
318
336
|
const nextThemes =
|
|
319
337
|
idx >= 0 ? docThemes.map((t, i) => (i === idx ? theme : t)) : [...docThemes, theme];
|
|
320
|
-
|
|
338
|
+
const updates: Record<string, string | null> = {
|
|
321
339
|
[FRONTMATTER_CUSTOM_THEMES_KEY]: writeCustomThemesToFrontmatter(nextThemes) ?? null,
|
|
322
340
|
[FM_KEYS.theme.canonical]: theme.id,
|
|
323
|
-
}
|
|
341
|
+
};
|
|
342
|
+
if (extras?.templates && extras.templates.length > 0) {
|
|
343
|
+
const existing = doc?.customTemplates ?? [];
|
|
344
|
+
const merged = [
|
|
345
|
+
...existing.filter((t) => !extras.templates!.some((n) => n.name === t.name)),
|
|
346
|
+
...extras.templates,
|
|
347
|
+
];
|
|
348
|
+
updates[FRONTMATTER_CUSTOM_TEMPLATES_KEY] =
|
|
349
|
+
writeCustomTemplatesToFrontmatter(merged) ?? null;
|
|
350
|
+
}
|
|
351
|
+
persistFrontmatter(updates);
|
|
324
352
|
setSelectedThemeId(theme.id);
|
|
325
353
|
}
|
|
326
354
|
closeThemeDesigner();
|
|
327
355
|
},
|
|
328
|
-
[custom, persistFrontmatter, closeThemeDesigner],
|
|
356
|
+
[custom, doc, persistFrontmatter, closeThemeDesigner],
|
|
329
357
|
);
|
|
330
358
|
const deleteCustomTheme = useCallback(
|
|
331
359
|
(id: string) => {
|
|
@@ -338,7 +366,7 @@ export function PreviewSettingsProvider({
|
|
|
338
366
|
// Transform — persisted to `squisq-transform` (legacy `transform-style` read for compat)
|
|
339
367
|
const fmTransform = useMemo(
|
|
340
368
|
() =>
|
|
341
|
-
|
|
369
|
+
resolvePersistedTransformStyleId(
|
|
342
370
|
readFrontmatterKey(frontmatter, FM_KEYS.transform.canonical, FM_KEYS.transform.legacy),
|
|
343
371
|
),
|
|
344
372
|
[frontmatter],
|
|
@@ -496,27 +524,64 @@ const FORMAT_SWITCH_OPTIONS: { key: ViewportPreset; label: string; w: number; h:
|
|
|
496
524
|
{ key: 'standard', label: '4:3', w: 12, h: 9 },
|
|
497
525
|
];
|
|
498
526
|
|
|
499
|
-
const DISPLAY_MODE_OPTIONS: {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
527
|
+
const DISPLAY_MODE_OPTIONS: {
|
|
528
|
+
key: DisplayMode;
|
|
529
|
+
label: string;
|
|
530
|
+
icon: string;
|
|
531
|
+
summary: string;
|
|
532
|
+
}[] = [
|
|
533
|
+
{
|
|
534
|
+
key: 'slideshow',
|
|
535
|
+
label: 'Slideshow',
|
|
536
|
+
icon: 'fa-solid fa-images',
|
|
537
|
+
summary: 'Present designed slides one at a time.',
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
key: 'video',
|
|
541
|
+
label: 'Video',
|
|
542
|
+
icon: 'fa-solid fa-circle-play',
|
|
543
|
+
summary: 'Play an automatically timed presentation.',
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
key: 'linear',
|
|
547
|
+
label: 'Page',
|
|
548
|
+
icon: 'fa-solid fa-window-maximize',
|
|
549
|
+
summary: 'Scroll through the fully designed page.',
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
key: 'page',
|
|
553
|
+
label: 'Document',
|
|
554
|
+
icon: 'fa-solid fa-align-left',
|
|
555
|
+
summary: 'Read a clean, text-first document.',
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
key: 'narrate',
|
|
559
|
+
label: 'Narrate',
|
|
560
|
+
icon: 'fa-solid fa-microphone-lines',
|
|
561
|
+
summary: 'Speak with a voice-paced teleprompter.',
|
|
562
|
+
},
|
|
504
563
|
];
|
|
505
564
|
|
|
565
|
+
export function displayModeLabel(mode: DisplayMode): string {
|
|
566
|
+
return DISPLAY_MODE_OPTIONS.find((option) => option.key === mode)?.label ?? 'Slideshow';
|
|
567
|
+
}
|
|
568
|
+
|
|
506
569
|
const TRANSFORM_STYLE_OPTIONS = [
|
|
507
570
|
{ key: '', label: 'None' },
|
|
508
571
|
...getTransformStyleSummaries().map((s) => ({ key: s.id, label: s.name })),
|
|
509
572
|
];
|
|
510
573
|
|
|
574
|
+
const SUMMARIZE_TOOLTIP =
|
|
575
|
+
'Extract and summarize content for presentation with these Use modes. Your underlying content is not changed.';
|
|
576
|
+
|
|
511
577
|
/**
|
|
512
578
|
* Left-to-right priority order for the preview controls. As the toolbar
|
|
513
579
|
* narrows, controls drop into the overflow menu from the END of this list
|
|
514
|
-
* first (Cover, then Captions, …).
|
|
515
|
-
*
|
|
516
|
-
* constrained.
|
|
580
|
+
* first (Cover, then Captions, …). Aspect ratio stays inline the longest, but
|
|
581
|
+
* still collapses into the same menu when the toolbar is very constrained.
|
|
517
582
|
*/
|
|
518
|
-
type ControlKey = '
|
|
519
|
-
const CONTROL_KEYS: ControlKey[] = ['
|
|
583
|
+
type ControlKey = 'format' | 'theme' | 'transform' | 'captions' | 'cover';
|
|
584
|
+
const CONTROL_KEYS: ControlKey[] = ['format', 'theme', 'transform', 'captions', 'cover'];
|
|
520
585
|
|
|
521
586
|
const PREVIEW_POPOVER_GAP = 4;
|
|
522
587
|
const PREVIEW_POPOVER_MARGIN = 8;
|
|
@@ -681,16 +746,6 @@ export function PreviewToolbarControls() {
|
|
|
681
746
|
// label-over-control layout used inside the overflow popover.
|
|
682
747
|
const renderControl = (key: ControlKey, compact: boolean): ReactNode => {
|
|
683
748
|
switch (key) {
|
|
684
|
-
case 'mode':
|
|
685
|
-
return (
|
|
686
|
-
<div
|
|
687
|
-
key="mode"
|
|
688
|
-
className={`squisq-preview-control squisq-preview-control--seg${compact ? ' squisq-preview-control--compact' : ''}`}
|
|
689
|
-
>
|
|
690
|
-
{compact && <label style={labelStyle}>Mode:</label>}
|
|
691
|
-
<PreviewModeSwitch />
|
|
692
|
-
</div>
|
|
693
|
-
);
|
|
694
749
|
case 'format':
|
|
695
750
|
return (
|
|
696
751
|
<div
|
|
@@ -747,7 +802,8 @@ export function PreviewToolbarControls() {
|
|
|
747
802
|
return (
|
|
748
803
|
<PreviewSelect
|
|
749
804
|
key="transform"
|
|
750
|
-
label="
|
|
805
|
+
label="Summarize"
|
|
806
|
+
labelTooltip={SUMMARIZE_TOOLTIP}
|
|
751
807
|
value={s.activeTransformStyle}
|
|
752
808
|
options={TRANSFORM_STYLE_OPTIONS}
|
|
753
809
|
onChange={(v) => s.setSelectedTransformStyle(v)}
|
|
@@ -798,7 +854,6 @@ export function PreviewToolbarControls() {
|
|
|
798
854
|
key="cover"
|
|
799
855
|
className={`squisq-preview-control${compact ? ' squisq-preview-control--compact' : ''}`}
|
|
800
856
|
>
|
|
801
|
-
<label style={labelStyle}>Cover:</label>
|
|
802
857
|
<label className="squisq-preview-checkbox">
|
|
803
858
|
<input
|
|
804
859
|
type="checkbox"
|
|
@@ -876,15 +931,17 @@ export function PreviewToolbarControls() {
|
|
|
876
931
|
}
|
|
877
932
|
|
|
878
933
|
/**
|
|
879
|
-
* Segmented display-mode switch
|
|
880
|
-
*
|
|
881
|
-
*
|
|
934
|
+
* Segmented display-mode switch retained as a public, embeddable control.
|
|
935
|
+
* The editor shell uses {@link PreviewModeMenu} beside its Use tab instead.
|
|
936
|
+
* Narrate is hidden when the host disables `allowNarrate`.
|
|
882
937
|
*/
|
|
883
938
|
export function PreviewModeSwitch() {
|
|
884
939
|
const s = usePreviewSettings();
|
|
940
|
+
const { allowNarrate } = useEditorContext();
|
|
941
|
+
const options = DISPLAY_MODE_OPTIONS.filter((opt) => opt.key !== 'narrate' || allowNarrate);
|
|
885
942
|
return (
|
|
886
943
|
<div className="squisq-preview-seg" role="group" aria-label="Display mode">
|
|
887
|
-
{
|
|
944
|
+
{options.map((opt) => {
|
|
888
945
|
const active = s.activeDisplayMode === opt.key;
|
|
889
946
|
return (
|
|
890
947
|
<button
|
|
@@ -902,6 +959,185 @@ export function PreviewModeSwitch() {
|
|
|
902
959
|
);
|
|
903
960
|
}
|
|
904
961
|
|
|
962
|
+
const USE_MODE_MENU_WIDTH = 340;
|
|
963
|
+
const USE_MODE_MENU_GAP = 4;
|
|
964
|
+
const USE_MODE_MENU_MARGIN = 8;
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Dropdown trigger rendered directly beside the Use tab. Selecting a mode
|
|
968
|
+
* also enters the Use view, so the menu works from Write and Source as well
|
|
969
|
+
* as from an already-active preview.
|
|
970
|
+
*/
|
|
971
|
+
export interface PreviewModeMenuProps {
|
|
972
|
+
/** Incremented by the parent to open the menu from another control. */
|
|
973
|
+
openRequest?: number;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
export function PreviewModeMenu({ openRequest = 0 }: PreviewModeMenuProps) {
|
|
977
|
+
const s = usePreviewSettings();
|
|
978
|
+
const { allowNarrate, colorScheme, setActiveView } = useEditorContext();
|
|
979
|
+
const options = DISPLAY_MODE_OPTIONS.filter((opt) => opt.key !== 'narrate' || allowNarrate);
|
|
980
|
+
const activeLabel = displayModeLabel(s.activeDisplayMode);
|
|
981
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
982
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
983
|
+
const itemIdPrefix = useId();
|
|
984
|
+
const [open, setOpen] = useState(false);
|
|
985
|
+
const [anchor, setAnchor] = useState<{ top: number; left: number } | null>(null);
|
|
986
|
+
|
|
987
|
+
const updatePosition = useCallback(() => {
|
|
988
|
+
const trigger = triggerRef.current;
|
|
989
|
+
if (!trigger) return;
|
|
990
|
+
const rect = trigger.getBoundingClientRect();
|
|
991
|
+
const menuWidth = Math.min(USE_MODE_MENU_WIDTH, window.innerWidth - USE_MODE_MENU_MARGIN * 2);
|
|
992
|
+
const maxLeft = Math.max(
|
|
993
|
+
USE_MODE_MENU_MARGIN,
|
|
994
|
+
window.innerWidth - menuWidth - USE_MODE_MENU_MARGIN,
|
|
995
|
+
);
|
|
996
|
+
setAnchor({
|
|
997
|
+
top: rect.bottom + USE_MODE_MENU_GAP,
|
|
998
|
+
left: Math.min(Math.max(USE_MODE_MENU_MARGIN, rect.right - menuWidth), maxLeft),
|
|
999
|
+
});
|
|
1000
|
+
}, []);
|
|
1001
|
+
|
|
1002
|
+
const closeMenu = useCallback((restoreFocus = false) => {
|
|
1003
|
+
setOpen(false);
|
|
1004
|
+
setAnchor(null);
|
|
1005
|
+
if (restoreFocus) triggerRef.current?.focus();
|
|
1006
|
+
}, []);
|
|
1007
|
+
|
|
1008
|
+
const openMenu = useCallback(() => {
|
|
1009
|
+
updatePosition();
|
|
1010
|
+
setOpen(true);
|
|
1011
|
+
}, [updatePosition]);
|
|
1012
|
+
|
|
1013
|
+
useEffect(() => {
|
|
1014
|
+
if (openRequest > 0) openMenu();
|
|
1015
|
+
}, [openMenu, openRequest]);
|
|
1016
|
+
|
|
1017
|
+
useEffect(() => {
|
|
1018
|
+
if (!open) return;
|
|
1019
|
+
const handlePointerDown = (event: MouseEvent) => {
|
|
1020
|
+
const target = event.target as Node;
|
|
1021
|
+
if (triggerRef.current?.contains(target) || menuRef.current?.contains(target)) return;
|
|
1022
|
+
closeMenu();
|
|
1023
|
+
};
|
|
1024
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
1025
|
+
if (event.key !== 'Escape') return;
|
|
1026
|
+
event.preventDefault();
|
|
1027
|
+
closeMenu(true);
|
|
1028
|
+
};
|
|
1029
|
+
document.addEventListener('mousedown', handlePointerDown);
|
|
1030
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
1031
|
+
window.addEventListener('resize', updatePosition);
|
|
1032
|
+
window.addEventListener('scroll', updatePosition, true);
|
|
1033
|
+
return () => {
|
|
1034
|
+
document.removeEventListener('mousedown', handlePointerDown);
|
|
1035
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
1036
|
+
window.removeEventListener('resize', updatePosition);
|
|
1037
|
+
window.removeEventListener('scroll', updatePosition, true);
|
|
1038
|
+
};
|
|
1039
|
+
}, [closeMenu, open, updatePosition]);
|
|
1040
|
+
|
|
1041
|
+
useLayoutEffect(() => {
|
|
1042
|
+
if (!open || !anchor) return;
|
|
1043
|
+
const selected = menuRef.current?.querySelector<HTMLButtonElement>('[aria-checked="true"]');
|
|
1044
|
+
const first = menuRef.current?.querySelector<HTMLButtonElement>('[role="menuitemradio"]');
|
|
1045
|
+
(selected ?? first)?.focus();
|
|
1046
|
+
}, [anchor, open]);
|
|
1047
|
+
|
|
1048
|
+
return (
|
|
1049
|
+
<>
|
|
1050
|
+
<button
|
|
1051
|
+
ref={triggerRef}
|
|
1052
|
+
type="button"
|
|
1053
|
+
className={`squisq-use-mode-trigger${open ? ' squisq-use-mode-trigger--open' : ''}`}
|
|
1054
|
+
aria-label="Choose Use mode"
|
|
1055
|
+
aria-haspopup="menu"
|
|
1056
|
+
aria-expanded={open}
|
|
1057
|
+
title={`Use mode: ${activeLabel}`}
|
|
1058
|
+
onClick={() => (open ? closeMenu() : openMenu())}
|
|
1059
|
+
onKeyDown={(event) => {
|
|
1060
|
+
if (event.key !== 'ArrowDown') return;
|
|
1061
|
+
event.preventDefault();
|
|
1062
|
+
openMenu();
|
|
1063
|
+
}}
|
|
1064
|
+
>
|
|
1065
|
+
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
|
1066
|
+
<path d="M2 3.5 5 6.5 8 3.5" fill="none" stroke="currentColor" strokeWidth="1.4" />
|
|
1067
|
+
</svg>
|
|
1068
|
+
</button>
|
|
1069
|
+
{open &&
|
|
1070
|
+
anchor &&
|
|
1071
|
+
createPortal(
|
|
1072
|
+
<div
|
|
1073
|
+
ref={menuRef}
|
|
1074
|
+
className="squisq-use-mode-menu"
|
|
1075
|
+
data-theme={colorScheme}
|
|
1076
|
+
role="menu"
|
|
1077
|
+
aria-label="Use mode"
|
|
1078
|
+
style={{ top: anchor.top, left: anchor.left }}
|
|
1079
|
+
onKeyDown={(event) => {
|
|
1080
|
+
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;
|
|
1081
|
+
event.preventDefault();
|
|
1082
|
+
const items = Array.from(
|
|
1083
|
+
event.currentTarget.querySelectorAll<HTMLButtonElement>('[role="menuitemradio"]'),
|
|
1084
|
+
);
|
|
1085
|
+
if (items.length === 0) return;
|
|
1086
|
+
const currentIndex = items.indexOf(document.activeElement as HTMLButtonElement);
|
|
1087
|
+
const nextIndex =
|
|
1088
|
+
event.key === 'Home'
|
|
1089
|
+
? 0
|
|
1090
|
+
: event.key === 'End'
|
|
1091
|
+
? items.length - 1
|
|
1092
|
+
: event.key === 'ArrowUp'
|
|
1093
|
+
? (currentIndex - 1 + items.length) % items.length
|
|
1094
|
+
: (currentIndex + 1) % items.length;
|
|
1095
|
+
items[nextIndex]?.focus();
|
|
1096
|
+
}}
|
|
1097
|
+
>
|
|
1098
|
+
{options.map((option) => {
|
|
1099
|
+
const selected = option.key === s.activeDisplayMode;
|
|
1100
|
+
const labelId = `${itemIdPrefix}-${option.key}-label`;
|
|
1101
|
+
const summaryId = `${itemIdPrefix}-${option.key}-summary`;
|
|
1102
|
+
return (
|
|
1103
|
+
<button
|
|
1104
|
+
key={option.key}
|
|
1105
|
+
type="button"
|
|
1106
|
+
className={`squisq-use-mode-menu-item${selected ? ' squisq-use-mode-menu-item--selected' : ''}`}
|
|
1107
|
+
role="menuitemradio"
|
|
1108
|
+
aria-checked={selected}
|
|
1109
|
+
aria-labelledby={labelId}
|
|
1110
|
+
aria-describedby={summaryId}
|
|
1111
|
+
onClick={() => {
|
|
1112
|
+
s.setSelectedDisplayMode(option.key);
|
|
1113
|
+
setActiveView('preview');
|
|
1114
|
+
closeMenu(true);
|
|
1115
|
+
}}
|
|
1116
|
+
>
|
|
1117
|
+
<span className="squisq-use-mode-menu-icon" aria-hidden="true">
|
|
1118
|
+
<Icon icon={option.icon} />
|
|
1119
|
+
</span>
|
|
1120
|
+
<span className="squisq-use-mode-menu-copy">
|
|
1121
|
+
<span id={labelId} className="squisq-use-mode-menu-label">
|
|
1122
|
+
{option.label}
|
|
1123
|
+
</span>
|
|
1124
|
+
<span id={summaryId} className="squisq-use-mode-menu-summary">
|
|
1125
|
+
{option.summary}
|
|
1126
|
+
</span>
|
|
1127
|
+
</span>
|
|
1128
|
+
<span className="squisq-use-mode-menu-check" aria-hidden="true">
|
|
1129
|
+
{selected && <Icon icon="fa-solid fa-check" />}
|
|
1130
|
+
</span>
|
|
1131
|
+
</button>
|
|
1132
|
+
);
|
|
1133
|
+
})}
|
|
1134
|
+
</div>,
|
|
1135
|
+
document.body,
|
|
1136
|
+
)}
|
|
1137
|
+
</>
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
905
1141
|
/** A simple aspect-ratio glyph: a centered rounded rectangle of `w`×`h` in a
|
|
906
1142
|
* 16×16 box, so 16:9 reads as a wide box, 1:1 a square, 9:16 a tall box. */
|
|
907
1143
|
function AspectIcon({ w, h }: { w: number; h: number }) {
|
|
@@ -951,12 +1187,14 @@ export function PreviewFormatSwitch() {
|
|
|
951
1187
|
|
|
952
1188
|
function PreviewSelect({
|
|
953
1189
|
label,
|
|
1190
|
+
labelTooltip,
|
|
954
1191
|
value,
|
|
955
1192
|
options,
|
|
956
1193
|
onChange,
|
|
957
1194
|
compact,
|
|
958
1195
|
}: {
|
|
959
1196
|
label: string;
|
|
1197
|
+
labelTooltip?: string;
|
|
960
1198
|
value: string;
|
|
961
1199
|
options: { key: string; label: string }[];
|
|
962
1200
|
onChange: (value: string) => void;
|
|
@@ -964,7 +1202,9 @@ function PreviewSelect({
|
|
|
964
1202
|
}) {
|
|
965
1203
|
return (
|
|
966
1204
|
<div className={`squisq-preview-control${compact ? ' squisq-preview-control--compact' : ''}`}>
|
|
967
|
-
<label style={labelStyle}
|
|
1205
|
+
<label style={labelStyle} title={labelTooltip}>
|
|
1206
|
+
{label}:
|
|
1207
|
+
</label>
|
|
968
1208
|
<select value={value} onChange={(e) => onChange(e.target.value)} style={selectStyle}>
|
|
969
1209
|
{options.map((o) => (
|
|
970
1210
|
<option key={o.key} value={o.key}>
|