@bendyline/squisq-editor-react 2.0.0 → 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 (55) hide show
  1. package/dist/index.d.ts +75 -33
  2. package/dist/index.js +1717 -935
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles/index.css +145 -5
  5. package/package.json +4 -4
  6. package/src/DocumentSettingsDialog.tsx +32 -18
  7. package/src/EditorShell.tsx +1 -1
  8. package/src/PreviewControls.tsx +57 -24
  9. package/src/RecorderEntry.tsx +2 -0
  10. package/src/Toolbar.tsx +164 -19
  11. package/src/__tests__/codeContextSectionView.test.tsx +8 -6
  12. package/src/__tests__/documentSettingsDialog.test.tsx +22 -0
  13. package/src/__tests__/mediaAttachmentFlow.test.ts +2 -2
  14. package/src/__tests__/previewControls.test.tsx +164 -0
  15. package/src/__tests__/recorderTheme.test.tsx +42 -0
  16. package/src/__tests__/selectionConversions.test.ts +80 -0
  17. package/src/__tests__/tiptapBridge.test.ts +48 -7
  18. package/src/__tests__/tiptapImageRoundTrip.test.ts +1 -1
  19. package/src/__tests__/toolbarSelectionConversion.test.tsx +164 -0
  20. package/src/asciiDiagram/AsciiDiagramWidget.tsx +34 -4
  21. package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +58 -1
  22. package/src/asciiDiagram/asciiDiagramCommands.ts +36 -10
  23. package/src/asciiDiagram/asciiDiagramData.ts +33 -0
  24. package/src/asciiDiagram/asciiDiagramOps.ts +19 -0
  25. package/src/codeContext/types.ts +1 -1
  26. package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +13 -0
  27. package/src/customTemplates/useMemoryLayerAdapter.ts +7 -1
  28. package/src/diagram/DiagramCanvas.tsx +16 -2
  29. package/src/frontmatterSettings.ts +23 -0
  30. package/src/index.ts +7 -1
  31. package/src/recorder/RecorderButton.tsx +9 -1
  32. package/src/recorder/RecorderModal.tsx +84 -41
  33. package/src/recorder/RecorderPanel.tsx +9 -1
  34. package/src/scene/__tests__/sceneIsolation.test.tsx +27 -1
  35. package/src/scene/adapters/DrawingAdapter.ts +6 -1
  36. package/src/scene/adapters/LayoutAdapter.ts +3 -0
  37. package/src/scene/commands/SceneCommand.ts +13 -2
  38. package/src/scene/tools/SelectTool.ts +5 -5
  39. package/src/selectionConversions.ts +155 -0
  40. package/src/styles/ascii-timeline.css +101 -4
  41. package/src/styles/editor.css +30 -0
  42. package/src/styles/tree-view.css +51 -1
  43. package/src/timeline/TimelineEditorWidget.tsx +200 -41
  44. package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +61 -2
  45. package/src/timeline/__tests__/timelineCommands.test.ts +32 -0
  46. package/src/timeline/__tests__/timelineOps.test.ts +55 -0
  47. package/src/timeline/timelineCommands.ts +54 -0
  48. package/src/timeline/timelineOps.ts +107 -3
  49. package/src/tiptapBridge.ts +23 -5
  50. package/src/treeview/TreeOutlineWidget.tsx +153 -3
  51. package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +156 -0
  52. package/src/treeview/__tests__/treeOps.test.ts +52 -0
  53. package/src/treeview/__tests__/treeViewCommands.test.ts +16 -0
  54. package/src/treeview/treeOps.ts +59 -0
  55. package/src/treeview/treeViewCommands.ts +5 -0
@@ -7560,6 +7560,7 @@
7560
7560
  z-index: 100;
7561
7561
  min-width: 200px;
7562
7562
  padding: 4px 0;
7563
+ font-family: var( --squisq-ux-font, -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif );
7563
7564
  background: #fff;
7564
7565
  border: 1px solid #e5e7eb;
7565
7566
  border-radius: 6px;
@@ -7574,6 +7575,11 @@
7574
7575
  color: #9ca3af;
7575
7576
  user-select: none;
7576
7577
  }
7578
+ .squisq-insert-menu-header--separated {
7579
+ margin-top: 4px;
7580
+ padding-top: 8px;
7581
+ border-top: 1px solid #e5e7eb;
7582
+ }
7577
7583
  .squisq-insert-menu[data-theme=dark] {
7578
7584
  background: #1f2937;
7579
7585
  border-color: #374151;
@@ -7588,6 +7594,9 @@
7588
7594
  .squisq-insert-menu[data-theme=dark] .squisq-insert-menu-header {
7589
7595
  color: #6b7280;
7590
7596
  }
7597
+ .squisq-insert-menu[data-theme=dark] .squisq-insert-menu-header--separated {
7598
+ border-top-color: #374151;
7599
+ }
7591
7600
  .squisq-template-picker {
7592
7601
  display: flex;
7593
7602
  align-items: center;
@@ -8686,6 +8695,9 @@
8686
8695
  margin: 0.5em 0;
8687
8696
  color: var(--squisq-theme-text-muted, #6b7280);
8688
8697
  }
8698
+ .squisq-wysiwyg-editor blockquote > p {
8699
+ margin: 0;
8700
+ }
8689
8701
  .squisq-wysiwyg-editor pre {
8690
8702
  background: var(--squisq-theme-bg-muted, #1e293b);
8691
8703
  color: var(--squisq-theme-text, #e2e8f0);
@@ -14080,7 +14092,7 @@
14080
14092
  .squisq-tree-btn:hover {
14081
14093
  border-color: var(--squisq-text-muted, #94a3b8);
14082
14094
  }
14083
- .squisq-tree-rows {
14095
+ .squisq-tree-outline .squisq-tree-rows {
14084
14096
  list-style: none;
14085
14097
  margin: 0;
14086
14098
  padding: 0;
@@ -14089,15 +14101,45 @@
14089
14101
  border-left: 1px solid var(--squisq-border, #e2e8f0);
14090
14102
  margin-left: 8px;
14091
14103
  }
14104
+ .squisq-tree-item {
14105
+ position: relative;
14106
+ }
14092
14107
  .squisq-tree-row {
14108
+ position: relative;
14093
14109
  display: flex;
14094
14110
  align-items: center;
14095
14111
  gap: 4px;
14096
14112
  padding: 1px 0;
14113
+ border-radius: 4px;
14097
14114
  }
14098
14115
  .squisq-tree-row:hover .squisq-tree-controls {
14099
14116
  opacity: 1;
14100
14117
  }
14118
+ .squisq-tree-item--dragging {
14119
+ opacity: 0.42;
14120
+ }
14121
+ .squisq-tree-item--drop-before > .squisq-tree-row::before,
14122
+ .squisq-tree-item--drop-after::after {
14123
+ content: "";
14124
+ position: absolute;
14125
+ z-index: 2;
14126
+ left: 0;
14127
+ right: 0;
14128
+ height: 2px;
14129
+ border-radius: 999px;
14130
+ background: #6366f1;
14131
+ pointer-events: none;
14132
+ }
14133
+ .squisq-tree-item--drop-before > .squisq-tree-row::before {
14134
+ top: -1px;
14135
+ }
14136
+ .squisq-tree-item--drop-after::after {
14137
+ bottom: -1px;
14138
+ }
14139
+ .squisq-tree-item--drop-child > .squisq-tree-row {
14140
+ background: rgba(99, 102, 241, 0.12);
14141
+ box-shadow: inset 0 0 0 1px #6366f1;
14142
+ }
14101
14143
  .squisq-tree-chevron {
14102
14144
  flex: 0 0 auto;
14103
14145
  width: 16px;
@@ -14142,6 +14184,18 @@
14142
14184
  opacity: 0;
14143
14185
  transition: opacity 0.12s;
14144
14186
  }
14187
+ .squisq-tree-drag-handle {
14188
+ flex: 0 0 auto;
14189
+ display: inline-flex;
14190
+ align-items: center;
14191
+ justify-content: center;
14192
+ width: 16px;
14193
+ color: var(--squisq-text-muted, #64748b);
14194
+ cursor: grab;
14195
+ }
14196
+ .squisq-tree-drag-handle:active {
14197
+ cursor: grabbing;
14198
+ }
14145
14199
  .squisq-tree-controls button {
14146
14200
  border: none;
14147
14201
  background: transparent;
@@ -14173,7 +14227,7 @@
14173
14227
  }
14174
14228
  .squisq-ascii-timeline-editor {
14175
14229
  --squisq-ascii-timeline-accent: var(--squisq-theme-primary, #6366f1);
14176
- --squisq-ascii-timeline-label-width: 120px;
14230
+ --squisq-ascii-timeline-label-width: 160px;
14177
14231
  border: 1px solid var(--squisq-border, #e2e8f0);
14178
14232
  border-radius: 10px;
14179
14233
  background: var(--squisq-input-bg, #fff);
@@ -14200,6 +14254,30 @@
14200
14254
  .squisq-ascii-timeline-header small {
14201
14255
  color: var(--squisq-text-muted, #64748b);
14202
14256
  }
14257
+ .squisq-ascii-timeline-header-actions {
14258
+ display: flex;
14259
+ align-items: center;
14260
+ justify-content: flex-end;
14261
+ gap: 10px;
14262
+ }
14263
+ .squisq-ascii-timeline-header-actions button {
14264
+ display: inline-flex;
14265
+ align-items: center;
14266
+ gap: 5px;
14267
+ padding: 4px 8px;
14268
+ border: 1px solid var(--squisq-border, #cbd5e1);
14269
+ border-radius: 5px;
14270
+ background: var(--squisq-input-bg, #fff);
14271
+ color: var(--squisq-ascii-timeline-accent);
14272
+ font: inherit;
14273
+ font-size: 12px;
14274
+ cursor: pointer;
14275
+ }
14276
+ .squisq-ascii-timeline-header-actions button:hover,
14277
+ .squisq-ascii-timeline-header-actions button:focus-visible {
14278
+ border-color: var(--squisq-ascii-timeline-accent);
14279
+ outline: 2px solid color-mix(in srgb, var(--squisq-ascii-timeline-accent) 22%, transparent);
14280
+ }
14203
14281
  .squisq-ascii-timeline-canvas {
14204
14282
  position: relative;
14205
14283
  min-width: 600px;
@@ -14225,9 +14303,64 @@
14225
14303
  text-align: right;
14226
14304
  white-space: nowrap;
14227
14305
  }
14228
- .squisq-ascii-timeline-track-label > span {
14306
+ .squisq-ascii-timeline-track-name {
14307
+ min-width: 0;
14308
+ flex: 1 1 auto;
14309
+ padding: 3px 4px;
14229
14310
  overflow: hidden;
14311
+ border: 1px solid transparent;
14312
+ border-radius: 4px;
14313
+ background: transparent;
14314
+ color: inherit;
14315
+ font: inherit;
14316
+ text-align: right;
14230
14317
  text-overflow: ellipsis;
14318
+ white-space: nowrap;
14319
+ }
14320
+ button.squisq-ascii-timeline-track-name {
14321
+ cursor: text;
14322
+ }
14323
+ button.squisq-ascii-timeline-track-name:hover,
14324
+ button.squisq-ascii-timeline-track-name:focus-visible {
14325
+ border-color: var(--squisq-border, #cbd5e1);
14326
+ background: var(--squisq-input-bg, #fff);
14327
+ outline: none;
14328
+ }
14329
+ .squisq-ascii-timeline-track-name-input {
14330
+ width: 100%;
14331
+ min-width: 0;
14332
+ flex: 1 1 auto;
14333
+ padding: 3px 4px;
14334
+ border: 1px solid var(--squisq-ascii-timeline-accent);
14335
+ border-radius: 4px;
14336
+ background: var(--squisq-input-bg, #fff);
14337
+ color: var(--squisq-text, #1e293b);
14338
+ font: inherit;
14339
+ text-align: right;
14340
+ outline: 2px solid color-mix(in srgb, var(--squisq-ascii-timeline-accent) 22%, transparent);
14341
+ }
14342
+ .squisq-ascii-timeline-track-delete {
14343
+ display: grid;
14344
+ width: 22px;
14345
+ height: 22px;
14346
+ flex: 0 0 22px;
14347
+ place-items: center;
14348
+ padding: 0;
14349
+ border: 0;
14350
+ border-radius: 4px;
14351
+ background: transparent;
14352
+ color: var(--squisq-text-muted, #64748b);
14353
+ cursor: pointer;
14354
+ }
14355
+ .squisq-ascii-timeline-track-delete:hover,
14356
+ .squisq-ascii-timeline-track-delete:focus-visible {
14357
+ background: color-mix(in srgb, #ef4444 12%, transparent);
14358
+ color: #dc2626;
14359
+ outline: 2px solid color-mix(in srgb, #ef4444 25%, transparent);
14360
+ }
14361
+ .squisq-ascii-timeline-track-delete:disabled {
14362
+ opacity: 0.35;
14363
+ cursor: not-allowed;
14231
14364
  }
14232
14365
  .squisq-ascii-timeline-track-add {
14233
14366
  display: grid;
@@ -14244,7 +14377,8 @@
14244
14377
  cursor: pointer;
14245
14378
  }
14246
14379
  .squisq-ascii-timeline-track-add:hover,
14247
- .squisq-ascii-timeline-track-add:focus-visible {
14380
+ .squisq-ascii-timeline-track-add:focus-visible,
14381
+ .squisq-ascii-timeline-track-add[aria-pressed=true] {
14248
14382
  border-color: var(--squisq-ascii-timeline-accent);
14249
14383
  outline: 2px solid color-mix(in srgb, var(--squisq-ascii-timeline-accent) 22%, transparent);
14250
14384
  }
@@ -14253,6 +14387,9 @@
14253
14387
  height: 96px;
14254
14388
  min-width: 420px;
14255
14389
  outline: none;
14390
+ cursor: default;
14391
+ }
14392
+ .squisq-ascii-timeline-rail--adding {
14256
14393
  cursor: crosshair;
14257
14394
  }
14258
14395
  .squisq-ascii-timeline-rail--readonly {
@@ -14554,7 +14691,7 @@
14554
14691
  }
14555
14692
  @media (max-width: 760px) {
14556
14693
  .squisq-ascii-timeline-editor {
14557
- --squisq-ascii-timeline-label-width: 88px;
14694
+ --squisq-ascii-timeline-label-width: 132px;
14558
14695
  }
14559
14696
  .squisq-ascii-timeline-inspector {
14560
14697
  grid-template-columns: 1fr 1fr;
@@ -14566,6 +14703,9 @@
14566
14703
  .squisq-ascii-timeline-delete {
14567
14704
  grid-column: 2 / 3;
14568
14705
  }
14706
+ .squisq-ascii-timeline-header-actions small {
14707
+ display: none;
14708
+ }
14569
14709
  }
14570
14710
  @media (prefers-reduced-motion: reduce) {
14571
14711
  .squisq-ascii-timeline-gap-add {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-editor-react",
3
- "version": "2.0.0",
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",
@@ -56,9 +56,9 @@
56
56
  }
57
57
  },
58
58
  "dependencies": {
59
- "@bendyline/squisq": "2.0.0",
60
- "@bendyline/squisq-formats": "2.0.0",
61
- "@bendyline/squisq-react": "2.0.0",
59
+ "@bendyline/squisq": "2.0.1",
60
+ "@bendyline/squisq-formats": "2.0.1",
61
+ "@bendyline/squisq-react": "2.0.1",
62
62
  "@fortawesome/fontawesome-free": "7.2.0",
63
63
  "@monaco-editor/react": "4.7.0",
64
64
  "@tiptap/extension-image": "2.27.2",
@@ -25,17 +25,14 @@ import {
25
25
  import { getTransformStyleSummaries } from '@bendyline/squisq/transform';
26
26
  import { ThemePicker } from './ThemePicker';
27
27
  import { resolvePersistedTransformStyleId } from './transformStyleId';
28
+ import {
29
+ FRONTMATTER_SETTING_DEFAULTS,
30
+ FRONTMATTER_SETTING_KEYS,
31
+ omitFrontmatterDefault,
32
+ } from './frontmatterSettings';
28
33
 
29
34
  // ── Frontmatter key constants ─────────────────────────────────────
30
35
 
31
- /** Mirror of `FM_KEYS` in PreviewControls.tsx. Canonical names are the
32
- * ones we write; legacy aliases are still read so older docs work. */
33
- const FM = {
34
- theme: { canonical: 'squisq-theme', legacy: ['themeId', 'theme'] },
35
- transform: { canonical: 'squisq-transform', legacy: 'transform-style' },
36
- captions: { canonical: 'squisq-captions', legacy: 'caption-style' },
37
- } as const;
38
-
39
36
  function readFm(
40
37
  fm: Record<string, unknown> | undefined,
41
38
  canonical: string,
@@ -80,9 +77,17 @@ export function DocumentSettingsDialog({
80
77
  const currentTheme = readFrontmatterThemeId(frontmatter) ?? '';
81
78
  const currentTransform =
82
79
  resolvePersistedTransformStyleId(
83
- readFm(frontmatter, FM.transform.canonical, FM.transform.legacy),
80
+ readFm(
81
+ frontmatter,
82
+ FRONTMATTER_SETTING_KEYS.transform.canonical,
83
+ FRONTMATTER_SETTING_KEYS.transform.legacy,
84
+ ),
84
85
  ) ?? '';
85
- const currentCaptions = readFm(frontmatter, FM.captions.canonical, FM.captions.legacy);
86
+ const currentCaptions = readFm(
87
+ frontmatter,
88
+ FRONTMATTER_SETTING_KEYS.captions.canonical,
89
+ FRONTMATTER_SETTING_KEYS.captions.legacy,
90
+ );
86
91
 
87
92
  const [title, setTitle] = useState(currentTitle);
88
93
  const [theme, setTheme] = useState(currentTheme);
@@ -132,20 +137,29 @@ export function DocumentSettingsDialog({
132
137
 
133
138
  const updates: Record<string, string | null> = {
134
139
  title: nextTitle,
135
- [FM.theme.canonical]: theme || null,
140
+ [FRONTMATTER_SETTING_KEYS.theme.canonical]: omitFrontmatterDefault(
141
+ theme || FRONTMATTER_SETTING_DEFAULTS.theme,
142
+ FRONTMATTER_SETTING_DEFAULTS.theme,
143
+ ),
136
144
  // Saving settings canonicalizes all older theme spellings.
137
- [FM.theme.legacy[0]]: null,
138
- [FM.theme.legacy[1]]: null,
139
- [FM.transform.canonical]: transform || null,
140
- [FM.transform.legacy]: null,
141
- [FM.captions.canonical]: captions || null,
142
- ...(currentCaptions && captions !== currentCaptions ? { [FM.captions.legacy]: null } : {}),
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,
143
157
  };
144
158
 
145
159
  const nextSource = setFrontmatterValues(markdownSource, updates);
146
160
  onSave(nextSource);
147
161
  },
148
- [title, theme, transform, captions, inferredTitle, markdownSource, currentCaptions, onSave],
162
+ [title, theme, transform, captions, inferredTitle, markdownSource, onSave],
149
163
  );
150
164
 
151
165
  return (
@@ -716,7 +716,7 @@ function EditorShellInner({
716
716
  *
717
717
  * Without this, upload-via-MediaBin and upload-via-drop both
718
718
  * added the file to the bin and nowhere else — the composer sent
719
- * an empty body and the downstream gezel reported "nothing came
719
+ * an empty body and the downstream consumer reported "nothing came
720
720
  * through."
721
721
  */
722
722
  const insertMediaRef = useCallback(
@@ -47,6 +47,11 @@ import {
47
47
  } from './customThemes';
48
48
  import { Icon } from './Icon';
49
49
  import { resolvePersistedTransformStyleId } from './transformStyleId';
50
+ import {
51
+ FRONTMATTER_SETTING_DEFAULTS,
52
+ FRONTMATTER_SETTING_KEYS,
53
+ omitFrontmatterDefault,
54
+ } from './frontmatterSettings';
50
55
 
51
56
  // ── Context ──────────────────────────────────────────────────────
52
57
 
@@ -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: ['themeId', '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,
@@ -226,7 +221,7 @@ export function PreviewSettingsProvider({
226
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);
@@ -271,7 +266,7 @@ export function PreviewSettingsProvider({
271
266
  );
272
267
  const [selectedThemeId, setSelectedThemeId] = useState<string | null>(null);
273
268
  useEffect(() => setSelectedThemeId(null), [fmTheme]);
274
- const resolvedThemeId = selectedThemeId ?? fmTheme ?? 'standard';
269
+ const resolvedThemeId = selectedThemeId ?? fmTheme ?? FRONTMATTER_SETTING_DEFAULTS.theme;
275
270
  // Doc themes precede browser-library themes in `allThemes`; choosing a
276
271
  // library-only entry copies it into the document below for portable export.
277
272
  const resolvedTheme = useMemo(
@@ -300,9 +295,12 @@ export function PreviewSettingsProvider({
300
295
  const selectedCustom = customThemes.find((theme) => theme.id === id);
301
296
  const alreadyDocScoped = docThemes.some((theme) => theme.id === id);
302
297
  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,
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,
306
304
  };
307
305
  if (selectedCustom && !alreadyDocScoped) {
308
306
  updates[FRONTMATTER_CUSTOM_THEMES_KEY] =
@@ -337,7 +335,7 @@ export function PreviewSettingsProvider({
337
335
  idx >= 0 ? docThemes.map((t, i) => (i === idx ? theme : t)) : [...docThemes, theme];
338
336
  const updates: Record<string, string | null> = {
339
337
  [FRONTMATTER_CUSTOM_THEMES_KEY]: writeCustomThemesToFrontmatter(nextThemes) ?? null,
340
- [FM_KEYS.theme.canonical]: theme.id,
338
+ [FRONTMATTER_SETTING_KEYS.theme.canonical]: theme.id,
341
339
  };
342
340
  if (extras?.templates && extras.templates.length > 0) {
343
341
  const existing = doc?.customTemplates ?? [];
@@ -367,7 +365,11 @@ export function PreviewSettingsProvider({
367
365
  const fmTransform = useMemo(
368
366
  () =>
369
367
  resolvePersistedTransformStyleId(
370
- readFrontmatterKey(frontmatter, FM_KEYS.transform.canonical, FM_KEYS.transform.legacy),
368
+ readFrontmatterKey(
369
+ frontmatter,
370
+ FRONTMATTER_SETTING_KEYS.transform.canonical,
371
+ FRONTMATTER_SETTING_KEYS.transform.legacy,
372
+ ),
371
373
  ),
372
374
  [frontmatter],
373
375
  );
@@ -379,7 +381,13 @@ export function PreviewSettingsProvider({
379
381
  setSelectedTransformStyle(id);
380
382
  if (id !== null) {
381
383
  // Empty string = "None" — remove the key rather than writing a blank value.
382
- persistFrontmatter({ [FM_KEYS.transform.canonical]: id === '' ? null : id });
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
+ });
383
391
  }
384
392
  },
385
393
  [persistFrontmatter],
@@ -391,19 +399,30 @@ export function PreviewSettingsProvider({
391
399
  const fmCaptionMode = useMemo(
392
400
  () =>
393
401
  resolveFrontmatterCaptionMode(
394
- readFrontmatterKey(frontmatter, FM_KEYS.captions.canonical, FM_KEYS.captions.legacy),
402
+ readFrontmatterKey(
403
+ frontmatter,
404
+ FRONTMATTER_SETTING_KEYS.captions.canonical,
405
+ FRONTMATTER_SETTING_KEYS.captions.legacy,
406
+ ),
395
407
  ),
396
408
  [frontmatter],
397
409
  );
398
410
  const [selectedCaptionMode, setSelectedCaptionMode] = useState<CaptionMode | null>(null);
399
411
  useEffect(() => setSelectedCaptionMode(null), [fmCaptionMode]);
400
- const activeCaptionMode = selectedCaptionMode ?? fmCaptionMode ?? 'standard';
412
+ const activeCaptionMode =
413
+ selectedCaptionMode ?? fmCaptionMode ?? FRONTMATTER_SETTING_DEFAULTS.captions;
401
414
  const activeCaptionsEnabled = activeCaptionMode !== 'off';
402
415
  const activeCaptionStyle: CaptionStyle = activeCaptionMode === 'social' ? 'social' : 'standard';
403
416
  const handleSetCaptionMode = useCallback(
404
417
  (mode: CaptionMode) => {
405
418
  setSelectedCaptionMode(mode);
406
- persistFrontmatter({ [FM_KEYS.captions.canonical]: mode });
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
+ });
407
426
  },
408
427
  [persistFrontmatter],
409
428
  );
@@ -413,17 +432,31 @@ export function PreviewSettingsProvider({
413
432
  const fmCoverSlide = useMemo(
414
433
  () =>
415
434
  resolveFrontmatterBoolean(
416
- readFrontmatterKey(frontmatter, FM_KEYS.coverSlide.canonical, FM_KEYS.coverSlide.legacy),
435
+ readFrontmatterKey(
436
+ frontmatter,
437
+ FRONTMATTER_SETTING_KEYS.coverSlide.canonical,
438
+ FRONTMATTER_SETTING_KEYS.coverSlide.legacy,
439
+ ),
417
440
  ),
418
441
  [frontmatter],
419
442
  );
420
443
  const [selectedCoverSlide, setSelectedCoverSlide] = useState<boolean | null>(null);
421
444
  useEffect(() => setSelectedCoverSlide(null), [fmCoverSlide]);
422
- const activeCoverSlide = selectedCoverSlide ?? fmCoverSlide ?? true;
445
+ const activeCoverSlide =
446
+ selectedCoverSlide ?? fmCoverSlide ?? FRONTMATTER_SETTING_DEFAULTS.coverSlide;
423
447
  const handleSetCoverSlideEnabled = useCallback(
424
448
  (enabled: boolean) => {
425
449
  setSelectedCoverSlide(enabled);
426
- persistFrontmatter({ [FM_KEYS.coverSlide.canonical]: enabled ? 'true' : 'false' });
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
+ });
427
460
  },
428
461
  [persistFrontmatter],
429
462
  );
@@ -81,6 +81,7 @@ export function RecorderEntry() {
81
81
  bumpMediaRevision,
82
82
  markdownSource,
83
83
  setMarkdownSource,
84
+ colorScheme,
84
85
  } = useEditorContext();
85
86
 
86
87
  const handleSave = useCallback(
@@ -150,6 +151,7 @@ export function RecorderEntry() {
150
151
  <RecorderPanel
151
152
  mediaProvider={mediaProvider}
152
153
  container={workspaceContainer}
154
+ colorScheme={colorScheme}
153
155
  onSave={handleSave}
154
156
  className="squisq-toolbar-button"
155
157
  />