@bendyline/squisq-editor-react 1.6.0 → 1.6.2

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 (68) hide show
  1. package/README.md +57 -10
  2. package/dist/index.d.ts +476 -105
  3. package/dist/index.js +8703 -6741
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles/fa-brands-400-AHOAZHCU.woff2 +0 -0
  6. package/dist/styles/fa-regular-400-VRZYIBIZ.woff2 +0 -0
  7. package/dist/styles/fa-solid-900-MDEYK55F.woff2 +0 -0
  8. package/dist/styles/fa-v4compatibility-ETEVP6IB.woff2 +0 -0
  9. package/dist/styles/index.css +14617 -0
  10. package/package.json +15 -7
  11. package/src/BlockPropertiesPopover.tsx +23 -7
  12. package/src/EditorContext.tsx +22 -16
  13. package/src/EditorShell.tsx +121 -35
  14. package/src/MediaBin.tsx +171 -28
  15. package/src/OutlinePanel.tsx +26 -4
  16. package/src/PreviewControls.tsx +475 -145
  17. package/src/PreviewPanel.tsx +17 -9
  18. package/src/RawEditor.tsx +10 -4
  19. package/src/TemplateAnnotation.ts +22 -7
  20. package/src/TemplateContentPreview.tsx +56 -0
  21. package/src/TemplatePicker.tsx +295 -128
  22. package/src/ThemeCustomizerPanel.tsx +22 -15
  23. package/src/Toolbar.tsx +547 -217
  24. package/src/TransitionPicker.tsx +8 -1
  25. package/src/VersionHistoryPanel.tsx +2 -2
  26. package/src/ViewSwitcher.tsx +4 -4
  27. package/src/WysiwygEditor.tsx +45 -3
  28. package/src/__tests__/buildPreviewDocTransition.test.ts +1 -2
  29. package/src/__tests__/codeContextSectionView.test.tsx +95 -0
  30. package/src/__tests__/codeContextZoneManager.test.ts +127 -0
  31. package/src/__tests__/diffContextSections.test.ts +39 -0
  32. package/src/__tests__/editorShellCodeContext.test.tsx +86 -0
  33. package/src/__tests__/editorShellProps.test.tsx +363 -0
  34. package/src/__tests__/headingTransition.test.ts +59 -9
  35. package/src/__tests__/imageEditorShell.test.tsx +23 -0
  36. package/src/__tests__/mediaReferences.test.ts +82 -0
  37. package/src/__tests__/previewControls.test.tsx +163 -0
  38. package/src/__tests__/templateAnnotationRoundTrip.test.ts +23 -2
  39. package/src/__tests__/templateContentPreview.test.ts +101 -0
  40. package/src/__tests__/tiptapBridge.test.ts +47 -0
  41. package/src/__tests__/useJsonEditorTokens.test.ts +59 -0
  42. package/src/__tests__/useMediaRecorder.test.ts +17 -0
  43. package/src/codeContext/CodeContextSectionView.tsx +124 -0
  44. package/src/codeContext/CodeContextZoneManager.ts +149 -0
  45. package/src/codeContext/CodeContextZones.tsx +121 -0
  46. package/src/codeContext/diffContextSections.ts +38 -0
  47. package/src/codeContext/types.ts +75 -0
  48. package/src/diagram/DiagramWidget.tsx +6 -4
  49. package/src/headingTransition.ts +96 -21
  50. package/src/index.ts +34 -2
  51. package/src/jsonEditor/useJsonEditorTokens.ts +13 -43
  52. package/src/mediaReferences.ts +299 -0
  53. package/src/recorder/hooks/useMediaRecorder.ts +9 -10
  54. package/src/scene/Scene.tsx +53 -7
  55. package/src/scene/SceneBlockWidget.tsx +6 -3
  56. package/src/scene/SceneSelection.tsx +19 -15
  57. package/src/scene/SceneSideToolbar.tsx +89 -0
  58. package/src/scene/layers/DiagramEdges.tsx +4 -3
  59. package/src/scene/layers/edgeGeometry.ts +23 -4
  60. package/src/scene/scene.css +142 -2
  61. package/src/scene/tools/ConnectTool.ts +113 -24
  62. package/src/scene/tools/DrawingConnectTool.ts +86 -16
  63. package/src/scene/tools/SceneTool.ts +2 -0
  64. package/src/styles/code-context.css +155 -0
  65. package/src/styles/editor.css +991 -84
  66. package/src/styles/index.css +1 -0
  67. package/src/templateContentPreviewResolver.ts +353 -0
  68. package/src/tiptapBridge.ts +60 -33
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-editor-react",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "React editor shell with raw/WYSIWYG/preview modes for Squisq documents",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -34,10 +34,10 @@
34
34
  "types": "./dist/index.d.ts",
35
35
  "import": "./dist/index.js"
36
36
  },
37
- "./styles": "./src/styles/index.css"
37
+ "./styles": "./dist/styles/index.css"
38
38
  },
39
39
  "scripts": {
40
- "build": "tsup",
40
+ "build": "tsup && node ../../scripts/build-styles.mjs",
41
41
  "dev": "concurrently -n js,dts -c blue,gray -r \"tsup --watch --no-dts --no-clean\" \"tsup --watch --dts-only --no-clean\"",
42
42
  "typecheck": "tsc --noEmit"
43
43
  },
@@ -46,10 +46,15 @@
46
46
  "react": "^18.0.0 || ^19.0.0",
47
47
  "react-dom": "^18.0.0 || ^19.0.0"
48
48
  },
49
+ "peerDependenciesMeta": {
50
+ "monaco-editor": {
51
+ "optional": true
52
+ }
53
+ },
49
54
  "dependencies": {
50
- "@bendyline/squisq": "1.5.0",
51
- "@bendyline/squisq-formats": "1.4.0",
52
- "@bendyline/squisq-react": "1.4.0",
55
+ "@bendyline/squisq": "1.5.2",
56
+ "@bendyline/squisq-formats": "1.4.2",
57
+ "@bendyline/squisq-react": "1.4.2",
53
58
  "@fortawesome/fontawesome-free": "7.2.0",
54
59
  "@monaco-editor/react": "4.7.0",
55
60
  "@tiptap/extension-image": "2.27.2",
@@ -74,5 +79,8 @@
74
79
  "react-dom": "18.3.1",
75
80
  "tsup": "8.5.1",
76
81
  "typescript": "5.9.3"
77
- }
82
+ },
83
+ "sideEffects": [
84
+ "**/*.css"
85
+ ]
78
86
  }
@@ -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
@@ -23,6 +22,7 @@ import { TransitionPicker } from './TransitionPicker';
23
22
  import {
24
23
  readBlockAttrsTransition,
25
24
  setBlockAttrsTransition,
25
+ setHeadingAttrsTransition,
26
26
  type TransitionFields,
27
27
  } from './headingTransition';
28
28
  import { readBlockAttrsValue, setBlockAttrsValue } from './blockProperties';
@@ -36,6 +36,11 @@ export interface BlockPropertiesPopoverProps {
36
36
  templateParams: string | null;
37
37
  /** Apply a new `dataBlockAttrs` inner to the heading (null clears it). */
38
38
  onChange: (nextInner: string | null) => void;
39
+ /** Apply a paired `dataBlockAttrs` / `dataTemplateParams` transition rewrite. */
40
+ onAnnotationChange?: (next: {
41
+ blockAttrsInner: string | null;
42
+ templateParams: string | null;
43
+ }) => void;
39
44
  onClose: () => void;
40
45
  }
41
46
 
@@ -48,10 +53,12 @@ export function BlockPropertiesPopover({
48
53
  blockAttrs,
49
54
  templateParams,
50
55
  onChange,
56
+ onAnnotationChange,
51
57
  onClose,
52
58
  }: BlockPropertiesPopoverProps) {
53
59
  // Working copy of the Pandoc inner; successive edits compose off it.
54
60
  const [inner, setInner] = useState<string | null>(blockAttrs);
61
+ const [templateInner, setTemplateInner] = useState<string | null>(templateParams);
55
62
  const [style, setStyle] = useState<React.CSSProperties>(() => computeStyle(anchorRect));
56
63
 
57
64
  useEffect(() => {
@@ -84,11 +91,20 @@ export function BlockPropertiesPopover({
84
91
  onChange(next);
85
92
  };
86
93
 
87
- const transition = readBlockAttrsTransition(inner, templateParams);
94
+ const transition = readBlockAttrsTransition(inner, templateInner);
88
95
  const duration = readBlockAttrsValue(inner, 'duration');
89
96
  const startTime = readBlockAttrsValue(inner, 'startTime');
90
97
 
91
- const onTransition = (next: TransitionFields) => apply(setBlockAttrsTransition(inner, next));
98
+ const onTransition = (next: TransitionFields) => {
99
+ if (!onAnnotationChange) {
100
+ apply(setBlockAttrsTransition(inner, next));
101
+ return;
102
+ }
103
+ const updated = setHeadingAttrsTransition(inner, templateInner, next);
104
+ setInner(updated.blockAttrsInner);
105
+ setTemplateInner(updated.templateParams);
106
+ onAnnotationChange(updated);
107
+ };
92
108
  const onDuration = (v: string) => apply(setBlockAttrsValue(inner, 'duration', v));
93
109
  const onStartTime = (v: string) => apply(setBlockAttrsValue(inner, 'startTime', v));
94
110
 
@@ -91,7 +91,13 @@ export type DocumentLinkProvider = (query: string) => Promise<DocumentLinkCandid
91
91
  // ─── Types ───────────────────────────────────────────────
92
92
 
93
93
  export type EditorView = 'raw' | 'wysiwyg' | 'preview';
94
- export type EditorTheme = 'light' | 'dark';
94
+ /**
95
+ * Light/dark chrome mode for the editor shell (toolbar, tabs, status bar,
96
+ * side panes). This is the editor's *UI color scheme* — distinct from a
97
+ * Squisq `Theme` object, which styles the rendered document. Renamed from
98
+ * the former `EditorTheme` to remove that ambiguity.
99
+ */
100
+ export type EditorColorScheme = 'light' | 'dark';
95
101
  /**
96
102
  * Document layout mode. `'document'` shows the whole markdown document in
97
103
  * the active view (the historical behavior). `'block'` is the
@@ -128,8 +134,8 @@ export interface EditorState {
128
134
  parseError: string | null;
129
135
  /** Whether a parse is pending */
130
136
  isParsing: boolean;
131
- /** Current color theme */
132
- theme: EditorTheme;
137
+ /** Current light/dark chrome color scheme for the editor shell. */
138
+ colorScheme: EditorColorScheme;
133
139
  /** Operating mode — 'markdown' for the full shell, 'code' for Monaco-only. */
134
140
  editorMode: EditorMode;
135
141
  /** Monaco language ID for the Raw editor. */
@@ -240,8 +246,8 @@ export interface EditorActions {
240
246
  setTiptapEditor: (editor: TiptapEditor | null) => void;
241
247
  /** Register / unregister the Monaco editor instance (called by RawEditor) */
242
248
  setMonacoEditor: (editor: MonacoEditor | null) => void;
243
- /** Set the color theme */
244
- setTheme: (theme: EditorTheme) => void;
249
+ /** Set the light/dark chrome color scheme for the editor shell. */
250
+ setColorScheme: (colorScheme: EditorColorScheme) => void;
245
251
  /** Show or hide the inline preview gutter at runtime (driven by the View menu). */
246
252
  setInlinePreviewVisible: (visible: boolean) => void;
247
253
  /** Show or hide the bottom status bar at runtime (driven by the View menu). */
@@ -360,8 +366,8 @@ export interface EditorProviderProps {
360
366
  initialView?: EditorView;
361
367
  /** Article ID used when generating the Doc */
362
368
  articleId?: string;
363
- /** Color theme */
364
- theme?: EditorTheme;
369
+ /** Light/dark chrome color scheme for the editor shell. */
370
+ colorScheme?: EditorColorScheme;
365
371
  /**
366
372
  * Workspace-scoped `ContentContainer` for this document — the folder
367
373
  * holding the doc, its `_files/` sidecar, sibling documents, and any
@@ -509,7 +515,7 @@ export function EditorProvider({
509
515
  initialMarkdown = '',
510
516
  initialView = 'raw',
511
517
  articleId = 'untitled',
512
- theme: initialTheme = 'light',
518
+ colorScheme: initialColorScheme = 'light',
513
519
  workspaceContainer = null,
514
520
  allowVersioning = false,
515
521
  versionBasename,
@@ -570,7 +576,7 @@ export function EditorProvider({
570
576
  );
571
577
  const [parseError, setParseError] = useState<string | null>(null);
572
578
  const [isParsing, setIsParsing] = useState(false);
573
- const [theme, setTheme] = useState<EditorTheme>(initialTheme);
579
+ const [colorScheme, setColorScheme] = useState<EditorColorScheme>(initialColorScheme);
574
580
  const [inlinePreviewVisible, setInlinePreviewVisibleRaw] =
575
581
  useState<boolean>(effectiveInlinePreview);
576
582
  // Sync visibility when the host changes the prop (e.g., toggle from outside).
@@ -731,10 +737,10 @@ export function EditorProvider({
731
737
  const articleIdRef = useRef(articleId);
732
738
  articleIdRef.current = articleId;
733
739
 
734
- // Sync theme when prop changes
740
+ // Sync color scheme when prop changes
735
741
  useEffect(() => {
736
- setTheme(initialTheme);
737
- }, [initialTheme]);
742
+ setColorScheme(initialColorScheme);
743
+ }, [initialColorScheme]);
738
744
 
739
745
  // Debounced parse on markdown source change
740
746
  const parseTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -971,7 +977,7 @@ export function EditorProvider({
971
977
  activeView,
972
978
  parseError,
973
979
  isParsing,
974
- theme,
980
+ colorScheme,
975
981
  editorMode,
976
982
  language: resolvedLanguage,
977
983
  inlinePreviewVisible,
@@ -1009,7 +1015,7 @@ export function EditorProvider({
1009
1015
  setActiveView,
1010
1016
  setTiptapEditor,
1011
1017
  setMonacoEditor,
1012
- setTheme,
1018
+ setColorScheme,
1013
1019
  setInlinePreviewVisible,
1014
1020
  setStatusBarVisible,
1015
1021
  setOutlineVisible,
@@ -1028,7 +1034,7 @@ export function EditorProvider({
1028
1034
  activeView,
1029
1035
  parseError,
1030
1036
  isParsing,
1031
- theme,
1037
+ colorScheme,
1032
1038
  editorMode,
1033
1039
  resolvedLanguage,
1034
1040
  inlinePreviewVisible,
@@ -1063,7 +1069,7 @@ export function EditorProvider({
1063
1069
  setActiveView,
1064
1070
  setTiptapEditor,
1065
1071
  setMonacoEditor,
1066
- setTheme,
1072
+ setColorScheme,
1067
1073
  setInlinePreviewVisible,
1068
1074
  setStatusBarVisible,
1069
1075
  setOutlineVisible,
@@ -23,7 +23,9 @@ import { RawEditor } from './RawEditor';
23
23
  import { WysiwygEditor } from './WysiwygEditor';
24
24
  import { InlinePreviewGutter } from './InlinePreviewGutter';
25
25
  import { BlockPreviewPanel } from './BlockPreviewPanel';
26
- import { OutlinePanel } from './OutlinePanel';
26
+ import { OutlinePanel, OUTLINE_RESPONSIVE_WIDTH } from './OutlinePanel';
27
+ import { CodeContextZones } from './codeContext/CodeContextZones';
28
+ import type { CodeContext } from './codeContext/types';
27
29
  import { BlockCardView } from './BlockCardView';
28
30
  import { TimelineTrack } from './TimelineTrack';
29
31
  import { PreviewPanel } from './PreviewPanel';
@@ -45,6 +47,10 @@ import {
45
47
  processTextFile,
46
48
  processTextFiles,
47
49
  } from './utils/dropUtils';
50
+ import {
51
+ collectMediaReferencesFromMarkdown,
52
+ removeMediaReferencesFromMarkdown,
53
+ } from './mediaReferences';
48
54
  import type { MediaProvider, Theme } from '@bendyline/squisq/schemas';
49
55
  import { DARK_SURFACE, LIGHT_SURFACE } from '@bendyline/squisq/schemas';
50
56
  import type { ContentContainer } from '@bendyline/squisq/storage';
@@ -56,7 +62,7 @@ import {
56
62
  import type { PrunePolicy, SaveVersionResult } from '@bendyline/squisq/versions';
57
63
  import type { CSSProperties, ReactNode } from 'react';
58
64
 
59
- export type { EditorTheme } from './EditorContext';
65
+ export type { EditorColorScheme } from './EditorContext';
60
66
 
61
67
  export interface EditorShellProps {
62
68
  /** Initial markdown content */
@@ -70,8 +76,13 @@ export interface EditorShellProps {
70
76
  basePath?: string;
71
77
  /** Called when markdown source changes */
72
78
  onChange?: (source: string) => void;
73
- /** Color theme: 'light' or 'dark' (default: 'light') */
74
- theme?: 'light' | 'dark';
79
+ /**
80
+ * Light/dark chrome color scheme for the editor shell — toolbar, tabs,
81
+ * status bar, and side panes (default: `'light'`). This is the editor's
82
+ * UI mode, **not** a Squisq `Theme` object; the rendered document's
83
+ * styling is controlled separately via `themeOverride` / `Doc.themeId`.
84
+ */
85
+ colorScheme?: 'light' | 'dark';
75
86
  /** Additional class name */
76
87
  className?: string;
77
88
  /** CSS height for the shell container (default: '100vh') */
@@ -166,6 +177,13 @@ export interface EditorShellProps {
166
177
  * (Slack, Discord). When omitted, the editor behaves normally.
167
178
  */
168
179
  submitOnEnter?: () => void;
180
+ /**
181
+ * Host-supplied context dictionary rendered inside the Monaco (raw / code)
182
+ * surface: collapsible markdown sections injected above anchor lines, plus
183
+ * an optional file-top summary. See {@link CodeContext}. Ignored in
184
+ * WYSIWYG / preview / image surfaces.
185
+ */
186
+ codeContext?: CodeContext;
169
187
  /**
170
188
  * Let the WYSIWYG editing surface fill its container instead of rendering
171
189
  * as a centered 800px "page" column. Useful when embedding in chat
@@ -324,9 +342,11 @@ export interface EditorShellProps {
324
342
  */
325
343
  outline?: boolean;
326
344
  /**
327
- * Width in pixels for the outline pane. Defaults to 240. Only takes
328
- * effect when {@link EditorShellProps.outline} is true (or the View
329
- * menu has toggled it on).
345
+ * Fixed width in pixels for the outline pane. When omitted, the pane sizes
346
+ * responsively never narrower than 260px, growing with the window and
347
+ * capped at 460px — so it stretches out when there's horizontal space to
348
+ * spare. Only takes effect when {@link EditorShellProps.outline} is true (or
349
+ * the View menu has toggled it on).
330
350
  */
331
351
  outlineWidth?: number;
332
352
  /**
@@ -378,7 +398,7 @@ export function EditorShell({
378
398
  articleId = 'untitled',
379
399
  basePath = '/',
380
400
  onChange,
381
- theme = 'light',
401
+ colorScheme = 'light',
382
402
  className,
383
403
  height = '100vh',
384
404
  minHeight,
@@ -397,6 +417,7 @@ export function EditorShell({
397
417
  toolbarSlotRight,
398
418
  showPlayTab = true,
399
419
  submitOnEnter,
420
+ codeContext,
400
421
  fullWidth = false,
401
422
  uxFont,
402
423
  thinMargins = false,
@@ -417,7 +438,7 @@ export function EditorShell({
417
438
  inlinePreview = false,
418
439
  inlinePreviewWidth = 320,
419
440
  outline = false,
420
- outlineWidth = 240,
441
+ outlineWidth,
421
442
  blockTags = true,
422
443
  themeInheritance = 'fonts',
423
444
  viewPreferences,
@@ -449,7 +470,7 @@ export function EditorShell({
449
470
  initialMarkdown={initialMarkdown}
450
471
  initialView={effectiveInitialView}
451
472
  articleId={articleId}
452
- theme={theme}
473
+ colorScheme={colorScheme}
453
474
  workspaceContainer={effectiveContainer}
454
475
  allowVersioning={allowVersioning}
455
476
  versionBasename={versionBasename}
@@ -487,6 +508,7 @@ export function EditorShell({
487
508
  toolbarSlotRight={toolbarSlotRight}
488
509
  showPlayTab={showPlayTab}
489
510
  submitOnEnter={submitOnEnter}
511
+ codeContext={codeContext}
490
512
  fullWidth={fullWidth}
491
513
  uxFont={uxFont}
492
514
  thinMargins={thinMargins}
@@ -522,6 +544,7 @@ interface EditorShellInnerProps {
522
544
  toolbarSlotRight?: ReactNode;
523
545
  showPlayTab: boolean;
524
546
  submitOnEnter?: () => void;
547
+ codeContext?: CodeContext;
525
548
  fullWidth: boolean;
526
549
  uxFont?: string;
527
550
  thinMargins: boolean;
@@ -534,7 +557,7 @@ interface EditorShellInnerProps {
534
557
  allowVersioning: boolean;
535
558
  versioningAutoSaveIdleMs?: number;
536
559
  inlinePreviewWidth: number;
537
- outlineWidth: number;
560
+ outlineWidth?: number;
538
561
  themeOverride: Theme | null;
539
562
  }
540
563
 
@@ -554,6 +577,7 @@ function EditorShellInner({
554
577
  toolbarSlotRight,
555
578
  showPlayTab,
556
579
  submitOnEnter,
580
+ codeContext,
557
581
  fullWidth,
558
582
  uxFont,
559
583
  thinMargins,
@@ -573,7 +597,7 @@ function EditorShellInner({
573
597
  activeView,
574
598
  markdownSource,
575
599
  doc,
576
- theme,
600
+ colorScheme,
577
601
  editorMode,
578
602
  insertAtCursor,
579
603
  replaceAll,
@@ -592,6 +616,7 @@ function EditorShellInner({
592
616
  imageEditTarget,
593
617
  closeImageEdit,
594
618
  bumpMediaRevision,
619
+ mediaRevision,
595
620
  } = useEditorContext();
596
621
  // Dual-catalog custom themes (doc + browser library), mirroring how
597
622
  // CustomTemplateProvider is fed. Wraps the preview subtree so the theme
@@ -610,6 +635,12 @@ function EditorShellInner({
610
635
  const isTimelineMode = isMarkdownMode && layoutMode === 'timeline' && !isPreview;
611
636
  const [showFiles, setShowFiles] = useState(false);
612
637
  const [mediaRefreshKey, setMediaRefreshKey] = useState(0);
638
+ const [mediaCount, setMediaCount] = useState(0);
639
+ const mediaListRefreshKey = mediaRefreshKey + mediaRevision;
640
+ const usedMediaPaths = useMemo(
641
+ () => collectMediaReferencesFromMarkdown(markdownSource),
642
+ [markdownSource],
643
+ );
613
644
  // Persistent fallback container for image-edit sidecars when the host
614
645
  // didn't supply one. Lifted to shell scope so opening the same image
615
646
  // multiple times sees the same `.imageEdits/<sanitized>/.versions/...`
@@ -620,7 +651,34 @@ function EditorShellInner({
620
651
  imageEditFallbackContainerRef.current = new MemoryContentContainer();
621
652
  }
622
653
  const imageEditFallbackContainer = imageEditFallbackContainerRef.current;
623
- const isDark = theme === 'dark';
654
+ const isDark = colorScheme === 'dark';
655
+
656
+ useEffect(() => {
657
+ if (!mediaProvider) {
658
+ setMediaCount(0);
659
+ return;
660
+ }
661
+
662
+ let cancelled = false;
663
+ mediaProvider.listMedia().then(
664
+ (entries) => {
665
+ if (!cancelled) setMediaCount(entries.length);
666
+ },
667
+ (err: unknown) => {
668
+ if (!cancelled) {
669
+ setMediaCount(0);
670
+ console.warn(
671
+ '[squisq-editor] Failed to read media list:',
672
+ err instanceof Error ? err.message : err,
673
+ );
674
+ }
675
+ },
676
+ );
677
+
678
+ return () => {
679
+ cancelled = true;
680
+ };
681
+ }, [mediaProvider, mediaListRefreshKey]);
624
682
 
625
683
  const handleToggleFiles = useCallback(() => {
626
684
  setShowFiles((prev) => !prev);
@@ -679,6 +737,24 @@ function EditorShellInner({
679
737
  [activeView, tiptapEditor, monacoEditor, insertAtCursor, markdownSource, setMarkdownSource],
680
738
  );
681
739
 
740
+ const handleMediaUploaded = useCallback(
741
+ (relativePath: string, name: string, mimeType: string) => {
742
+ insertMediaRef(relativePath, name, mimeType);
743
+ setMediaRefreshKey((k) => k + 1);
744
+ },
745
+ [insertMediaRef],
746
+ );
747
+
748
+ const handleMediaRemoved = useCallback(
749
+ (relativePath: string) => {
750
+ const nextSource = removeMediaReferencesFromMarkdown(markdownSource, relativePath);
751
+ if (nextSource !== markdownSource) replaceAll(nextSource);
752
+ setMediaRefreshKey((k) => k + 1);
753
+ bumpMediaRevision();
754
+ },
755
+ [bumpMediaRevision, markdownSource, replaceAll],
756
+ );
757
+
682
758
  const handleFileDrop = useCallback(
683
759
  async (files: File[], target: DropTarget) => {
684
760
  try {
@@ -761,7 +837,7 @@ function EditorShellInner({
761
837
  return (
762
838
  <div
763
839
  className={`squisq-editor-shell ${className || ''}`}
764
- data-theme={theme}
840
+ data-theme={colorScheme}
765
841
  data-full-width={fullWidth ? 'true' : undefined}
766
842
  data-thin-margins={thinMargins ? 'true' : undefined}
767
843
  data-outline-visible={isMarkdownMode && outlineVisible ? 'true' : undefined}
@@ -775,13 +851,16 @@ function EditorShellInner({
775
851
  // tabs, status bar) consume `--squisq-ux-font` as their
776
852
  // `font-family`, falling back to the system stack when unset.
777
853
  ...(uxFont ? ({ '--squisq-ux-font': uxFont } as CSSProperties) : {}),
778
- // Exposed so the toolbar's view-tabs section can match the outline
779
- // pane's width, lining up its right-edge separator with the
780
- // outline's right edge. The variable is set unconditionally so the
781
- // outline pane itself can also read it if needed; the
782
- // `data-outline-visible` gate above keeps the toolbar override
783
- // scoped to the case where alignment matters.
784
- ...({ '--squisq-outline-width': `${outlineWidth}px` } as CSSProperties),
854
+ // Exposed so the toolbar's view-tabs section AND the outline pane read
855
+ // one shared width, lining up the view-tabs' right-edge separator with
856
+ // the outline's right edge. A fixed `outlineWidth` pins it to px;
857
+ // otherwise it's the responsive clamp so the pane stretches when
858
+ // there's room. The `data-outline-visible` gate above keeps the
859
+ // toolbar override scoped to the case where alignment matters.
860
+ ...({
861
+ '--squisq-outline-width':
862
+ outlineWidth != null ? `${outlineWidth}px` : OUTLINE_RESPONSIVE_WIDTH,
863
+ } as CSSProperties),
785
864
  }}
786
865
  {...containerProps}
787
866
  >
@@ -802,14 +881,11 @@ function EditorShellInner({
802
881
  <div className="squisq-editor-header">
803
882
  <Toolbar
804
883
  showFiles={showFiles}
884
+ fileCount={mediaCount}
805
885
  onToggleFiles={!isCodeMode && filesToggleEnabled ? handleToggleFiles : undefined}
806
886
  slotLeft={toolbarSlotLeft}
807
- slotAfterActions={
808
- <>
809
- {toolbarSlotAfterActions}
810
- {!isCodeMode && isPreview && <PreviewToolbarControls />}
811
- </>
812
- }
887
+ slotAfterTabs={!isCodeMode && isPreview && <PreviewToolbarControls />}
888
+ slotAfterActions={toolbarSlotAfterActions}
813
889
  slotRight={toolbarSlotRight}
814
890
  showPlayTab={showPlayTab}
815
891
  />
@@ -845,9 +921,10 @@ function EditorShellInner({
845
921
  allowVersioning={allowVersioning}
846
922
  versioningAutoSaveIdleMs={versioningAutoSaveIdleMs}
847
923
  onExport={onImageExport}
924
+ surface={colorScheme === 'dark' ? DARK_SURFACE : LIGHT_SURFACE}
848
925
  />
849
926
  ) : (
850
- <ImageViewer src={imageSrc} alt={imageAlt} theme={theme} />
927
+ <ImageViewer src={imageSrc} alt={imageAlt} theme={colorScheme} />
851
928
  ))}
852
929
  {/* Raw (Monaco) view. Always wrapped in `.squisq-editor-with-gutter`
853
930
  so toggling a pane on/off doesn't change the editor's tree
@@ -870,7 +947,7 @@ function EditorShellInner({
870
947
  >
871
948
  <div key="raw-editor" className="squisq-raw-editor-container">
872
949
  <RawEditor
873
- theme={theme === 'dark' ? 'vs-dark' : 'vs'}
950
+ monacoTheme={colorScheme === 'dark' ? 'vs-dark' : 'vs'}
874
951
  submitOnEnter={submitOnEnter}
875
952
  readOnly={readOnly}
876
953
  />
@@ -879,12 +956,18 @@ function EditorShellInner({
879
956
  ) : (
880
957
  <div key="raw-editor" className="squisq-raw-editor-container">
881
958
  <RawEditor
882
- theme={theme === 'dark' ? 'vs-dark' : 'vs'}
959
+ monacoTheme={colorScheme === 'dark' ? 'vs-dark' : 'vs'}
883
960
  submitOnEnter={submitOnEnter}
884
961
  readOnly={readOnly}
885
962
  />
886
963
  </div>
887
964
  )}
965
+ {/* Renders nothing in normal flow — portals context
966
+ sections into Monaco view zones via the context's
967
+ monacoEditor. Code mode only. */}
968
+ {isCodeMode && codeContext && (
969
+ <CodeContextZones key="code-context" options={codeContext} />
970
+ )}
888
971
  {isMarkdownMode && isCardMode && inlinePreviewVisible && (
889
972
  <BlockPreviewPanel key="block-preview" basePath={basePath} />
890
973
  )}
@@ -952,8 +1035,11 @@ function EditorShellInner({
952
1035
  <MediaBin
953
1036
  mediaProvider={mediaProvider}
954
1037
  isDark={isDark}
955
- refreshKey={mediaRefreshKey}
956
- onMediaUploaded={insertMediaRef}
1038
+ refreshKey={mediaListRefreshKey}
1039
+ usedMediaPaths={usedMediaPaths}
1040
+ onMediaUploaded={handleMediaUploaded}
1041
+ onMediaRemoved={handleMediaRemoved}
1042
+ onCountChange={setMediaCount}
957
1043
  />
958
1044
  )}
959
1045
 
@@ -1006,7 +1092,7 @@ function EditorShellInner({
1006
1092
  }}
1007
1093
  allowVersioning={allowVersioning}
1008
1094
  versioningAutoSaveIdleMs={versioningAutoSaveIdleMs}
1009
- shellTheme={theme}
1095
+ shellTheme={colorScheme}
1010
1096
  />
1011
1097
  )}
1012
1098
  </div>
@@ -1031,8 +1117,8 @@ interface ImageEditModalProps {
1031
1117
  onSaved: () => void;
1032
1118
  allowVersioning: boolean;
1033
1119
  versioningAutoSaveIdleMs?: number;
1034
- /** EditorShell's `theme` prop — 'light' or 'dark'. Threaded through so
1035
- * the image editor chrome matches the host shell. */
1120
+ /** EditorShell's `colorScheme` prop — 'light' or 'dark'. Threaded through
1121
+ * so the image editor chrome matches the host shell. */
1036
1122
  shellTheme?: 'light' | 'dark';
1037
1123
  }
1038
1124