@bendyline/squisq-editor-react 1.6.1 → 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.
Files changed (208) hide show
  1. package/README.md +12 -3
  2. package/dist/index.d.ts +1304 -208
  3. package/dist/index.js +22838 -14494
  4. package/dist/index.js.map +1 -1
  5. package/dist/monaco.d.ts +1 -0
  6. package/dist/monaco.js +4 -0
  7. package/dist/monaco.js.map +1 -0
  8. package/dist/styles/index.css +2384 -408
  9. package/package.json +9 -5
  10. package/src/BlockPropertiesPopover.tsx +48 -17
  11. package/src/DocumentSettingsDialog.tsx +13 -21
  12. package/src/EditorContext.tsx +199 -42
  13. package/src/EditorShell.tsx +403 -270
  14. package/src/ImageEditor.tsx +22 -1
  15. package/src/InlinePreviewGutter.tsx +16 -8
  16. package/src/MediaBin.tsx +276 -40
  17. package/src/OutlinePanel.tsx +202 -7
  18. package/src/PlainHtmlPreview.tsx +40 -1
  19. package/src/PreviewControls.tsx +466 -93
  20. package/src/PreviewPanel.tsx +313 -77
  21. package/src/RawEditor.tsx +13 -2
  22. package/src/StatusBar.tsx +5 -2
  23. package/src/TemplateAnnotation.ts +34 -10
  24. package/src/TemplateContentPreview.tsx +56 -0
  25. package/src/TemplatePicker.tsx +355 -134
  26. package/src/ThemeCustomizerPanel.tsx +30 -15
  27. package/src/ThemePicker.tsx +10 -5
  28. package/src/TimelineBlockPreview.tsx +1 -1
  29. package/src/Toolbar.tsx +719 -238
  30. package/src/TransitionPicker.tsx +44 -5
  31. package/src/ViewMenuPanel.tsx +12 -14
  32. package/src/ViewSwitcher.tsx +4 -4
  33. package/src/WysiwygEditor.tsx +167 -92
  34. package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +33 -0
  35. package/src/__tests__/blockTagActivity.test.ts +183 -0
  36. package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +75 -0
  37. package/src/__tests__/buildPreviewDocContent.test.ts +48 -0
  38. package/src/__tests__/buildPreviewDocTransition.test.ts +22 -3
  39. package/src/__tests__/canvasSurfaceTextEditing.test.tsx +60 -0
  40. package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
  41. package/src/__tests__/documentSettingsDialog.test.tsx +29 -1
  42. package/src/__tests__/editorScrollHandoff.test.tsx +102 -0
  43. package/src/__tests__/editorScrollSync.test.ts +65 -0
  44. package/src/__tests__/editorShellProps.test.tsx +390 -1
  45. package/src/__tests__/emojiPicker.test.tsx +2 -42
  46. package/src/__tests__/headingTransition.test.ts +53 -15
  47. package/src/__tests__/imageEditAffordance.test.tsx +5 -1
  48. package/src/__tests__/imageEditModalContract.test.tsx +119 -0
  49. package/src/__tests__/imageEditorShell.test.tsx +38 -1
  50. package/src/__tests__/jsonEditor.test.tsx +56 -1
  51. package/src/__tests__/layersPanel.test.tsx +75 -5
  52. package/src/__tests__/majorApiTypes.test.ts +26 -0
  53. package/src/__tests__/mediaBinDrop.test.tsx +90 -0
  54. package/src/__tests__/mediaReferences.test.ts +82 -0
  55. package/src/__tests__/narrationSave.test.ts +148 -0
  56. package/src/__tests__/outlinePanel.test.tsx +93 -3
  57. package/src/__tests__/outlineSource.test.ts +217 -0
  58. package/src/__tests__/plainHtmlPreview.test.tsx +17 -1
  59. package/src/__tests__/presentationMode.test.tsx +290 -0
  60. package/src/__tests__/previewControls.test.tsx +312 -6
  61. package/src/__tests__/previewPanelAudioFailure.test.tsx +46 -0
  62. package/src/__tests__/previewPanelPresentation.test.tsx +126 -0
  63. package/src/__tests__/previewPanelTransforms.test.tsx +117 -0
  64. package/src/__tests__/rawEditorModelIsolation.test.ts +19 -0
  65. package/src/__tests__/resolveBlockVisual.test.ts +43 -0
  66. package/src/__tests__/statusBar.test.tsx +27 -0
  67. package/src/__tests__/teleprompter.test.tsx +190 -0
  68. package/src/__tests__/templateAnnotationRoundTrip.test.ts +49 -2
  69. package/src/__tests__/templateContentPreview.test.ts +101 -0
  70. package/src/__tests__/templatePickerPortal.test.tsx +62 -0
  71. package/src/__tests__/tiptapBridge.test.ts +70 -0
  72. package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
  73. package/src/__tests__/transformStyleId.test.ts +13 -0
  74. package/src/__tests__/useImageEditor.test.tsx +67 -0
  75. package/src/__tests__/useMediaRecorder.test.ts +75 -0
  76. package/src/__tests__/viewMenuPanel.test.tsx +130 -0
  77. package/src/__tests__/wysiwygImageUpload.test.ts +69 -0
  78. package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +56 -0
  79. package/src/asciiDiagram/AsciiDiagramExtension.ts +343 -0
  80. package/src/asciiDiagram/AsciiDiagramWidget.tsx +224 -0
  81. package/src/asciiDiagram/RepairableDiagramExtension.ts +200 -0
  82. package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +145 -0
  83. package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +159 -0
  84. package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +227 -0
  85. package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
  86. package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
  87. package/src/asciiDiagram/asciiDiagramCommands.ts +148 -0
  88. package/src/asciiDiagram/asciiDiagramData.ts +116 -0
  89. package/src/asciiDiagram/asciiDiagramOps.ts +219 -0
  90. package/src/asciiDiagram/asciiPaste.ts +19 -0
  91. package/src/blockTagActivity.ts +233 -0
  92. package/src/buildDocumentPreviewMarkdown.ts +168 -0
  93. package/src/buildPreviewDoc.ts +19 -12
  94. package/src/customTemplates/CustomLayoutManager.tsx +3 -2
  95. package/src/customTemplates/CustomTemplateContext.tsx +6 -0
  96. package/src/customTemplates/TemplateDesigner.tsx +7 -4
  97. package/src/customTemplates/__tests__/library.test.ts +8 -0
  98. package/src/customTemplates/designer.css +158 -99
  99. package/src/customTemplates/library.ts +16 -3
  100. package/src/customThemes/CustomThemeContext.tsx +6 -0
  101. package/src/customThemes/CustomThemeDialog.tsx +35 -5
  102. package/src/customThemes/ImportThemeSection.tsx +178 -0
  103. package/src/customThemes/__tests__/customThemeLibrary.test.ts +9 -0
  104. package/src/customThemes/__tests__/importThemeSection.test.tsx +192 -0
  105. package/src/customThemes/customThemeLibrary.ts +7 -4
  106. package/src/customThemes/index.ts +7 -1
  107. package/src/customThemes/themeDraft.ts +23 -7
  108. package/src/diagram/DiagramCanvas.tsx +9 -4
  109. package/src/diagram/types.ts +35 -0
  110. package/src/editorScrollSync.ts +73 -0
  111. package/src/emojiData.ts +3 -23
  112. package/src/headingTransition.ts +85 -25
  113. package/src/imageEditor/CanvasSurface.tsx +29 -23
  114. package/src/imageEditor/LayersPanel.tsx +78 -3
  115. package/src/imageEditor/Toolbar.tsx +5 -1
  116. package/src/imageEditor/icons.tsx +4 -0
  117. package/src/imageEditor/image-editor.css +75 -0
  118. package/src/imageEditor/layers/SelectionHandles.tsx +1 -1
  119. package/src/imageEditor/useImageEditor.ts +71 -14
  120. package/src/index.ts +193 -20
  121. package/src/jsonEditor/JsonEditorContext.tsx +12 -6
  122. package/src/jsonEditor/RenderNode.tsx +24 -3
  123. package/src/jsonEditor/editors.tsx +86 -19
  124. package/src/mediaEntries.ts +12 -0
  125. package/src/mediaReferences.ts +299 -0
  126. package/src/monaco.ts +35 -0
  127. package/src/monacoWorkers.ts +89 -0
  128. package/src/outlineSource.ts +171 -0
  129. package/src/presentation/PresentationMode.tsx +596 -0
  130. package/src/rawEditorIsolation.ts +18 -0
  131. package/src/recorder/hooks/useMediaRecorder.ts +97 -53
  132. package/src/resolveBlockVisual.ts +10 -4
  133. package/src/scene/Scene.tsx +101 -25
  134. package/src/scene/SceneBlockExtension.ts +17 -10
  135. package/src/scene/SceneBlockWidget.tsx +11 -4
  136. package/src/scene/SceneSelection.tsx +19 -15
  137. package/src/scene/SceneSideToolbar.tsx +89 -0
  138. package/src/scene/SceneViewport.tsx +7 -3
  139. package/src/scene/ShapePalette.tsx +17 -2
  140. package/src/scene/__tests__/DiagramAdapter.test.ts +86 -0
  141. package/src/scene/__tests__/SceneBlockExtension.test.ts +33 -0
  142. package/src/scene/__tests__/sceneIsolation.test.tsx +93 -0
  143. package/src/scene/adapters/DiagramAdapter.ts +12 -101
  144. package/src/scene/commands/SceneCommand.ts +4 -1
  145. package/src/scene/index.ts +1 -6
  146. package/src/scene/layers/DiagramEdges.tsx +24 -9
  147. package/src/scene/layers/edgeGeometry.ts +31 -4
  148. package/src/scene/layers/nodeCard.tsx +27 -8
  149. package/src/scene/scene.css +145 -2
  150. package/src/scene/text/SceneTextOverlay.tsx +5 -5
  151. package/src/scene/text/sceneTextChannel.ts +26 -20
  152. package/src/scene/text/sceneTextConfig.ts +4 -0
  153. package/src/scene/tools/ConnectTool.ts +126 -28
  154. package/src/scene/tools/DrawingConnectTool.ts +86 -16
  155. package/src/scene/tools/SceneTool.ts +10 -0
  156. package/src/scene/tools/SelectTool.ts +34 -18
  157. package/src/scene/tools/ShapeTool.ts +2 -3
  158. package/src/styles/ascii-diagram.css +79 -0
  159. package/src/styles/ascii-timeline.css +499 -0
  160. package/src/styles/editor.css +1566 -219
  161. package/src/styles/index.css +3 -0
  162. package/src/styles/tree-view.css +139 -0
  163. package/src/teleprompter/TeleprompterControls.tsx +218 -0
  164. package/src/teleprompter/TeleprompterSelfView.tsx +22 -0
  165. package/src/teleprompter/TeleprompterSurface.tsx +267 -0
  166. package/src/teleprompter/TeleprompterView.tsx +338 -0
  167. package/src/teleprompter/canvasRenderer.ts +161 -0
  168. package/src/teleprompter/floatingWindow.ts +352 -0
  169. package/src/teleprompter/index.ts +61 -0
  170. package/src/teleprompter/pcmWorklet.ts +62 -0
  171. package/src/teleprompter/recording/insertPreamble.ts +80 -0
  172. package/src/teleprompter/recording/narrationSave.ts +134 -0
  173. package/src/teleprompter/recording/useNarrationRecorder.ts +367 -0
  174. package/src/teleprompter/scrollModel.ts +85 -0
  175. package/src/teleprompter/teleprompterTheme.ts +303 -0
  176. package/src/teleprompter/types.ts +38 -0
  177. package/src/teleprompter/useFloatingWindow.ts +74 -0
  178. package/src/teleprompter/useMicAnalysis.ts +211 -0
  179. package/src/teleprompter/useTeleprompter.ts +421 -0
  180. package/src/templateContentPreviewResolver.ts +360 -0
  181. package/src/timeline/TimelineEditorWidget.tsx +702 -0
  182. package/src/timeline/TimelineViewExtension.ts +252 -0
  183. package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +355 -0
  184. package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
  185. package/src/timeline/__tests__/timelineCommands.test.ts +327 -0
  186. package/src/timeline/__tests__/timelineOps.test.ts +222 -0
  187. package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
  188. package/src/timeline/timelineCommands.ts +367 -0
  189. package/src/timeline/timelineData.ts +54 -0
  190. package/src/timeline/timelineOps.ts +277 -0
  191. package/src/timeline/timelinePaste.ts +8 -0
  192. package/src/tiptapBridge.ts +84 -49
  193. package/src/transformStyleId.ts +17 -0
  194. package/src/treeview/TreeOutlineWidget.tsx +240 -0
  195. package/src/treeview/TreeViewExtension.ts +250 -0
  196. package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
  197. package/src/treeview/__tests__/treeOps.test.ts +125 -0
  198. package/src/treeview/__tests__/treePaste.test.ts +40 -0
  199. package/src/treeview/__tests__/treeViewCommands.test.ts +163 -0
  200. package/src/treeview/treeOps.ts +172 -0
  201. package/src/treeview/treePaste.ts +13 -0
  202. package/src/treeview/treeViewCommands.ts +94 -0
  203. package/src/treeview/treeViewData.ts +41 -0
  204. package/src/useMonacoLoader.ts +11 -34
  205. package/src/wysiwygImageUpload.ts +113 -0
  206. package/src/diagram/DiagramExtension.ts +0 -209
  207. package/src/diagram/DiagramWidget.tsx +0 -270
  208. package/src/diagram/useDiagramData.ts +0 -126
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-editor-react",
3
- "version": "1.6.1",
3
+ "version": "2.0.0",
4
4
  "description": "React editor shell with raw/WYSIWYG/preview modes for Squisq documents",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -34,6 +34,10 @@
34
34
  "types": "./dist/index.d.ts",
35
35
  "import": "./dist/index.js"
36
36
  },
37
+ "./monaco": {
38
+ "types": "./dist/monaco.d.ts",
39
+ "import": "./dist/monaco.js"
40
+ },
37
41
  "./styles": "./dist/styles/index.css"
38
42
  },
39
43
  "scripts": {
@@ -42,7 +46,7 @@
42
46
  "typecheck": "tsc --noEmit"
43
47
  },
44
48
  "peerDependencies": {
45
- "monaco-editor": "^0.50.0",
49
+ "monaco-editor": ">=0.50.0",
46
50
  "react": "^18.0.0 || ^19.0.0",
47
51
  "react-dom": "^18.0.0 || ^19.0.0"
48
52
  },
@@ -52,9 +56,9 @@
52
56
  }
53
57
  },
54
58
  "dependencies": {
55
- "@bendyline/squisq": "1.5.1",
56
- "@bendyline/squisq-formats": "1.4.1",
57
- "@bendyline/squisq-react": "1.4.1",
59
+ "@bendyline/squisq": "2.0.0",
60
+ "@bendyline/squisq-formats": "2.0.0",
61
+ "@bendyline/squisq-react": "2.0.0",
58
62
  "@fortawesome/fontawesome-free": "7.2.0",
59
63
  "@monaco-editor/react": "4.7.0",
60
64
  "@tiptap/extension-image": "2.27.2",
@@ -3,12 +3,11 @@
3
3
  *
4
4
  * The on-canvas "block properties" palette — the sibling of the block-template
5
5
  * badge. Anchored at the `.squisq-props-badge` chip on a heading, it edits the
6
- * block's playback/animation metadata, all stored in the heading's Pandoc `{…}`
7
- * attribute block (`dataBlockAttrs`):
6
+ * block's playback/animation metadata:
8
7
  *
9
- * - Transition (type / direction / duration) — reuses `TransitionPicker`
10
- * - Duration (`duration`) — how long the block is shown
11
- * - Start time (`startTime`) — timeline position
8
+ * - Transition (type / direction / duration) — stored in `{[…]}` params
9
+ * - Duration (`duration`) — stored in Pandoc `dataBlockAttrs`
10
+ * - Start time (`startTime`) — stored in Pandoc `dataBlockAttrs`
12
11
  *
13
12
  * The popover holds the `dataBlockAttrs` inner string as working state and
14
13
  * re-derives each control from it, so successive edits compose. Every change
@@ -17,12 +16,12 @@
17
16
  * `TemplateBadgePopover`.
18
17
  */
19
18
 
20
- import { useEffect, useState } from 'react';
19
+ import { useEffect, useId, useRef, useState } from 'react';
21
20
  import { createPortal } from 'react-dom';
22
21
  import { TransitionPicker } from './TransitionPicker';
23
22
  import {
24
23
  readBlockAttrsTransition,
25
- setBlockAttrsTransition,
24
+ setHeadingAttrsTransition,
26
25
  type TransitionFields,
27
26
  } from './headingTransition';
28
27
  import { readBlockAttrsValue, setBlockAttrsValue } from './blockProperties';
@@ -36,10 +35,18 @@ export interface BlockPropertiesPopoverProps {
36
35
  templateParams: string | null;
37
36
  /** Apply a new `dataBlockAttrs` inner to the heading (null clears it). */
38
37
  onChange: (nextInner: string | null) => void;
38
+ /** Apply a paired `dataBlockAttrs` / `dataTemplateParams` transition rewrite. */
39
+ onAnnotationChange: (next: {
40
+ blockAttrsInner: string | null;
41
+ templateParams: string | null;
42
+ }) => void;
43
+ /** Editor surface scheme. Required explicitly because this popover is portaled to `<body>`. */
44
+ colorScheme?: 'light' | 'dark';
45
+ /** Active document-theme accent used for focus and selected-state highlights. */
46
+ accentColor?: string;
39
47
  onClose: () => void;
40
48
  }
41
49
 
42
- const PANEL_ID = 'squisq-block-props-portal';
43
50
  /** The nested transition flyout is portaled out here; don't treat it as "outside". */
44
51
  const TRANSITION_FLYOUT = '.squisq-transition-flyout';
45
52
 
@@ -48,14 +55,20 @@ export function BlockPropertiesPopover({
48
55
  blockAttrs,
49
56
  templateParams,
50
57
  onChange,
58
+ onAnnotationChange,
59
+ colorScheme = 'light',
60
+ accentColor,
51
61
  onClose,
52
62
  }: BlockPropertiesPopoverProps) {
63
+ const panelRef = useRef<HTMLDivElement>(null);
64
+ const panelId = `squisq-block-props-portal-${useId().replace(/:/g, '')}`;
53
65
  // Working copy of the Pandoc inner; successive edits compose off it.
54
66
  const [inner, setInner] = useState<string | null>(blockAttrs);
67
+ const [templateInner, setTemplateInner] = useState<string | null>(templateParams);
55
68
  const [style, setStyle] = useState<React.CSSProperties>(() => computeStyle(anchorRect));
56
69
 
57
70
  useEffect(() => {
58
- requestAnimationFrame(() => setStyle(computeStyle(anchorRect)));
71
+ requestAnimationFrame(() => setStyle(computeStyle(anchorRect, panelRef.current)));
59
72
  }, [anchorRect]);
60
73
 
61
74
  // Outside click + Escape close. A click inside the popover OR inside the
@@ -66,7 +79,7 @@ export function BlockPropertiesPopover({
66
79
  };
67
80
  const onMouse = (e: MouseEvent) => {
68
81
  const target = e.target as HTMLElement;
69
- const inPanel = document.getElementById(PANEL_ID)?.contains(target);
82
+ const inPanel = panelRef.current?.contains(target);
70
83
  const inFlyout = target.closest?.(TRANSITION_FLYOUT);
71
84
  if (!inPanel && !inFlyout) onClose();
72
85
  };
@@ -84,25 +97,37 @@ export function BlockPropertiesPopover({
84
97
  onChange(next);
85
98
  };
86
99
 
87
- const transition = readBlockAttrsTransition(inner, templateParams);
100
+ const transition = readBlockAttrsTransition(inner, templateInner);
88
101
  const duration = readBlockAttrsValue(inner, 'duration');
89
102
  const startTime = readBlockAttrsValue(inner, 'startTime');
90
103
 
91
- const onTransition = (next: TransitionFields) => apply(setBlockAttrsTransition(inner, next));
104
+ const onTransition = (next: TransitionFields) => {
105
+ const updated = setHeadingAttrsTransition(inner, templateInner, next);
106
+ setInner(updated.blockAttrsInner);
107
+ setTemplateInner(updated.templateParams);
108
+ onAnnotationChange(updated);
109
+ };
92
110
  const onDuration = (v: string) => apply(setBlockAttrsValue(inner, 'duration', v));
93
111
  const onStartTime = (v: string) => apply(setBlockAttrsValue(inner, 'startTime', v));
94
112
 
95
113
  return createPortal(
96
114
  <div
97
- id={PANEL_ID}
115
+ ref={panelRef}
116
+ id={panelId}
98
117
  className="squisq-block-props-popover"
118
+ data-theme={colorScheme}
99
119
  role="dialog"
100
120
  aria-label="Block properties"
101
- style={style}
121
+ style={{ ...style, ...blockAccentStyle(accentColor) }}
102
122
  >
103
123
  <div className="squisq-block-props-row">
104
124
  <span className="squisq-block-props-label">Transition</span>
105
- <TransitionPicker value={transition} onChange={onTransition} />
125
+ <TransitionPicker
126
+ value={transition}
127
+ onChange={onTransition}
128
+ colorScheme={colorScheme}
129
+ accentColor={accentColor}
130
+ />
106
131
  </div>
107
132
 
108
133
  <div className="squisq-block-props-row">
@@ -131,6 +156,12 @@ export function BlockPropertiesPopover({
131
156
  );
132
157
  }
133
158
 
159
+ function blockAccentStyle(accentColor: string | undefined): React.CSSProperties {
160
+ return accentColor
161
+ ? ({ ['--squisq-block-props-accent' as string]: accentColor } as React.CSSProperties)
162
+ : {};
163
+ }
164
+
134
165
  function NumberField({
135
166
  value,
136
167
  placeholder,
@@ -162,8 +193,8 @@ function NumberField({
162
193
  }
163
194
 
164
195
  /** Place the panel below the badge, flipping above / clamping to the viewport. */
165
- function computeStyle(rect: DOMRect): React.CSSProperties {
166
- const panel = document.getElementById(PANEL_ID)?.getBoundingClientRect();
196
+ function computeStyle(rect: DOMRect, element?: HTMLElement | null): React.CSSProperties {
197
+ const panel = element?.getBoundingClientRect();
167
198
  const width = panel?.width ?? 280;
168
199
  const height = panel?.height ?? 160;
169
200
  const margin = 8;
@@ -19,17 +19,19 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
19
19
  import {
20
20
  inferDocumentTitle,
21
21
  parseMarkdown,
22
+ readFrontmatterThemeId,
22
23
  setFrontmatterValues,
23
24
  } from '@bendyline/squisq/markdown';
24
25
  import { getTransformStyleSummaries } from '@bendyline/squisq/transform';
25
26
  import { ThemePicker } from './ThemePicker';
27
+ import { resolvePersistedTransformStyleId } from './transformStyleId';
26
28
 
27
29
  // ── Frontmatter key constants ─────────────────────────────────────
28
30
 
29
31
  /** Mirror of `FM_KEYS` in PreviewControls.tsx. Canonical names are the
30
32
  * ones we write; legacy aliases are still read so older docs work. */
31
33
  const FM = {
32
- theme: { canonical: 'squisq-theme', legacy: 'theme' },
34
+ theme: { canonical: 'squisq-theme', legacy: ['themeId', 'theme'] },
33
35
  transform: { canonical: 'squisq-transform', legacy: 'transform-style' },
34
36
  captions: { canonical: 'squisq-captions', legacy: 'caption-style' },
35
37
  } as const;
@@ -75,8 +77,11 @@ export function DocumentSettingsDialog({
75
77
  const inferredTitle = useMemo(() => inferDocumentTitle(parsed), [parsed]);
76
78
 
77
79
  const currentTitle = typeof frontmatter?.title === 'string' ? (frontmatter.title as string) : '';
78
- const currentTheme = readFm(frontmatter, FM.theme.canonical, FM.theme.legacy);
79
- const currentTransform = readFm(frontmatter, FM.transform.canonical, FM.transform.legacy);
80
+ const currentTheme = readFrontmatterThemeId(frontmatter) ?? '';
81
+ const currentTransform =
82
+ resolvePersistedTransformStyleId(
83
+ readFm(frontmatter, FM.transform.canonical, FM.transform.legacy),
84
+ ) ?? '';
80
85
  const currentCaptions = readFm(frontmatter, FM.captions.canonical, FM.captions.legacy);
81
86
 
82
87
  const [title, setTitle] = useState(currentTitle);
@@ -128,13 +133,11 @@ export function DocumentSettingsDialog({
128
133
  const updates: Record<string, string | null> = {
129
134
  title: nextTitle,
130
135
  [FM.theme.canonical]: theme || null,
131
- // Legacy `theme` key would shadow our canonical one — clear it
132
- // when the user changes the theme so the canonical write wins.
133
- ...(currentTheme && theme !== currentTheme ? { [FM.theme.legacy]: null } : {}),
136
+ // Saving settings canonicalizes all older theme spellings.
137
+ [FM.theme.legacy[0]]: null,
138
+ [FM.theme.legacy[1]]: null,
134
139
  [FM.transform.canonical]: transform || null,
135
- ...(currentTransform && transform !== currentTransform
136
- ? { [FM.transform.legacy]: null }
137
- : {}),
140
+ [FM.transform.legacy]: null,
138
141
  [FM.captions.canonical]: captions || null,
139
142
  ...(currentCaptions && captions !== currentCaptions ? { [FM.captions.legacy]: null } : {}),
140
143
  };
@@ -142,18 +145,7 @@ export function DocumentSettingsDialog({
142
145
  const nextSource = setFrontmatterValues(markdownSource, updates);
143
146
  onSave(nextSource);
144
147
  },
145
- [
146
- title,
147
- theme,
148
- transform,
149
- captions,
150
- inferredTitle,
151
- markdownSource,
152
- currentTheme,
153
- currentTransform,
154
- currentCaptions,
155
- onSave,
156
- ],
148
+ [title, theme, transform, captions, inferredTitle, markdownSource, currentCaptions, onSave],
157
149
  );
158
150
 
159
151
  return (