@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
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,28 @@ 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';
|
|
50
|
+
import {
|
|
51
|
+
FRONTMATTER_SETTING_DEFAULTS,
|
|
52
|
+
FRONTMATTER_SETTING_KEYS,
|
|
53
|
+
omitFrontmatterDefault,
|
|
54
|
+
} from './frontmatterSettings';
|
|
40
55
|
|
|
41
56
|
// ── Context ──────────────────────────────────────────────────────
|
|
42
57
|
|
|
@@ -81,7 +96,7 @@ export interface PreviewSettings {
|
|
|
81
96
|
export interface ThemeDesignerConfig {
|
|
82
97
|
value: Theme | null;
|
|
83
98
|
onChange: (theme: Theme) => void;
|
|
84
|
-
onSave: (theme: Theme, target: ThemeSaveTarget) => void;
|
|
99
|
+
onSave: (theme: Theme, target: ThemeSaveTarget, extras?: ThemeSaveExtras) => void;
|
|
85
100
|
onClose: () => void;
|
|
86
101
|
}
|
|
87
102
|
|
|
@@ -127,8 +142,9 @@ function resolveRenderAs(value: unknown): ViewportPreset | null {
|
|
|
127
142
|
function resolveDisplayMode(value: unknown): DisplayMode | null {
|
|
128
143
|
if (typeof value !== 'string') return null;
|
|
129
144
|
const v = value.trim().toLowerCase();
|
|
130
|
-
if (v === 'video' || v === 'slideshow' || v === 'linear') return v;
|
|
145
|
+
if (v === 'video' || v === 'slideshow' || v === 'linear' || v === 'narrate') return v;
|
|
131
146
|
if (v === 'slides' || v === 'presentation' || v === 'deck') return 'slideshow';
|
|
147
|
+
if (v === 'teleprompter' || v === 'prompter') return 'narrate';
|
|
132
148
|
// Frontmatter uses product-facing names: Document is the plain text/HTML
|
|
133
149
|
// preview, Page is the styled Squisq page view. The raw DisplayMode values
|
|
134
150
|
// are older and remain stable for the public React API.
|
|
@@ -155,17 +171,6 @@ function resolveFrontmatterTheme(value: unknown, customIds?: Set<string>): strin
|
|
|
155
171
|
return null;
|
|
156
172
|
}
|
|
157
173
|
|
|
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
174
|
function resolveFrontmatterCaptionMode(value: unknown): CaptionMode | null {
|
|
170
175
|
if (typeof value !== 'string') return null;
|
|
171
176
|
const v = value.trim().toLowerCase();
|
|
@@ -198,16 +203,6 @@ export interface PreviewSettingsProviderProps {
|
|
|
198
203
|
themeOverride?: Theme | null;
|
|
199
204
|
}
|
|
200
205
|
|
|
201
|
-
/** Frontmatter keys we read/write for preview settings. The squisq-prefixed
|
|
202
|
-
* keys are the canonical names; the legacy keys are still read so existing
|
|
203
|
-
* documents keep working. Persistence (writes) uses only the squisq names. */
|
|
204
|
-
const FM_KEYS = {
|
|
205
|
-
theme: { canonical: 'squisq-theme', legacy: 'theme' as const },
|
|
206
|
-
transform: { canonical: 'squisq-transform', legacy: 'transform-style' as const },
|
|
207
|
-
captions: { canonical: 'squisq-captions', legacy: 'caption-style' as const },
|
|
208
|
-
coverSlide: { canonical: 'squisq-cover-slide', legacy: 'cover-slide' as const },
|
|
209
|
-
} as const;
|
|
210
|
-
|
|
211
206
|
function readFrontmatterKey(
|
|
212
207
|
fm: Record<string, unknown> | undefined,
|
|
213
208
|
canonical: string,
|
|
@@ -223,10 +218,10 @@ export function PreviewSettingsProvider({
|
|
|
223
218
|
themeOverride,
|
|
224
219
|
}: PreviewSettingsProviderProps) {
|
|
225
220
|
const frontmatter = doc?.frontmatter;
|
|
226
|
-
const { markdownSource, setMarkdownSource } = useEditorContext();
|
|
221
|
+
const { markdownSource, setMarkdownSource, allowNarrate } = useEditorContext();
|
|
227
222
|
|
|
228
223
|
const persistFrontmatter = useCallback(
|
|
229
|
-
(updates: Record<string, string | null>) => {
|
|
224
|
+
(updates: Record<string, string | number | boolean | null | undefined>) => {
|
|
230
225
|
const next = setFrontmatterValues(markdownSource, updates);
|
|
231
226
|
if (next !== markdownSource) {
|
|
232
227
|
setMarkdownSource(next);
|
|
@@ -245,35 +240,40 @@ export function PreviewSettingsProvider({
|
|
|
245
240
|
const activePreset = selectedPreset ?? fmPreset ?? 'landscape';
|
|
246
241
|
const activeViewport = VIEWPORT_PRESETS[activePreset];
|
|
247
242
|
|
|
248
|
-
// Display mode
|
|
243
|
+
// Display mode. A frontmatter-forced `narrate` clamps back to video when
|
|
244
|
+
// the host disabled the mode, so hostile frontmatter can't turn it on.
|
|
249
245
|
const fmMode = useMemo(() => resolveDisplayMode(frontmatter?.['display-mode']), [frontmatter]);
|
|
250
246
|
const [selectedDisplayMode, setSelectedDisplayMode] = useState<DisplayMode | null>(null);
|
|
251
247
|
useEffect(() => setSelectedDisplayMode(null), [fmMode]);
|
|
252
|
-
const
|
|
248
|
+
const requestedDisplayMode = selectedDisplayMode ?? fmMode ?? 'slideshow';
|
|
249
|
+
const activeDisplayMode =
|
|
250
|
+
requestedDisplayMode === 'narrate' && !allowNarrate ? 'video' : requestedDisplayMode;
|
|
253
251
|
|
|
254
252
|
// Custom themes (doc + browser library). `useCustomThemes` returns null when
|
|
255
|
-
// no provider is mounted;
|
|
253
|
+
// no provider is mounted; document-scoped themes still remain available.
|
|
256
254
|
const custom = useCustomThemes();
|
|
257
|
-
const
|
|
255
|
+
const docThemes = useMemo(
|
|
256
|
+
() => custom?.docThemes ?? doc?.customThemes ?? [],
|
|
257
|
+
[custom, doc?.customThemes],
|
|
258
|
+
);
|
|
259
|
+
const customThemes = useMemo(() => custom?.allThemes ?? docThemes, [custom, docThemes]);
|
|
258
260
|
const customIds = useMemo(() => new Set(customThemes.map((t) => t.id)), [customThemes]);
|
|
259
261
|
|
|
260
|
-
// Theme — persisted to `squisq-theme`
|
|
262
|
+
// Theme — persisted to `squisq-theme`; `themeId` / `theme` remain readable.
|
|
261
263
|
const fmTheme = useMemo(
|
|
262
|
-
() =>
|
|
263
|
-
resolveFrontmatterTheme(
|
|
264
|
-
readFrontmatterKey(frontmatter, FM_KEYS.theme.canonical, FM_KEYS.theme.legacy),
|
|
265
|
-
customIds,
|
|
266
|
-
),
|
|
264
|
+
() => resolveFrontmatterTheme(readFrontmatterThemeId(frontmatter), customIds),
|
|
267
265
|
[frontmatter, customIds],
|
|
268
266
|
);
|
|
269
267
|
const [selectedThemeId, setSelectedThemeId] = useState<string | null>(null);
|
|
270
268
|
useEffect(() => setSelectedThemeId(null), [fmTheme]);
|
|
271
|
-
const resolvedThemeId = selectedThemeId ?? fmTheme ??
|
|
272
|
-
// Doc
|
|
273
|
-
//
|
|
269
|
+
const resolvedThemeId = selectedThemeId ?? fmTheme ?? FRONTMATTER_SETTING_DEFAULTS.theme;
|
|
270
|
+
// Doc themes precede browser-library themes in `allThemes`; choosing a
|
|
271
|
+
// library-only entry copies it into the document below for portable export.
|
|
274
272
|
const resolvedTheme = useMemo(
|
|
275
|
-
() =>
|
|
276
|
-
|
|
273
|
+
() =>
|
|
274
|
+
customThemes.find((theme) => theme.id === resolvedThemeId) ??
|
|
275
|
+
resolveThemeForDoc(doc, resolvedThemeId),
|
|
276
|
+
[customThemes, doc, resolvedThemeId],
|
|
277
277
|
);
|
|
278
278
|
|
|
279
279
|
// In-progress theme from the designer dialog; previews live without mutating
|
|
@@ -291,9 +291,24 @@ export function PreviewSettingsProvider({
|
|
|
291
291
|
const handleSetThemeId = useCallback(
|
|
292
292
|
(id: string | null) => {
|
|
293
293
|
setSelectedThemeId(id);
|
|
294
|
-
if (id
|
|
294
|
+
if (id === null) return;
|
|
295
|
+
const selectedCustom = customThemes.find((theme) => theme.id === id);
|
|
296
|
+
const alreadyDocScoped = docThemes.some((theme) => theme.id === id);
|
|
297
|
+
const updates: Record<string, string | null> = {
|
|
298
|
+
[FRONTMATTER_SETTING_KEYS.theme.canonical]: omitFrontmatterDefault(
|
|
299
|
+
id,
|
|
300
|
+
FRONTMATTER_SETTING_DEFAULTS.theme,
|
|
301
|
+
),
|
|
302
|
+
[FRONTMATTER_SETTING_KEYS.theme.legacy[0]]: null,
|
|
303
|
+
[FRONTMATTER_SETTING_KEYS.theme.legacy[1]]: null,
|
|
304
|
+
};
|
|
305
|
+
if (selectedCustom && !alreadyDocScoped) {
|
|
306
|
+
updates[FRONTMATTER_CUSTOM_THEMES_KEY] =
|
|
307
|
+
writeCustomThemesToFrontmatter([...docThemes, selectedCustom]) ?? null;
|
|
308
|
+
}
|
|
309
|
+
persistFrontmatter(updates);
|
|
295
310
|
},
|
|
296
|
-
[persistFrontmatter],
|
|
311
|
+
[customThemes, docThemes, persistFrontmatter],
|
|
297
312
|
);
|
|
298
313
|
|
|
299
314
|
const openThemeDesigner = useCallback((theme: Theme | null) => {
|
|
@@ -305,27 +320,38 @@ export function PreviewSettingsProvider({
|
|
|
305
320
|
setPreviewTheme(null);
|
|
306
321
|
}, []);
|
|
307
322
|
const handleDesignerSave = useCallback(
|
|
308
|
-
(theme: Theme, target: ThemeSaveTarget) => {
|
|
323
|
+
(theme: Theme, target: ThemeSaveTarget, extras?: ThemeSaveExtras) => {
|
|
309
324
|
if (target === 'library') {
|
|
325
|
+
// Imported slide layouts are doc-scoped in v1; the dialog hints at this.
|
|
310
326
|
custom?.upsertLibraryTheme(theme);
|
|
311
327
|
} else {
|
|
312
|
-
// Write the theme payload
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
328
|
+
// Write the theme payload, its selection, AND any imported layout
|
|
329
|
+
// templates in a SINGLE frontmatter update. Separate
|
|
330
|
+
// `setMarkdownSource` calls would each derive from the same stale
|
|
331
|
+
// source, so later writes would clobber earlier ones.
|
|
316
332
|
const docThemes = custom?.docThemes ?? [];
|
|
317
333
|
const idx = docThemes.findIndex((t) => t.id === theme.id);
|
|
318
334
|
const nextThemes =
|
|
319
335
|
idx >= 0 ? docThemes.map((t, i) => (i === idx ? theme : t)) : [...docThemes, theme];
|
|
320
|
-
|
|
336
|
+
const updates: Record<string, string | null> = {
|
|
321
337
|
[FRONTMATTER_CUSTOM_THEMES_KEY]: writeCustomThemesToFrontmatter(nextThemes) ?? null,
|
|
322
|
-
[
|
|
323
|
-
}
|
|
338
|
+
[FRONTMATTER_SETTING_KEYS.theme.canonical]: theme.id,
|
|
339
|
+
};
|
|
340
|
+
if (extras?.templates && extras.templates.length > 0) {
|
|
341
|
+
const existing = doc?.customTemplates ?? [];
|
|
342
|
+
const merged = [
|
|
343
|
+
...existing.filter((t) => !extras.templates!.some((n) => n.name === t.name)),
|
|
344
|
+
...extras.templates,
|
|
345
|
+
];
|
|
346
|
+
updates[FRONTMATTER_CUSTOM_TEMPLATES_KEY] =
|
|
347
|
+
writeCustomTemplatesToFrontmatter(merged) ?? null;
|
|
348
|
+
}
|
|
349
|
+
persistFrontmatter(updates);
|
|
324
350
|
setSelectedThemeId(theme.id);
|
|
325
351
|
}
|
|
326
352
|
closeThemeDesigner();
|
|
327
353
|
},
|
|
328
|
-
[custom, persistFrontmatter, closeThemeDesigner],
|
|
354
|
+
[custom, doc, persistFrontmatter, closeThemeDesigner],
|
|
329
355
|
);
|
|
330
356
|
const deleteCustomTheme = useCallback(
|
|
331
357
|
(id: string) => {
|
|
@@ -338,8 +364,12 @@ export function PreviewSettingsProvider({
|
|
|
338
364
|
// Transform — persisted to `squisq-transform` (legacy `transform-style` read for compat)
|
|
339
365
|
const fmTransform = useMemo(
|
|
340
366
|
() =>
|
|
341
|
-
|
|
342
|
-
readFrontmatterKey(
|
|
367
|
+
resolvePersistedTransformStyleId(
|
|
368
|
+
readFrontmatterKey(
|
|
369
|
+
frontmatter,
|
|
370
|
+
FRONTMATTER_SETTING_KEYS.transform.canonical,
|
|
371
|
+
FRONTMATTER_SETTING_KEYS.transform.legacy,
|
|
372
|
+
),
|
|
343
373
|
),
|
|
344
374
|
[frontmatter],
|
|
345
375
|
);
|
|
@@ -351,7 +381,13 @@ export function PreviewSettingsProvider({
|
|
|
351
381
|
setSelectedTransformStyle(id);
|
|
352
382
|
if (id !== null) {
|
|
353
383
|
// Empty string = "None" — remove the key rather than writing a blank value.
|
|
354
|
-
persistFrontmatter({
|
|
384
|
+
persistFrontmatter({
|
|
385
|
+
[FRONTMATTER_SETTING_KEYS.transform.canonical]: omitFrontmatterDefault(
|
|
386
|
+
id,
|
|
387
|
+
FRONTMATTER_SETTING_DEFAULTS.transform,
|
|
388
|
+
),
|
|
389
|
+
[FRONTMATTER_SETTING_KEYS.transform.legacy]: null,
|
|
390
|
+
});
|
|
355
391
|
}
|
|
356
392
|
},
|
|
357
393
|
[persistFrontmatter],
|
|
@@ -363,19 +399,30 @@ export function PreviewSettingsProvider({
|
|
|
363
399
|
const fmCaptionMode = useMemo(
|
|
364
400
|
() =>
|
|
365
401
|
resolveFrontmatterCaptionMode(
|
|
366
|
-
readFrontmatterKey(
|
|
402
|
+
readFrontmatterKey(
|
|
403
|
+
frontmatter,
|
|
404
|
+
FRONTMATTER_SETTING_KEYS.captions.canonical,
|
|
405
|
+
FRONTMATTER_SETTING_KEYS.captions.legacy,
|
|
406
|
+
),
|
|
367
407
|
),
|
|
368
408
|
[frontmatter],
|
|
369
409
|
);
|
|
370
410
|
const [selectedCaptionMode, setSelectedCaptionMode] = useState<CaptionMode | null>(null);
|
|
371
411
|
useEffect(() => setSelectedCaptionMode(null), [fmCaptionMode]);
|
|
372
|
-
const activeCaptionMode =
|
|
412
|
+
const activeCaptionMode =
|
|
413
|
+
selectedCaptionMode ?? fmCaptionMode ?? FRONTMATTER_SETTING_DEFAULTS.captions;
|
|
373
414
|
const activeCaptionsEnabled = activeCaptionMode !== 'off';
|
|
374
415
|
const activeCaptionStyle: CaptionStyle = activeCaptionMode === 'social' ? 'social' : 'standard';
|
|
375
416
|
const handleSetCaptionMode = useCallback(
|
|
376
417
|
(mode: CaptionMode) => {
|
|
377
418
|
setSelectedCaptionMode(mode);
|
|
378
|
-
persistFrontmatter({
|
|
419
|
+
persistFrontmatter({
|
|
420
|
+
[FRONTMATTER_SETTING_KEYS.captions.canonical]: omitFrontmatterDefault(
|
|
421
|
+
mode,
|
|
422
|
+
FRONTMATTER_SETTING_DEFAULTS.captions,
|
|
423
|
+
),
|
|
424
|
+
[FRONTMATTER_SETTING_KEYS.captions.legacy]: null,
|
|
425
|
+
});
|
|
379
426
|
},
|
|
380
427
|
[persistFrontmatter],
|
|
381
428
|
);
|
|
@@ -385,17 +432,31 @@ export function PreviewSettingsProvider({
|
|
|
385
432
|
const fmCoverSlide = useMemo(
|
|
386
433
|
() =>
|
|
387
434
|
resolveFrontmatterBoolean(
|
|
388
|
-
readFrontmatterKey(
|
|
435
|
+
readFrontmatterKey(
|
|
436
|
+
frontmatter,
|
|
437
|
+
FRONTMATTER_SETTING_KEYS.coverSlide.canonical,
|
|
438
|
+
FRONTMATTER_SETTING_KEYS.coverSlide.legacy,
|
|
439
|
+
),
|
|
389
440
|
),
|
|
390
441
|
[frontmatter],
|
|
391
442
|
);
|
|
392
443
|
const [selectedCoverSlide, setSelectedCoverSlide] = useState<boolean | null>(null);
|
|
393
444
|
useEffect(() => setSelectedCoverSlide(null), [fmCoverSlide]);
|
|
394
|
-
const activeCoverSlide =
|
|
445
|
+
const activeCoverSlide =
|
|
446
|
+
selectedCoverSlide ?? fmCoverSlide ?? FRONTMATTER_SETTING_DEFAULTS.coverSlide;
|
|
395
447
|
const handleSetCoverSlideEnabled = useCallback(
|
|
396
448
|
(enabled: boolean) => {
|
|
397
449
|
setSelectedCoverSlide(enabled);
|
|
398
|
-
persistFrontmatter({
|
|
450
|
+
persistFrontmatter({
|
|
451
|
+
// The cover is enabled by default, so only persist the non-default
|
|
452
|
+
// state. Pass the boolean through so YAML writes `false`, not
|
|
453
|
+
// the string `"false"`.
|
|
454
|
+
[FRONTMATTER_SETTING_KEYS.coverSlide.canonical]: omitFrontmatterDefault(
|
|
455
|
+
enabled,
|
|
456
|
+
FRONTMATTER_SETTING_DEFAULTS.coverSlide,
|
|
457
|
+
),
|
|
458
|
+
[FRONTMATTER_SETTING_KEYS.coverSlide.legacy]: null,
|
|
459
|
+
});
|
|
399
460
|
},
|
|
400
461
|
[persistFrontmatter],
|
|
401
462
|
);
|
|
@@ -496,27 +557,64 @@ const FORMAT_SWITCH_OPTIONS: { key: ViewportPreset; label: string; w: number; h:
|
|
|
496
557
|
{ key: 'standard', label: '4:3', w: 12, h: 9 },
|
|
497
558
|
];
|
|
498
559
|
|
|
499
|
-
const DISPLAY_MODE_OPTIONS: {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
560
|
+
const DISPLAY_MODE_OPTIONS: {
|
|
561
|
+
key: DisplayMode;
|
|
562
|
+
label: string;
|
|
563
|
+
icon: string;
|
|
564
|
+
summary: string;
|
|
565
|
+
}[] = [
|
|
566
|
+
{
|
|
567
|
+
key: 'slideshow',
|
|
568
|
+
label: 'Slideshow',
|
|
569
|
+
icon: 'fa-solid fa-images',
|
|
570
|
+
summary: 'Present designed slides one at a time.',
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
key: 'video',
|
|
574
|
+
label: 'Video',
|
|
575
|
+
icon: 'fa-solid fa-circle-play',
|
|
576
|
+
summary: 'Play an automatically timed presentation.',
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
key: 'linear',
|
|
580
|
+
label: 'Page',
|
|
581
|
+
icon: 'fa-solid fa-window-maximize',
|
|
582
|
+
summary: 'Scroll through the fully designed page.',
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
key: 'page',
|
|
586
|
+
label: 'Document',
|
|
587
|
+
icon: 'fa-solid fa-align-left',
|
|
588
|
+
summary: 'Read a clean, text-first document.',
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
key: 'narrate',
|
|
592
|
+
label: 'Narrate',
|
|
593
|
+
icon: 'fa-solid fa-microphone-lines',
|
|
594
|
+
summary: 'Speak with a voice-paced teleprompter.',
|
|
595
|
+
},
|
|
504
596
|
];
|
|
505
597
|
|
|
598
|
+
export function displayModeLabel(mode: DisplayMode): string {
|
|
599
|
+
return DISPLAY_MODE_OPTIONS.find((option) => option.key === mode)?.label ?? 'Slideshow';
|
|
600
|
+
}
|
|
601
|
+
|
|
506
602
|
const TRANSFORM_STYLE_OPTIONS = [
|
|
507
603
|
{ key: '', label: 'None' },
|
|
508
604
|
...getTransformStyleSummaries().map((s) => ({ key: s.id, label: s.name })),
|
|
509
605
|
];
|
|
510
606
|
|
|
607
|
+
const SUMMARIZE_TOOLTIP =
|
|
608
|
+
'Extract and summarize content for presentation with these Use modes. Your underlying content is not changed.';
|
|
609
|
+
|
|
511
610
|
/**
|
|
512
611
|
* Left-to-right priority order for the preview controls. As the toolbar
|
|
513
612
|
* narrows, controls drop into the overflow menu from the END of this list
|
|
514
|
-
* first (Cover, then Captions, …).
|
|
515
|
-
*
|
|
516
|
-
* constrained.
|
|
613
|
+
* first (Cover, then Captions, …). Aspect ratio stays inline the longest, but
|
|
614
|
+
* still collapses into the same menu when the toolbar is very constrained.
|
|
517
615
|
*/
|
|
518
|
-
type ControlKey = '
|
|
519
|
-
const CONTROL_KEYS: ControlKey[] = ['
|
|
616
|
+
type ControlKey = 'format' | 'theme' | 'transform' | 'captions' | 'cover';
|
|
617
|
+
const CONTROL_KEYS: ControlKey[] = ['format', 'theme', 'transform', 'captions', 'cover'];
|
|
520
618
|
|
|
521
619
|
const PREVIEW_POPOVER_GAP = 4;
|
|
522
620
|
const PREVIEW_POPOVER_MARGIN = 8;
|
|
@@ -681,16 +779,6 @@ export function PreviewToolbarControls() {
|
|
|
681
779
|
// label-over-control layout used inside the overflow popover.
|
|
682
780
|
const renderControl = (key: ControlKey, compact: boolean): ReactNode => {
|
|
683
781
|
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
782
|
case 'format':
|
|
695
783
|
return (
|
|
696
784
|
<div
|
|
@@ -747,7 +835,8 @@ export function PreviewToolbarControls() {
|
|
|
747
835
|
return (
|
|
748
836
|
<PreviewSelect
|
|
749
837
|
key="transform"
|
|
750
|
-
label="
|
|
838
|
+
label="Summarize"
|
|
839
|
+
labelTooltip={SUMMARIZE_TOOLTIP}
|
|
751
840
|
value={s.activeTransformStyle}
|
|
752
841
|
options={TRANSFORM_STYLE_OPTIONS}
|
|
753
842
|
onChange={(v) => s.setSelectedTransformStyle(v)}
|
|
@@ -798,7 +887,6 @@ export function PreviewToolbarControls() {
|
|
|
798
887
|
key="cover"
|
|
799
888
|
className={`squisq-preview-control${compact ? ' squisq-preview-control--compact' : ''}`}
|
|
800
889
|
>
|
|
801
|
-
<label style={labelStyle}>Cover:</label>
|
|
802
890
|
<label className="squisq-preview-checkbox">
|
|
803
891
|
<input
|
|
804
892
|
type="checkbox"
|
|
@@ -876,15 +964,17 @@ export function PreviewToolbarControls() {
|
|
|
876
964
|
}
|
|
877
965
|
|
|
878
966
|
/**
|
|
879
|
-
* Segmented display-mode switch
|
|
880
|
-
*
|
|
881
|
-
*
|
|
967
|
+
* Segmented display-mode switch retained as a public, embeddable control.
|
|
968
|
+
* The editor shell uses {@link PreviewModeMenu} beside its Use tab instead.
|
|
969
|
+
* Narrate is hidden when the host disables `allowNarrate`.
|
|
882
970
|
*/
|
|
883
971
|
export function PreviewModeSwitch() {
|
|
884
972
|
const s = usePreviewSettings();
|
|
973
|
+
const { allowNarrate } = useEditorContext();
|
|
974
|
+
const options = DISPLAY_MODE_OPTIONS.filter((opt) => opt.key !== 'narrate' || allowNarrate);
|
|
885
975
|
return (
|
|
886
976
|
<div className="squisq-preview-seg" role="group" aria-label="Display mode">
|
|
887
|
-
{
|
|
977
|
+
{options.map((opt) => {
|
|
888
978
|
const active = s.activeDisplayMode === opt.key;
|
|
889
979
|
return (
|
|
890
980
|
<button
|
|
@@ -902,6 +992,185 @@ export function PreviewModeSwitch() {
|
|
|
902
992
|
);
|
|
903
993
|
}
|
|
904
994
|
|
|
995
|
+
const USE_MODE_MENU_WIDTH = 340;
|
|
996
|
+
const USE_MODE_MENU_GAP = 4;
|
|
997
|
+
const USE_MODE_MENU_MARGIN = 8;
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Dropdown trigger rendered directly beside the Use tab. Selecting a mode
|
|
1001
|
+
* also enters the Use view, so the menu works from Write and Source as well
|
|
1002
|
+
* as from an already-active preview.
|
|
1003
|
+
*/
|
|
1004
|
+
export interface PreviewModeMenuProps {
|
|
1005
|
+
/** Incremented by the parent to open the menu from another control. */
|
|
1006
|
+
openRequest?: number;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
export function PreviewModeMenu({ openRequest = 0 }: PreviewModeMenuProps) {
|
|
1010
|
+
const s = usePreviewSettings();
|
|
1011
|
+
const { allowNarrate, colorScheme, setActiveView } = useEditorContext();
|
|
1012
|
+
const options = DISPLAY_MODE_OPTIONS.filter((opt) => opt.key !== 'narrate' || allowNarrate);
|
|
1013
|
+
const activeLabel = displayModeLabel(s.activeDisplayMode);
|
|
1014
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
1015
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
1016
|
+
const itemIdPrefix = useId();
|
|
1017
|
+
const [open, setOpen] = useState(false);
|
|
1018
|
+
const [anchor, setAnchor] = useState<{ top: number; left: number } | null>(null);
|
|
1019
|
+
|
|
1020
|
+
const updatePosition = useCallback(() => {
|
|
1021
|
+
const trigger = triggerRef.current;
|
|
1022
|
+
if (!trigger) return;
|
|
1023
|
+
const rect = trigger.getBoundingClientRect();
|
|
1024
|
+
const menuWidth = Math.min(USE_MODE_MENU_WIDTH, window.innerWidth - USE_MODE_MENU_MARGIN * 2);
|
|
1025
|
+
const maxLeft = Math.max(
|
|
1026
|
+
USE_MODE_MENU_MARGIN,
|
|
1027
|
+
window.innerWidth - menuWidth - USE_MODE_MENU_MARGIN,
|
|
1028
|
+
);
|
|
1029
|
+
setAnchor({
|
|
1030
|
+
top: rect.bottom + USE_MODE_MENU_GAP,
|
|
1031
|
+
left: Math.min(Math.max(USE_MODE_MENU_MARGIN, rect.right - menuWidth), maxLeft),
|
|
1032
|
+
});
|
|
1033
|
+
}, []);
|
|
1034
|
+
|
|
1035
|
+
const closeMenu = useCallback((restoreFocus = false) => {
|
|
1036
|
+
setOpen(false);
|
|
1037
|
+
setAnchor(null);
|
|
1038
|
+
if (restoreFocus) triggerRef.current?.focus();
|
|
1039
|
+
}, []);
|
|
1040
|
+
|
|
1041
|
+
const openMenu = useCallback(() => {
|
|
1042
|
+
updatePosition();
|
|
1043
|
+
setOpen(true);
|
|
1044
|
+
}, [updatePosition]);
|
|
1045
|
+
|
|
1046
|
+
useEffect(() => {
|
|
1047
|
+
if (openRequest > 0) openMenu();
|
|
1048
|
+
}, [openMenu, openRequest]);
|
|
1049
|
+
|
|
1050
|
+
useEffect(() => {
|
|
1051
|
+
if (!open) return;
|
|
1052
|
+
const handlePointerDown = (event: MouseEvent) => {
|
|
1053
|
+
const target = event.target as Node;
|
|
1054
|
+
if (triggerRef.current?.contains(target) || menuRef.current?.contains(target)) return;
|
|
1055
|
+
closeMenu();
|
|
1056
|
+
};
|
|
1057
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
1058
|
+
if (event.key !== 'Escape') return;
|
|
1059
|
+
event.preventDefault();
|
|
1060
|
+
closeMenu(true);
|
|
1061
|
+
};
|
|
1062
|
+
document.addEventListener('mousedown', handlePointerDown);
|
|
1063
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
1064
|
+
window.addEventListener('resize', updatePosition);
|
|
1065
|
+
window.addEventListener('scroll', updatePosition, true);
|
|
1066
|
+
return () => {
|
|
1067
|
+
document.removeEventListener('mousedown', handlePointerDown);
|
|
1068
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
1069
|
+
window.removeEventListener('resize', updatePosition);
|
|
1070
|
+
window.removeEventListener('scroll', updatePosition, true);
|
|
1071
|
+
};
|
|
1072
|
+
}, [closeMenu, open, updatePosition]);
|
|
1073
|
+
|
|
1074
|
+
useLayoutEffect(() => {
|
|
1075
|
+
if (!open || !anchor) return;
|
|
1076
|
+
const selected = menuRef.current?.querySelector<HTMLButtonElement>('[aria-checked="true"]');
|
|
1077
|
+
const first = menuRef.current?.querySelector<HTMLButtonElement>('[role="menuitemradio"]');
|
|
1078
|
+
(selected ?? first)?.focus();
|
|
1079
|
+
}, [anchor, open]);
|
|
1080
|
+
|
|
1081
|
+
return (
|
|
1082
|
+
<>
|
|
1083
|
+
<button
|
|
1084
|
+
ref={triggerRef}
|
|
1085
|
+
type="button"
|
|
1086
|
+
className={`squisq-use-mode-trigger${open ? ' squisq-use-mode-trigger--open' : ''}`}
|
|
1087
|
+
aria-label="Choose Use mode"
|
|
1088
|
+
aria-haspopup="menu"
|
|
1089
|
+
aria-expanded={open}
|
|
1090
|
+
title={`Use mode: ${activeLabel}`}
|
|
1091
|
+
onClick={() => (open ? closeMenu() : openMenu())}
|
|
1092
|
+
onKeyDown={(event) => {
|
|
1093
|
+
if (event.key !== 'ArrowDown') return;
|
|
1094
|
+
event.preventDefault();
|
|
1095
|
+
openMenu();
|
|
1096
|
+
}}
|
|
1097
|
+
>
|
|
1098
|
+
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
|
1099
|
+
<path d="M2 3.5 5 6.5 8 3.5" fill="none" stroke="currentColor" strokeWidth="1.4" />
|
|
1100
|
+
</svg>
|
|
1101
|
+
</button>
|
|
1102
|
+
{open &&
|
|
1103
|
+
anchor &&
|
|
1104
|
+
createPortal(
|
|
1105
|
+
<div
|
|
1106
|
+
ref={menuRef}
|
|
1107
|
+
className="squisq-use-mode-menu"
|
|
1108
|
+
data-theme={colorScheme}
|
|
1109
|
+
role="menu"
|
|
1110
|
+
aria-label="Use mode"
|
|
1111
|
+
style={{ top: anchor.top, left: anchor.left }}
|
|
1112
|
+
onKeyDown={(event) => {
|
|
1113
|
+
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;
|
|
1114
|
+
event.preventDefault();
|
|
1115
|
+
const items = Array.from(
|
|
1116
|
+
event.currentTarget.querySelectorAll<HTMLButtonElement>('[role="menuitemradio"]'),
|
|
1117
|
+
);
|
|
1118
|
+
if (items.length === 0) return;
|
|
1119
|
+
const currentIndex = items.indexOf(document.activeElement as HTMLButtonElement);
|
|
1120
|
+
const nextIndex =
|
|
1121
|
+
event.key === 'Home'
|
|
1122
|
+
? 0
|
|
1123
|
+
: event.key === 'End'
|
|
1124
|
+
? items.length - 1
|
|
1125
|
+
: event.key === 'ArrowUp'
|
|
1126
|
+
? (currentIndex - 1 + items.length) % items.length
|
|
1127
|
+
: (currentIndex + 1) % items.length;
|
|
1128
|
+
items[nextIndex]?.focus();
|
|
1129
|
+
}}
|
|
1130
|
+
>
|
|
1131
|
+
{options.map((option) => {
|
|
1132
|
+
const selected = option.key === s.activeDisplayMode;
|
|
1133
|
+
const labelId = `${itemIdPrefix}-${option.key}-label`;
|
|
1134
|
+
const summaryId = `${itemIdPrefix}-${option.key}-summary`;
|
|
1135
|
+
return (
|
|
1136
|
+
<button
|
|
1137
|
+
key={option.key}
|
|
1138
|
+
type="button"
|
|
1139
|
+
className={`squisq-use-mode-menu-item${selected ? ' squisq-use-mode-menu-item--selected' : ''}`}
|
|
1140
|
+
role="menuitemradio"
|
|
1141
|
+
aria-checked={selected}
|
|
1142
|
+
aria-labelledby={labelId}
|
|
1143
|
+
aria-describedby={summaryId}
|
|
1144
|
+
onClick={() => {
|
|
1145
|
+
s.setSelectedDisplayMode(option.key);
|
|
1146
|
+
setActiveView('preview');
|
|
1147
|
+
closeMenu(true);
|
|
1148
|
+
}}
|
|
1149
|
+
>
|
|
1150
|
+
<span className="squisq-use-mode-menu-icon" aria-hidden="true">
|
|
1151
|
+
<Icon icon={option.icon} />
|
|
1152
|
+
</span>
|
|
1153
|
+
<span className="squisq-use-mode-menu-copy">
|
|
1154
|
+
<span id={labelId} className="squisq-use-mode-menu-label">
|
|
1155
|
+
{option.label}
|
|
1156
|
+
</span>
|
|
1157
|
+
<span id={summaryId} className="squisq-use-mode-menu-summary">
|
|
1158
|
+
{option.summary}
|
|
1159
|
+
</span>
|
|
1160
|
+
</span>
|
|
1161
|
+
<span className="squisq-use-mode-menu-check" aria-hidden="true">
|
|
1162
|
+
{selected && <Icon icon="fa-solid fa-check" />}
|
|
1163
|
+
</span>
|
|
1164
|
+
</button>
|
|
1165
|
+
);
|
|
1166
|
+
})}
|
|
1167
|
+
</div>,
|
|
1168
|
+
document.body,
|
|
1169
|
+
)}
|
|
1170
|
+
</>
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
905
1174
|
/** A simple aspect-ratio glyph: a centered rounded rectangle of `w`×`h` in a
|
|
906
1175
|
* 16×16 box, so 16:9 reads as a wide box, 1:1 a square, 9:16 a tall box. */
|
|
907
1176
|
function AspectIcon({ w, h }: { w: number; h: number }) {
|
|
@@ -951,12 +1220,14 @@ export function PreviewFormatSwitch() {
|
|
|
951
1220
|
|
|
952
1221
|
function PreviewSelect({
|
|
953
1222
|
label,
|
|
1223
|
+
labelTooltip,
|
|
954
1224
|
value,
|
|
955
1225
|
options,
|
|
956
1226
|
onChange,
|
|
957
1227
|
compact,
|
|
958
1228
|
}: {
|
|
959
1229
|
label: string;
|
|
1230
|
+
labelTooltip?: string;
|
|
960
1231
|
value: string;
|
|
961
1232
|
options: { key: string; label: string }[];
|
|
962
1233
|
onChange: (value: string) => void;
|
|
@@ -964,7 +1235,9 @@ function PreviewSelect({
|
|
|
964
1235
|
}) {
|
|
965
1236
|
return (
|
|
966
1237
|
<div className={`squisq-preview-control${compact ? ' squisq-preview-control--compact' : ''}`}>
|
|
967
|
-
<label style={labelStyle}
|
|
1238
|
+
<label style={labelStyle} title={labelTooltip}>
|
|
1239
|
+
{label}:
|
|
1240
|
+
</label>
|
|
968
1241
|
<select value={value} onChange={(e) => onChange(e.target.value)} style={selectStyle}>
|
|
969
1242
|
{options.map((o) => (
|
|
970
1243
|
<option key={o.key} value={o.key}>
|