@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.
Files changed (218) hide show
  1. package/README.md +12 -3
  2. package/dist/index.d.ts +1275 -183
  3. package/dist/index.js +20345 -12777
  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 +1607 -241
  9. package/package.json +9 -5
  10. package/src/BlockPropertiesPopover.tsx +30 -15
  11. package/src/DocumentSettingsDialog.tsx +39 -33
  12. package/src/EditorContext.tsx +199 -42
  13. package/src/EditorShell.tsx +349 -261
  14. package/src/ImageEditor.tsx +22 -1
  15. package/src/InlinePreviewGutter.tsx +16 -8
  16. package/src/MediaBin.tsx +107 -14
  17. package/src/OutlinePanel.tsx +202 -7
  18. package/src/PlainHtmlPreview.tsx +40 -1
  19. package/src/PreviewControls.tsx +362 -89
  20. package/src/PreviewPanel.tsx +312 -78
  21. package/src/RawEditor.tsx +13 -2
  22. package/src/RecorderEntry.tsx +2 -0
  23. package/src/StatusBar.tsx +5 -2
  24. package/src/TemplateAnnotation.ts +12 -3
  25. package/src/TemplatePicker.tsx +75 -21
  26. package/src/ThemeCustomizerPanel.tsx +8 -0
  27. package/src/ThemePicker.tsx +10 -5
  28. package/src/TimelineBlockPreview.tsx +1 -1
  29. package/src/Toolbar.tsx +357 -51
  30. package/src/TransitionPicker.tsx +36 -4
  31. package/src/ViewMenuPanel.tsx +12 -14
  32. package/src/WysiwygEditor.tsx +124 -91
  33. package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +33 -0
  34. package/src/__tests__/blockTagActivity.test.ts +183 -0
  35. package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +75 -0
  36. package/src/__tests__/buildPreviewDocContent.test.ts +48 -0
  37. package/src/__tests__/buildPreviewDocTransition.test.ts +21 -1
  38. package/src/__tests__/canvasSurfaceTextEditing.test.tsx +60 -0
  39. package/src/__tests__/codeContextSectionView.test.tsx +8 -6
  40. package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
  41. package/src/__tests__/documentSettingsDialog.test.tsx +51 -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 +123 -1
  45. package/src/__tests__/emojiPicker.test.tsx +2 -42
  46. package/src/__tests__/headingTransition.test.ts +0 -12
  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 +15 -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__/mediaAttachmentFlow.test.ts +2 -2
  54. package/src/__tests__/mediaBinDrop.test.tsx +90 -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 +382 -5
  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__/recorderTheme.test.tsx +42 -0
  66. package/src/__tests__/resolveBlockVisual.test.ts +43 -0
  67. package/src/__tests__/selectionConversions.test.ts +80 -0
  68. package/src/__tests__/statusBar.test.tsx +27 -0
  69. package/src/__tests__/teleprompter.test.tsx +190 -0
  70. package/src/__tests__/templateAnnotationRoundTrip.test.ts +26 -0
  71. package/src/__tests__/templatePickerPortal.test.tsx +62 -0
  72. package/src/__tests__/tiptapBridge.test.ts +71 -7
  73. package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
  74. package/src/__tests__/tiptapImageRoundTrip.test.ts +1 -1
  75. package/src/__tests__/toolbarSelectionConversion.test.tsx +164 -0
  76. package/src/__tests__/transformStyleId.test.ts +13 -0
  77. package/src/__tests__/useImageEditor.test.tsx +67 -0
  78. package/src/__tests__/useMediaRecorder.test.ts +75 -0
  79. package/src/__tests__/viewMenuPanel.test.tsx +130 -0
  80. package/src/__tests__/wysiwygImageUpload.test.ts +69 -0
  81. package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +56 -0
  82. package/src/asciiDiagram/AsciiDiagramExtension.ts +343 -0
  83. package/src/asciiDiagram/AsciiDiagramWidget.tsx +254 -0
  84. package/src/asciiDiagram/RepairableDiagramExtension.ts +200 -0
  85. package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +145 -0
  86. package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +159 -0
  87. package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +284 -0
  88. package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
  89. package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
  90. package/src/asciiDiagram/asciiDiagramCommands.ts +174 -0
  91. package/src/asciiDiagram/asciiDiagramData.ts +149 -0
  92. package/src/asciiDiagram/asciiDiagramOps.ts +238 -0
  93. package/src/asciiDiagram/asciiPaste.ts +19 -0
  94. package/src/blockTagActivity.ts +233 -0
  95. package/src/buildDocumentPreviewMarkdown.ts +168 -0
  96. package/src/buildPreviewDoc.ts +19 -12
  97. package/src/codeContext/types.ts +1 -1
  98. package/src/customTemplates/CustomLayoutManager.tsx +3 -2
  99. package/src/customTemplates/CustomTemplateContext.tsx +6 -0
  100. package/src/customTemplates/TemplateDesigner.tsx +7 -4
  101. package/src/customTemplates/__tests__/library.test.ts +8 -0
  102. package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +13 -0
  103. package/src/customTemplates/designer.css +158 -99
  104. package/src/customTemplates/library.ts +16 -3
  105. package/src/customTemplates/useMemoryLayerAdapter.ts +7 -1
  106. package/src/customThemes/CustomThemeContext.tsx +6 -0
  107. package/src/customThemes/CustomThemeDialog.tsx +35 -5
  108. package/src/customThemes/ImportThemeSection.tsx +178 -0
  109. package/src/customThemes/__tests__/customThemeLibrary.test.ts +9 -0
  110. package/src/customThemes/__tests__/importThemeSection.test.tsx +192 -0
  111. package/src/customThemes/customThemeLibrary.ts +7 -4
  112. package/src/customThemes/index.ts +7 -1
  113. package/src/customThemes/themeDraft.ts +23 -7
  114. package/src/diagram/DiagramCanvas.tsx +25 -6
  115. package/src/diagram/types.ts +35 -0
  116. package/src/editorScrollSync.ts +73 -0
  117. package/src/emojiData.ts +3 -23
  118. package/src/frontmatterSettings.ts +23 -0
  119. package/src/headingTransition.ts +0 -15
  120. package/src/imageEditor/CanvasSurface.tsx +29 -23
  121. package/src/imageEditor/LayersPanel.tsx +78 -3
  122. package/src/imageEditor/Toolbar.tsx +5 -1
  123. package/src/imageEditor/icons.tsx +4 -0
  124. package/src/imageEditor/image-editor.css +75 -0
  125. package/src/imageEditor/layers/SelectionHandles.tsx +1 -1
  126. package/src/imageEditor/useImageEditor.ts +71 -14
  127. package/src/index.ts +198 -20
  128. package/src/jsonEditor/JsonEditorContext.tsx +12 -6
  129. package/src/jsonEditor/RenderNode.tsx +24 -3
  130. package/src/jsonEditor/editors.tsx +86 -19
  131. package/src/mediaEntries.ts +12 -0
  132. package/src/monaco.ts +35 -0
  133. package/src/monacoWorkers.ts +89 -0
  134. package/src/outlineSource.ts +171 -0
  135. package/src/presentation/PresentationMode.tsx +596 -0
  136. package/src/rawEditorIsolation.ts +18 -0
  137. package/src/recorder/RecorderButton.tsx +9 -1
  138. package/src/recorder/RecorderModal.tsx +84 -41
  139. package/src/recorder/RecorderPanel.tsx +9 -1
  140. package/src/recorder/hooks/useMediaRecorder.ts +97 -53
  141. package/src/resolveBlockVisual.ts +10 -4
  142. package/src/scene/Scene.tsx +48 -18
  143. package/src/scene/SceneBlockExtension.ts +17 -10
  144. package/src/scene/SceneBlockWidget.tsx +5 -1
  145. package/src/scene/SceneViewport.tsx +7 -3
  146. package/src/scene/ShapePalette.tsx +17 -2
  147. package/src/scene/__tests__/DiagramAdapter.test.ts +86 -0
  148. package/src/scene/__tests__/SceneBlockExtension.test.ts +33 -0
  149. package/src/scene/__tests__/sceneIsolation.test.tsx +119 -0
  150. package/src/scene/adapters/DiagramAdapter.ts +12 -101
  151. package/src/scene/adapters/DrawingAdapter.ts +6 -1
  152. package/src/scene/adapters/LayoutAdapter.ts +3 -0
  153. package/src/scene/commands/SceneCommand.ts +17 -3
  154. package/src/scene/index.ts +1 -6
  155. package/src/scene/layers/DiagramEdges.tsx +21 -7
  156. package/src/scene/layers/edgeGeometry.ts +9 -1
  157. package/src/scene/layers/nodeCard.tsx +27 -8
  158. package/src/scene/scene.css +5 -2
  159. package/src/scene/text/SceneTextOverlay.tsx +5 -5
  160. package/src/scene/text/sceneTextChannel.ts +26 -20
  161. package/src/scene/text/sceneTextConfig.ts +4 -0
  162. package/src/scene/tools/ConnectTool.ts +13 -4
  163. package/src/scene/tools/SceneTool.ts +8 -0
  164. package/src/scene/tools/SelectTool.ts +39 -23
  165. package/src/scene/tools/ShapeTool.ts +2 -3
  166. package/src/selectionConversions.ts +155 -0
  167. package/src/styles/ascii-diagram.css +79 -0
  168. package/src/styles/ascii-timeline.css +596 -0
  169. package/src/styles/editor.css +736 -120
  170. package/src/styles/index.css +3 -0
  171. package/src/styles/tree-view.css +189 -0
  172. package/src/teleprompter/TeleprompterControls.tsx +218 -0
  173. package/src/teleprompter/TeleprompterSelfView.tsx +22 -0
  174. package/src/teleprompter/TeleprompterSurface.tsx +267 -0
  175. package/src/teleprompter/TeleprompterView.tsx +338 -0
  176. package/src/teleprompter/canvasRenderer.ts +161 -0
  177. package/src/teleprompter/floatingWindow.ts +352 -0
  178. package/src/teleprompter/index.ts +61 -0
  179. package/src/teleprompter/pcmWorklet.ts +62 -0
  180. package/src/teleprompter/recording/insertPreamble.ts +80 -0
  181. package/src/teleprompter/recording/narrationSave.ts +134 -0
  182. package/src/teleprompter/recording/useNarrationRecorder.ts +367 -0
  183. package/src/teleprompter/scrollModel.ts +85 -0
  184. package/src/teleprompter/teleprompterTheme.ts +303 -0
  185. package/src/teleprompter/types.ts +38 -0
  186. package/src/teleprompter/useFloatingWindow.ts +74 -0
  187. package/src/teleprompter/useMicAnalysis.ts +211 -0
  188. package/src/teleprompter/useTeleprompter.ts +421 -0
  189. package/src/templateContentPreviewResolver.ts +12 -5
  190. package/src/timeline/TimelineEditorWidget.tsx +861 -0
  191. package/src/timeline/TimelineViewExtension.ts +252 -0
  192. package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +414 -0
  193. package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
  194. package/src/timeline/__tests__/timelineCommands.test.ts +359 -0
  195. package/src/timeline/__tests__/timelineOps.test.ts +277 -0
  196. package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
  197. package/src/timeline/timelineCommands.ts +421 -0
  198. package/src/timeline/timelineData.ts +54 -0
  199. package/src/timeline/timelineOps.ts +381 -0
  200. package/src/timeline/timelinePaste.ts +8 -0
  201. package/src/tiptapBridge.ts +47 -21
  202. package/src/transformStyleId.ts +17 -0
  203. package/src/treeview/TreeOutlineWidget.tsx +390 -0
  204. package/src/treeview/TreeViewExtension.ts +250 -0
  205. package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +156 -0
  206. package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
  207. package/src/treeview/__tests__/treeOps.test.ts +177 -0
  208. package/src/treeview/__tests__/treePaste.test.ts +40 -0
  209. package/src/treeview/__tests__/treeViewCommands.test.ts +179 -0
  210. package/src/treeview/treeOps.ts +231 -0
  211. package/src/treeview/treePaste.ts +13 -0
  212. package/src/treeview/treeViewCommands.ts +99 -0
  213. package/src/treeview/treeViewData.ts +41 -0
  214. package/src/useMonacoLoader.ts +11 -34
  215. package/src/wysiwygImageUpload.ts +113 -0
  216. package/src/diagram/DiagramExtension.ts +0 -209
  217. package/src/diagram/DiagramWidget.tsx +0 -272
  218. 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.2",
3
+ "version": "2.0.1",
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.2",
56
- "@bendyline/squisq-formats": "1.4.2",
57
- "@bendyline/squisq-react": "1.4.2",
59
+ "@bendyline/squisq": "2.0.1",
60
+ "@bendyline/squisq-formats": "2.0.1",
61
+ "@bendyline/squisq-react": "2.0.1",
58
62
  "@fortawesome/fontawesome-free": "7.2.0",
59
63
  "@monaco-editor/react": "4.7.0",
60
64
  "@tiptap/extension-image": "2.27.2",
@@ -16,12 +16,11 @@
16
16
  * `TemplateBadgePopover`.
17
17
  */
18
18
 
19
- import { useEffect, useState } from 'react';
19
+ import { useEffect, useId, useRef, useState } from 'react';
20
20
  import { createPortal } from 'react-dom';
21
21
  import { TransitionPicker } from './TransitionPicker';
22
22
  import {
23
23
  readBlockAttrsTransition,
24
- setBlockAttrsTransition,
25
24
  setHeadingAttrsTransition,
26
25
  type TransitionFields,
27
26
  } from './headingTransition';
@@ -37,14 +36,17 @@ export interface BlockPropertiesPopoverProps {
37
36
  /** Apply a new `dataBlockAttrs` inner to the heading (null clears it). */
38
37
  onChange: (nextInner: string | null) => void;
39
38
  /** Apply a paired `dataBlockAttrs` / `dataTemplateParams` transition rewrite. */
40
- onAnnotationChange?: (next: {
39
+ onAnnotationChange: (next: {
41
40
  blockAttrsInner: string | null;
42
41
  templateParams: string | null;
43
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;
44
47
  onClose: () => void;
45
48
  }
46
49
 
47
- const PANEL_ID = 'squisq-block-props-portal';
48
50
  /** The nested transition flyout is portaled out here; don't treat it as "outside". */
49
51
  const TRANSITION_FLYOUT = '.squisq-transition-flyout';
50
52
 
@@ -54,15 +56,19 @@ export function BlockPropertiesPopover({
54
56
  templateParams,
55
57
  onChange,
56
58
  onAnnotationChange,
59
+ colorScheme = 'light',
60
+ accentColor,
57
61
  onClose,
58
62
  }: BlockPropertiesPopoverProps) {
63
+ const panelRef = useRef<HTMLDivElement>(null);
64
+ const panelId = `squisq-block-props-portal-${useId().replace(/:/g, '')}`;
59
65
  // Working copy of the Pandoc inner; successive edits compose off it.
60
66
  const [inner, setInner] = useState<string | null>(blockAttrs);
61
67
  const [templateInner, setTemplateInner] = useState<string | null>(templateParams);
62
68
  const [style, setStyle] = useState<React.CSSProperties>(() => computeStyle(anchorRect));
63
69
 
64
70
  useEffect(() => {
65
- requestAnimationFrame(() => setStyle(computeStyle(anchorRect)));
71
+ requestAnimationFrame(() => setStyle(computeStyle(anchorRect, panelRef.current)));
66
72
  }, [anchorRect]);
67
73
 
68
74
  // Outside click + Escape close. A click inside the popover OR inside the
@@ -73,7 +79,7 @@ export function BlockPropertiesPopover({
73
79
  };
74
80
  const onMouse = (e: MouseEvent) => {
75
81
  const target = e.target as HTMLElement;
76
- const inPanel = document.getElementById(PANEL_ID)?.contains(target);
82
+ const inPanel = panelRef.current?.contains(target);
77
83
  const inFlyout = target.closest?.(TRANSITION_FLYOUT);
78
84
  if (!inPanel && !inFlyout) onClose();
79
85
  };
@@ -96,10 +102,6 @@ export function BlockPropertiesPopover({
96
102
  const startTime = readBlockAttrsValue(inner, 'startTime');
97
103
 
98
104
  const onTransition = (next: TransitionFields) => {
99
- if (!onAnnotationChange) {
100
- apply(setBlockAttrsTransition(inner, next));
101
- return;
102
- }
103
105
  const updated = setHeadingAttrsTransition(inner, templateInner, next);
104
106
  setInner(updated.blockAttrsInner);
105
107
  setTemplateInner(updated.templateParams);
@@ -110,15 +112,22 @@ export function BlockPropertiesPopover({
110
112
 
111
113
  return createPortal(
112
114
  <div
113
- id={PANEL_ID}
115
+ ref={panelRef}
116
+ id={panelId}
114
117
  className="squisq-block-props-popover"
118
+ data-theme={colorScheme}
115
119
  role="dialog"
116
120
  aria-label="Block properties"
117
- style={style}
121
+ style={{ ...style, ...blockAccentStyle(accentColor) }}
118
122
  >
119
123
  <div className="squisq-block-props-row">
120
124
  <span className="squisq-block-props-label">Transition</span>
121
- <TransitionPicker value={transition} onChange={onTransition} />
125
+ <TransitionPicker
126
+ value={transition}
127
+ onChange={onTransition}
128
+ colorScheme={colorScheme}
129
+ accentColor={accentColor}
130
+ />
122
131
  </div>
123
132
 
124
133
  <div className="squisq-block-props-row">
@@ -147,6 +156,12 @@ export function BlockPropertiesPopover({
147
156
  );
148
157
  }
149
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
+
150
165
  function NumberField({
151
166
  value,
152
167
  placeholder,
@@ -178,8 +193,8 @@ function NumberField({
178
193
  }
179
194
 
180
195
  /** Place the panel below the badge, flipping above / clamping to the viewport. */
181
- function computeStyle(rect: DOMRect): React.CSSProperties {
182
- const panel = document.getElementById(PANEL_ID)?.getBoundingClientRect();
196
+ function computeStyle(rect: DOMRect, element?: HTMLElement | null): React.CSSProperties {
197
+ const panel = element?.getBoundingClientRect();
183
198
  const width = panel?.width ?? 280;
184
199
  const height = panel?.height ?? 160;
185
200
  const margin = 8;
@@ -19,21 +19,20 @@ 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';
28
+ import {
29
+ FRONTMATTER_SETTING_DEFAULTS,
30
+ FRONTMATTER_SETTING_KEYS,
31
+ omitFrontmatterDefault,
32
+ } from './frontmatterSettings';
26
33
 
27
34
  // ── Frontmatter key constants ─────────────────────────────────────
28
35
 
29
- /** Mirror of `FM_KEYS` in PreviewControls.tsx. Canonical names are the
30
- * ones we write; legacy aliases are still read so older docs work. */
31
- const FM = {
32
- theme: { canonical: 'squisq-theme', legacy: 'theme' },
33
- transform: { canonical: 'squisq-transform', legacy: 'transform-style' },
34
- captions: { canonical: 'squisq-captions', legacy: 'caption-style' },
35
- } as const;
36
-
37
36
  function readFm(
38
37
  fm: Record<string, unknown> | undefined,
39
38
  canonical: string,
@@ -75,9 +74,20 @@ export function DocumentSettingsDialog({
75
74
  const inferredTitle = useMemo(() => inferDocumentTitle(parsed), [parsed]);
76
75
 
77
76
  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 currentCaptions = readFm(frontmatter, FM.captions.canonical, FM.captions.legacy);
77
+ const currentTheme = readFrontmatterThemeId(frontmatter) ?? '';
78
+ const currentTransform =
79
+ resolvePersistedTransformStyleId(
80
+ readFm(
81
+ frontmatter,
82
+ FRONTMATTER_SETTING_KEYS.transform.canonical,
83
+ FRONTMATTER_SETTING_KEYS.transform.legacy,
84
+ ),
85
+ ) ?? '';
86
+ const currentCaptions = readFm(
87
+ frontmatter,
88
+ FRONTMATTER_SETTING_KEYS.captions.canonical,
89
+ FRONTMATTER_SETTING_KEYS.captions.legacy,
90
+ );
81
91
 
82
92
  const [title, setTitle] = useState(currentTitle);
83
93
  const [theme, setTheme] = useState(currentTheme);
@@ -127,33 +137,29 @@ export function DocumentSettingsDialog({
127
137
 
128
138
  const updates: Record<string, string | null> = {
129
139
  title: nextTitle,
130
- [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 } : {}),
134
- [FM.transform.canonical]: transform || null,
135
- ...(currentTransform && transform !== currentTransform
136
- ? { [FM.transform.legacy]: null }
137
- : {}),
138
- [FM.captions.canonical]: captions || null,
139
- ...(currentCaptions && captions !== currentCaptions ? { [FM.captions.legacy]: null } : {}),
140
+ [FRONTMATTER_SETTING_KEYS.theme.canonical]: omitFrontmatterDefault(
141
+ theme || FRONTMATTER_SETTING_DEFAULTS.theme,
142
+ FRONTMATTER_SETTING_DEFAULTS.theme,
143
+ ),
144
+ // Saving settings canonicalizes all older theme spellings.
145
+ [FRONTMATTER_SETTING_KEYS.theme.legacy[0]]: null,
146
+ [FRONTMATTER_SETTING_KEYS.theme.legacy[1]]: null,
147
+ [FRONTMATTER_SETTING_KEYS.transform.canonical]: omitFrontmatterDefault(
148
+ transform,
149
+ FRONTMATTER_SETTING_DEFAULTS.transform,
150
+ ),
151
+ [FRONTMATTER_SETTING_KEYS.transform.legacy]: null,
152
+ [FRONTMATTER_SETTING_KEYS.captions.canonical]: omitFrontmatterDefault(
153
+ captions || FRONTMATTER_SETTING_DEFAULTS.captions,
154
+ FRONTMATTER_SETTING_DEFAULTS.captions,
155
+ ),
156
+ [FRONTMATTER_SETTING_KEYS.captions.legacy]: null,
140
157
  };
141
158
 
142
159
  const nextSource = setFrontmatterValues(markdownSource, updates);
143
160
  onSave(nextSource);
144
161
  },
145
- [
146
- title,
147
- theme,
148
- transform,
149
- captions,
150
- inferredTitle,
151
- markdownSource,
152
- currentTheme,
153
- currentTransform,
154
- currentCaptions,
155
- onSave,
156
- ],
162
+ [title, theme, transform, captions, inferredTitle, markdownSource, onSave],
157
163
  );
158
164
 
159
165
  return (