@stephen-netu/design-system 0.2.2 → 0.5.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 (229) hide show
  1. package/dist/d0-data-viz/BarChart.test.js +53 -0
  2. package/dist/d0-data-viz/CheckpointBar.test.js +83 -0
  3. package/dist/d0-data-viz/CheckpointPill.svelte +1 -1
  4. package/dist/d0-data-viz/CheckpointPill.test.js +78 -0
  5. package/dist/d0-data-viz/ConnectionHealth.test.js +70 -0
  6. package/dist/d0-data-viz/MetricCard.test.js +57 -0
  7. package/dist/d0-data-viz/SparkLine.test.js +47 -0
  8. package/dist/d0-data-viz/StorageMap.test.js +63 -0
  9. package/dist/d0-data-viz/Timeline.test.js +62 -0
  10. package/dist/d0-data-viz/WorldLineTimeline.test.js +99 -0
  11. package/dist/d0-data-viz/checkpoint-types.js +1 -1
  12. package/dist/d0-data-viz/storage-map-types.d.ts.map +1 -1
  13. package/dist/d0-data-viz/storage-map-types.js +6 -3
  14. package/dist/d0-data-viz/theme-integration.js +1 -1
  15. package/dist/demos/AmbientPrimitivesDemo.svelte +103 -0
  16. package/dist/demos/AmbientPrimitivesDemo.svelte.d.ts +5 -0
  17. package/dist/demos/AmbientPrimitivesDemo.svelte.d.ts.map +1 -0
  18. package/dist/demos/NodepadBlocksShowcase.svelte +2 -1
  19. package/dist/demos/NodepadBlocksShowcase.svelte.d.ts.map +1 -1
  20. package/dist/demos/index.d.ts +1 -0
  21. package/dist/demos/index.d.ts.map +1 -1
  22. package/dist/demos/index.js +1 -0
  23. package/dist/editor/CodeEditor.svelte +83 -26
  24. package/dist/editor/CodeEditor.svelte.d.ts +0 -1
  25. package/dist/editor/CodeEditor.svelte.d.ts.map +1 -1
  26. package/dist/editor/ExternalChangeDialog.svelte +117 -0
  27. package/dist/editor/ExternalChangeDialog.svelte.d.ts +13 -0
  28. package/dist/editor/ExternalChangeDialog.svelte.d.ts.map +1 -0
  29. package/dist/editor/MarkdownEditor.svelte +104 -617
  30. package/dist/editor/MarkdownEditor.svelte.d.ts +1 -1
  31. package/dist/editor/MarkdownEditor.svelte.d.ts.map +1 -1
  32. package/dist/editor/codemirror-config.d.ts +36 -0
  33. package/dist/editor/codemirror-config.d.ts.map +1 -0
  34. package/dist/editor/codemirror-config.js +87 -0
  35. package/dist/editor/components/CorrectionTooltip.svelte +180 -159
  36. package/dist/editor/components/CorrectionTooltip.svelte.d.ts +7 -23
  37. package/dist/editor/components/CorrectionTooltip.svelte.d.ts.map +1 -1
  38. package/dist/editor/extensions/autocomplete.d.ts +2 -2
  39. package/dist/editor/extensions/autocomplete.d.ts.map +1 -1
  40. package/dist/editor/extensions/autocomplete.test.js +121 -0
  41. package/dist/editor/extensions/spellcheck.d.ts +21 -3
  42. package/dist/editor/extensions/spellcheck.d.ts.map +1 -1
  43. package/dist/editor/extensions/spellcheck.js +51 -9
  44. package/dist/editor/extensions/spellcheck.test.js +168 -0
  45. package/dist/editor/extensions/types.d.ts +2 -1
  46. package/dist/editor/extensions/types.d.ts.map +1 -1
  47. package/dist/editor/index.d.ts +2 -1
  48. package/dist/editor/index.d.ts.map +1 -1
  49. package/dist/editor/index.js +1 -0
  50. package/dist/editor/markdown-preview.d.ts +9 -0
  51. package/dist/editor/markdown-preview.d.ts.map +1 -0
  52. package/dist/editor/markdown-preview.js +19 -0
  53. package/dist/editor/markdown-preview.test.js +38 -0
  54. package/dist/editor/useCodeMirror.d.ts +23 -0
  55. package/dist/editor/useCodeMirror.d.ts.map +1 -0
  56. package/dist/editor/useCodeMirror.js +40 -0
  57. package/dist/editor/useFilePersistence.d.ts +32 -0
  58. package/dist/editor/useFilePersistence.d.ts.map +1 -0
  59. package/dist/editor/useFilePersistence.js +117 -0
  60. package/dist/fabric/ai/ChatPanel.svelte +61 -62
  61. package/dist/fabric/ai/ChatPanel.svelte.d.ts.map +1 -1
  62. package/dist/fabric/ai/ResearchPanel.svelte +31 -24
  63. package/dist/fabric/data/FileTree.svelte +195 -0
  64. package/dist/fabric/data/FileTree.svelte.d.ts +19 -0
  65. package/dist/fabric/data/FileTree.svelte.d.ts.map +1 -0
  66. package/dist/fabric/data/FileTree.test.js +127 -0
  67. package/dist/fabric/data/index.d.ts +5 -9
  68. package/dist/fabric/data/index.d.ts.map +1 -1
  69. package/dist/fabric/data/index.js +4 -1
  70. package/dist/fabric/index.d.ts +1 -0
  71. package/dist/fabric/index.d.ts.map +1 -1
  72. package/dist/fabric/index.js +2 -0
  73. package/dist/fabric/input/KeyboardShortcuts.svelte +23 -21
  74. package/dist/fabric/layout/CollapsiblePanel.svelte +8 -10
  75. package/dist/fabric/navigation/ActivityRail.svelte +3 -5
  76. package/dist/fabric/navigation/ActivityRail.svelte.d.ts.map +1 -1
  77. package/dist/g0-generative/components/Text.svelte +38 -0
  78. package/dist/g0-generative/components/Text.svelte.d.ts +9 -0
  79. package/dist/g0-generative/components/Text.svelte.d.ts.map +1 -0
  80. package/dist/g0-generative/index.d.ts +1 -0
  81. package/dist/g0-generative/index.d.ts.map +1 -1
  82. package/dist/g0-generative/index.js +1 -0
  83. package/dist/g0-generative/library/ComponentLibrary.d.ts +4 -0
  84. package/dist/g0-generative/library/ComponentLibrary.d.ts.map +1 -1
  85. package/dist/g0-generative/library/ComponentLibrary.js +10 -0
  86. package/dist/g0-generative/library/ComponentLibrary.test.js +135 -0
  87. package/dist/g0-generative/renderer/OpenUIRenderer.svelte +16 -56
  88. package/dist/g0-generative/renderer/OpenUIRenderer.svelte.d.ts.map +1 -1
  89. package/dist/g0-generative/renderer/OpenUIRenderer.test.js +143 -0
  90. package/dist/g0-geo/SovereignMap.svelte +13 -13
  91. package/dist/g0-geo/SovereignMap.svelte.d.ts.map +1 -1
  92. package/dist/index.d.ts +0 -1
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +3 -1
  95. package/dist/l0-layout/block-writer/BlockContent.svelte +23 -22
  96. package/dist/l0-layout/block-writer/BlockContent.svelte.d.ts.map +1 -1
  97. package/dist/l0-layout/block-writer/BlockContent.test.js +47 -0
  98. package/dist/l0-layout/block-writer/markdown.js +11 -11
  99. package/dist/l0-layout/block-writer/markdown.test.js +81 -0
  100. package/dist/l0-layout/card/StackedCard.svelte +87 -0
  101. package/dist/l0-layout/card/StackedCard.svelte.d.ts +9 -0
  102. package/dist/l0-layout/card/StackedCard.svelte.d.ts.map +1 -0
  103. package/dist/l0-layout/card/StackedCard.test.js +29 -0
  104. package/dist/l0-layout/index.d.ts +1 -0
  105. package/dist/l0-layout/index.d.ts.map +1 -1
  106. package/dist/l0-layout/index.js +1 -0
  107. package/dist/l0-layout/kanban/KanbanBoard.svelte +1 -1
  108. package/dist/l0-layout/kanban/KanbanColumn.svelte +1 -1
  109. package/dist/n1-flow-canvas/FlowCanvas.svelte +50 -6
  110. package/dist/n1-flow-canvas/FlowCanvas.svelte.d.ts.map +1 -1
  111. package/dist/n1-flow-canvas/index.d.ts +2 -0
  112. package/dist/n1-flow-canvas/index.d.ts.map +1 -1
  113. package/dist/n1-flow-canvas/index.js +1 -0
  114. package/dist/n1-flow-canvas/pipeline-adapter.d.ts +37 -0
  115. package/dist/n1-flow-canvas/pipeline-adapter.d.ts.map +1 -0
  116. package/dist/n1-flow-canvas/pipeline-adapter.js +160 -0
  117. package/dist/n2-manifold-disk/ManifoldDisk.svelte +62 -35
  118. package/dist/n2-manifold-disk/ManifoldDisk.svelte.d.ts.map +1 -1
  119. package/dist/n4-adaptive-manifold/AdaptiveManifold.svelte +23 -19
  120. package/dist/n4-adaptive-manifold/AdaptiveManifold.svelte.d.ts.map +1 -1
  121. package/dist/n4-adaptive-manifold/types.d.ts +3 -0
  122. package/dist/n4-adaptive-manifold/types.d.ts.map +1 -1
  123. package/dist/p0-primitives/ThemeProvider.svelte +28 -7
  124. package/dist/p0-primitives/ThemeProvider.svelte.d.ts +5 -0
  125. package/dist/p0-primitives/ThemeProvider.svelte.d.ts.map +1 -1
  126. package/dist/p0-primitives/canvas-theme.d.ts +21 -1
  127. package/dist/p0-primitives/canvas-theme.d.ts.map +1 -1
  128. package/dist/p0-primitives/canvas-theme.js +78 -23
  129. package/dist/p0-primitives/debug.d.ts +6 -0
  130. package/dist/p0-primitives/debug.d.ts.map +1 -0
  131. package/dist/p0-primitives/debug.js +21 -0
  132. package/dist/p0-primitives/theme.d.ts.map +1 -1
  133. package/dist/p0-primitives/theme.js +11 -31
  134. package/dist/p0-primitives/tokens/ambient.css +76 -0
  135. package/dist/p0-primitives/tokens/tokens.css +124 -29
  136. package/dist/p0-primitives/utils/index.d.ts +2 -0
  137. package/dist/p0-primitives/utils/index.d.ts.map +1 -1
  138. package/dist/p0-primitives/utils/index.js +1 -0
  139. package/dist/p0-primitives/utils/reveal.d.ts +15 -0
  140. package/dist/p0-primitives/utils/reveal.d.ts.map +1 -0
  141. package/dist/p0-primitives/utils/reveal.js +29 -0
  142. package/dist/p0-primitives/utils/reveal.test.js +95 -0
  143. package/dist/s0-state/SyncStatus.svelte +1 -1
  144. package/dist/s0-state/index.d.ts +2 -1
  145. package/dist/s0-state/index.d.ts.map +1 -1
  146. package/dist/s0-state/index.js +2 -0
  147. package/dist/s0-state/modal-stack-store.svelte.d.ts +3 -3
  148. package/dist/s0-state/modal-stack-store.svelte.js +2 -2
  149. package/dist/spatial/ForceSimulation.svelte.js +1 -1
  150. package/dist/spatial/ParticleAssembleMark.svelte +285 -0
  151. package/dist/spatial/ParticleAssembleMark.svelte.d.ts +24 -0
  152. package/dist/spatial/ParticleAssembleMark.svelte.d.ts.map +1 -0
  153. package/dist/spatial/ZoneTiler.test.js +90 -0
  154. package/dist/spatial/index.js +1 -0
  155. package/dist/spatial/particle-assemble-math.d.ts +22 -0
  156. package/dist/spatial/particle-assemble-math.d.ts.map +1 -0
  157. package/dist/spatial/particle-assemble-math.js +26 -0
  158. package/dist/spatial/particle-assemble-math.test.js +56 -0
  159. package/dist/t0-transport/substrate.svelte.d.ts.map +1 -1
  160. package/dist/t0-transport/substrate.svelte.js +10 -4
  161. package/dist/u0-primitives/LockIndicator.svelte +16 -14
  162. package/dist/u0-primitives/VerticalToolbar.svelte +24 -21
  163. package/dist/u0-primitives/accordion/Accordion.svelte +8 -7
  164. package/dist/u0-primitives/animated-icon/AnimatedIcon.svelte +3 -3
  165. package/dist/u0-primitives/animated-icon/AnimatedIcon.test.js +82 -0
  166. package/dist/u0-primitives/avatar/Avatar.svelte +14 -14
  167. package/dist/u0-primitives/avatar/Avatar.test.js +81 -0
  168. package/dist/u0-primitives/badge/Badge.svelte +6 -4
  169. package/dist/u0-primitives/badge/ValidityBadge.svelte +4 -27
  170. package/dist/u0-primitives/button/Button.svelte +15 -12
  171. package/dist/u0-primitives/card/Card.svelte +37 -15
  172. package/dist/u0-primitives/card/Card.test.js +86 -0
  173. package/dist/u0-primitives/command-palette/CommandPalette.svelte +48 -43
  174. package/dist/u0-primitives/command-palette/CommandPalette.test.js +202 -0
  175. package/dist/u0-primitives/dropdown/Dropdown.svelte +20 -25
  176. package/dist/u0-primitives/grant-badge/GrantStatusBadge.svelte +2 -2
  177. package/dist/u0-primitives/index.d.ts +6 -0
  178. package/dist/u0-primitives/index.d.ts.map +1 -1
  179. package/dist/u0-primitives/index.js +8 -0
  180. package/dist/u0-primitives/input/Input.svelte +77 -24
  181. package/dist/u0-primitives/input/Input.svelte.d.ts +2 -10
  182. package/dist/u0-primitives/input/Input.svelte.d.ts.map +1 -1
  183. package/dist/u0-primitives/input/Input.test.js +81 -1
  184. package/dist/u0-primitives/live-dot/LiveDot.svelte +56 -0
  185. package/dist/u0-primitives/live-dot/LiveDot.svelte.d.ts +11 -0
  186. package/dist/u0-primitives/live-dot/LiveDot.svelte.d.ts.map +1 -0
  187. package/dist/u0-primitives/live-dot/LiveDot.test.js +41 -0
  188. package/dist/u0-primitives/live-dot/index.d.ts +3 -0
  189. package/dist/u0-primitives/live-dot/index.d.ts.map +1 -0
  190. package/dist/u0-primitives/live-dot/index.js +1 -0
  191. package/dist/u0-primitives/modal/Modal.svelte +22 -19
  192. package/dist/u0-primitives/modal/Modal.test.js +1 -1
  193. package/dist/u0-primitives/spinner/Spinner.svelte +11 -79
  194. package/dist/u0-primitives/spinner/Spinner.svelte.d.ts.map +1 -1
  195. package/dist/u0-primitives/steps/Steps.svelte +18 -19
  196. package/dist/u0-primitives/surface-state/SurfaceState.svelte +7 -7
  197. package/dist/u0-primitives/tabs/Tabs.svelte +13 -11
  198. package/dist/u0-primitives/theme-swatch-group/ThemeSwatchGroup.svelte +94 -0
  199. package/dist/u0-primitives/theme-swatch-group/ThemeSwatchGroup.svelte.d.ts +16 -0
  200. package/dist/u0-primitives/theme-swatch-group/ThemeSwatchGroup.svelte.d.ts.map +1 -0
  201. package/dist/u0-primitives/theme-swatch-group/ThemeSwatchGroup.test.js +48 -0
  202. package/dist/u0-primitives/theme-swatch-group/index.d.ts +3 -0
  203. package/dist/u0-primitives/theme-swatch-group/index.d.ts.map +1 -0
  204. package/dist/u0-primitives/theme-swatch-group/index.js +1 -0
  205. package/dist/u0-primitives/toggle/Toggle.svelte +10 -11
  206. package/dist/u0-primitives/tooltip/Tooltip.svelte +9 -9
  207. package/dist/u0-primitives/web-components/button-component.d.ts +5 -1
  208. package/dist/u0-primitives/web-components/button-component.d.ts.map +1 -1
  209. package/dist/u0-primitives/web-components/button-component.js +72 -33
  210. package/dist/u0-primitives/web-components/index.d.ts +3 -0
  211. package/dist/u0-primitives/web-components/index.d.ts.map +1 -0
  212. package/dist/u0-primitives/web-components/index.js +2 -0
  213. package/dist/u0-primitives/web-components/input-component.d.ts +5 -1
  214. package/dist/u0-primitives/web-components/input-component.d.ts.map +1 -1
  215. package/dist/u0-primitives/web-components/input-component.js +56 -39
  216. package/dist/u0-primitives/web-components/web-components.test.js +96 -0
  217. package/dist/u0-primitives/window-frame/WindowFrame.svelte +87 -0
  218. package/dist/u0-primitives/window-frame/WindowFrame.svelte.d.ts +10 -0
  219. package/dist/u0-primitives/window-frame/WindowFrame.svelte.d.ts.map +1 -0
  220. package/dist/u0-primitives/window-frame/WindowFrame.test.js +31 -0
  221. package/dist/u0-primitives/window-frame/index.d.ts +3 -0
  222. package/dist/u0-primitives/window-frame/index.d.ts.map +1 -0
  223. package/dist/u0-primitives/window-frame/index.js +1 -0
  224. package/dist/x0-enchanted-blocks/components/NodepadBlock.svelte +11 -2
  225. package/dist/x0-enchanted-blocks/components/NodepadBlock.svelte.d.ts.map +1 -1
  226. package/package.json +44 -28
  227. package/src/p0-primitives/tokens/ambient.css +76 -0
  228. package/src/p0-primitives/tokens/tokens.css +124 -29
  229. package/src/p0-primitives/tokens/tokens.json +42 -24
@@ -1,31 +1,15 @@
1
1
  <script lang="ts">
2
- /**
3
- * MarkdownEditor Component - Phase 2
4
- *
5
- * CodeMirror 6 integration with markdown support.
6
- * Includes external file change detection and find/replace.
7
- *
8
- * NOTE: This is a heavy stateful component (CodeMirror creates ~200+ DOM nodes).
9
- * Prefer {#if} over visibility:hidden when toggling to avoid mounting hidden
10
- * editor instances. Use visibility:hidden only for the outer chrome wrapper.
11
- */
12
-
13
- import { onMount, onDestroy, untrack } from 'svelte';
14
- import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightSpecialChars,
15
- drawSelection, dropCursor, rectangularSelection, crosshairCursor,
16
- highlightActiveLine, type ViewUpdate } from '@codemirror/view';
17
- import { EditorState, type Extension } from '@codemirror/state';
18
- import { markdown } from '@codemirror/lang-markdown';
19
- import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
20
- import { search } from '@codemirror/search';
21
- import { writeTextFile, readTextFile, getMtimeMs, isTauri } from '../t0-transport/fs';
2
+ import { untrack } from 'svelte';
3
+ import { EditorView } from '@codemirror/view';
22
4
  import FindReplaceDialog from './FindReplaceDialog.svelte';
23
- import { autoFormat } from './extensions/autoformat';
24
- import { expansion } from './extensions/expansion';
25
- import { autocomplete } from './extensions/autocomplete';
26
- import { spellcheck } from './extensions/spellcheck';
5
+ import CorrectionTooltip from './components/CorrectionTooltip.svelte';
6
+ import ExternalChangeDialog from './ExternalChangeDialog.svelte';
7
+ import DOMPurify from 'dompurify';
8
+ import { markdownToHtml } from './markdown-preview';
9
+ import { useCodeMirror } from './useCodeMirror';
10
+ import { useFilePersistence } from './useFilePersistence';
27
11
  import type { CompletionSource } from './extensions/autocomplete';
28
- import type { SpellCheckDictionary } from './extensions/spellcheck';
12
+ import type { SpellCheckCorrection, SpellCheckDictionary } from './extensions/spellcheck';
29
13
 
30
14
  interface Props {
31
15
  filePath: string | null;
@@ -33,656 +17,159 @@
33
17
  onChange?: (content: string) => void;
34
18
  onSave?: () => void;
35
19
  onClose?: () => void;
36
- onExternalChange?: (newContent: string) => void;
20
+ onExternalChange?: (content: string) => void;
37
21
  completionSource?: CompletionSource;
38
22
  spellCheckDictionary?: SpellCheckDictionary;
39
23
  }
40
24
 
41
- let { filePath, initialContent, onChange, onSave, onClose, onExternalChange, completionSource, spellCheckDictionary }: Props = $props();
25
+ interface ActiveCorrection extends SpellCheckCorrection { from: number; to: number; }
42
26
 
27
+ let { filePath, initialContent, onChange, onSave, onClose, onExternalChange, completionSource, spellCheckDictionary }: Props = $props();
43
28
  let content = $state(untrack(() => initialContent));
44
29
  let isDirty = $state(false);
45
30
  let isSaving = $state(false);
46
31
  let saveError = $state<string | null>(null);
47
32
  let showPreview = $state(false);
48
-
49
- // Auto-save
50
- let autoSaveTimeout: ReturnType<typeof setTimeout> | null = null;
51
-
52
- // Persistence backend (T0 transport). Static per session: without a Tauri
53
- // host there is no file backend, and the UI must say so instead of "Saved".
54
- const persistenceAvailable = isTauri();
55
-
56
- // External change detection
33
+ let showFindReplace = $state(false);
57
34
  let showExternalChangeDialog = $state(false);
58
- let externalChangeCheckInterval: ReturnType<typeof setInterval> | null = null;
59
- let isCheckingExternalChanges = $state(false);
60
- let lastKnownMtimeMs: number | null = null;
61
-
62
- // CodeMirror instance
35
+ let activeCorrection = $state<ActiveCorrection | null>(null);
63
36
  let editorContainer: HTMLDivElement;
64
37
  let editorView = $state<EditorView | null>(null);
38
+ let autoSaveTimeout: ReturnType<typeof setTimeout> | null = null;
39
+ let stopExternalChangeMonitoring: (() => void) | null = null;
40
+ let lastKnownMtimeMs = $state<number | null>(null);
41
+ let isCheckingExternalChanges = $state(false);
65
42
 
66
- // Find/Replace state
67
- let showFindReplace = $state(false);
68
-
69
- // Computed
70
- let fileName = $derived(filePath?.split('/').pop() || filePath || 'Untitled');
71
- let saveStatus = $derived(
72
- isSaving ? 'Saving...' :
73
- saveError ? 'Error!' :
74
- filePath && !persistenceAvailable ? 'Not persisted' :
75
- isDirty ? 'Unsaved' : 'Saved'
76
- );
43
+ const filePersistence = useFilePersistence({
44
+ getFilePath: () => filePath,
45
+ getContent: () => content,
46
+ getIsDirty: () => isDirty,
47
+ getIsSaving: () => isSaving,
48
+ getIsCheckingExternalChanges: () => isCheckingExternalChanges,
49
+ getShowExternalChangeDialog: () => showExternalChangeDialog,
50
+ getLastKnownMtimeMs: () => lastKnownMtimeMs,
51
+ getEditorView: () => editorView,
52
+ setContent: (value) => { content = value; },
53
+ setIsDirty: (value) => { isDirty = value; },
54
+ setIsSaving: (value) => { isSaving = value; },
55
+ setSaveError: (value) => { saveError = value; },
56
+ setShowExternalChangeDialog: (value) => { showExternalChangeDialog = value; },
57
+ setIsCheckingExternalChanges: (value) => { isCheckingExternalChanges = value; },
58
+ setLastKnownMtimeMs: (value) => { lastKnownMtimeMs = value; },
59
+ onChange: (value) => onChange?.(value),
60
+ onSave: () => onSave?.(),
61
+ onExternalChange: (value) => onExternalChange?.(value)
62
+ });
77
63
 
78
- // Custom keymap for save and find/replace
79
- const saveKeymap = keymap.of([{
80
- key: 'Ctrl-s',
81
- mac: 'Cmd-s',
82
- run: () => {
83
- saveContent();
84
- return true;
85
- }
86
- }, {
87
- key: 'Ctrl-f',
88
- mac: 'Cmd-f',
89
- run: () => {
90
- openFindReplace(false);
91
- return true;
92
- }
93
- }, {
94
- key: 'Ctrl-Shift-f',
95
- mac: 'Cmd-Shift-f',
96
- run: () => {
97
- openFindReplace(true);
98
- return true;
64
+ const codeMirror = useCodeMirror({
65
+ getContainer: () => editorContainer,
66
+ getInitialContent: () => initialContent,
67
+ completionSource: () => completionSource,
68
+ spellCheckDictionary: () => spellCheckDictionary,
69
+ onCorrectionClick: (correction, _view, from, to) => { activeCorrection = { ...correction, from, to }; },
70
+ onSave: filePersistence.saveContent,
71
+ onFind: () => openFindReplace(),
72
+ onFindReplace: () => openFindReplace(),
73
+ onDocChanged: handleDocChanged,
74
+ onEditorReady: (view) => {
75
+ editorView = view;
76
+ stopExternalChangeMonitoring = filePersistence.startExternalChangeMonitoring();
77
+ },
78
+ onEditorDestroy: () => {
79
+ editorView = null;
80
+ if (autoSaveTimeout) { clearTimeout(autoSaveTimeout); autoSaveTimeout = null; }
81
+ stopExternalChangeMonitoring?.();
82
+ stopExternalChangeMonitoring = null;
99
83
  }
100
- }]);
84
+ });
85
+
86
+ let fileName = $derived(filePath?.split('/').pop() || filePath || 'Untitled');
87
+ let saveStatus = $derived(isSaving ? 'Saving...' : saveError ? 'Error!' : filePath && !filePersistence.persistenceAvailable ? 'Not persisted' : isDirty ? 'Unsaved' : 'Saved');
101
88
 
102
- // Open find/replace dialog
103
- function openFindReplace(withReplace: boolean) {
89
+ function openFindReplace() {
104
90
  showFindReplace = true;
105
- // The dialog will handle toggling replace mode via the button
106
- // We could pass this as a prop if we want to open directly to replace mode
107
91
  }
108
92
 
109
- // Close find/replace dialog
110
93
  function closeFindReplace() {
111
94
  showFindReplace = false;
112
- // Focus back to editor
113
- editorView?.focus();
114
- }
115
-
116
- // Custom dark theme matching Atelier - uses CSS variables via style injection
117
- const atelierTheme = EditorView.theme({
118
- '&': {
119
- backgroundColor: 'var(--color-bg-canvas)',
120
- color: 'var(--color-text-primary)',
121
- fontSize: '14px',
122
- fontFamily: "var(--font-mono)"
123
- },
124
- '.cm-content': {
125
- caretColor: 'var(--color-accent)',
126
- padding: '16px'
127
- },
128
- '.cm-cursor': {
129
- borderLeftColor: 'var(--color-accent)'
130
- },
131
- '.cm-activeLine': {
132
- backgroundColor: 'var(--color-accent-subtle)'
133
- },
134
- '.cm-gutters': {
135
- backgroundColor: 'var(--color-bg-app)',
136
- borderRight: '1px solid var(--border-subtle)',
137
- color: 'var(--color-text-tertiary)'
138
- },
139
- '.cm-activeLineGutter': {
140
- backgroundColor: 'var(--color-accent-subtle)',
141
- color: 'var(--color-accent)'
142
- },
143
- '.cm-selectionBackground': {
144
- backgroundColor: 'var(--color-accent-subtle)'
145
- }
146
- }, { dark: true });
147
-
148
- // Check for external file changes (mtime-based via T0 transport)
149
- async function checkExternalChanges() {
150
- if (!filePath || !persistenceAvailable || isCheckingExternalChanges || showExternalChangeDialog || !isDirty) return;
151
-
152
- isCheckingExternalChanges = true;
153
- try {
154
- const mtime = await getMtimeMs(filePath);
155
- if (mtime !== null && lastKnownMtimeMs !== null && mtime > lastKnownMtimeMs) {
156
- showExternalChangeDialog = true;
157
- }
158
- } catch (err) {
159
- // Silently fail - file might not exist
160
- console.debug('External change check failed:', err);
161
- } finally {
162
- isCheckingExternalChanges = false;
163
- }
95
+ codeMirror.focus();
164
96
  }
165
97
 
166
- // Handle reload from disk
167
- async function handleReloadFromDisk() {
98
+ function handleDocChanged(newContent: string) {
99
+ content = newContent;
100
+ activeCorrection = null;
101
+ isDirty = true;
102
+ saveError = null;
103
+ onChange?.(newContent);
168
104
  if (!filePath) return;
169
-
170
- try {
171
- const newContent = await readTextFile(filePath);
172
-
173
- // Update editor content
174
- if (editorView) {
175
- editorView.dispatch({
176
- changes: {
177
- from: 0,
178
- to: editorView.state.doc.length,
179
- insert: newContent
180
- }
181
- });
182
- }
105
+ if (autoSaveTimeout) clearTimeout(autoSaveTimeout);
106
+ autoSaveTimeout = setTimeout(() => { void filePersistence.saveContent(); }, 800);
107
+ }
183
108
 
184
- content = newContent;
185
- isDirty = false;
186
- saveError = null;
109
+ function handleAcceptCorrection(suggestion: string) {
110
+ if (!activeCorrection || !editorView) return;
111
+ editorView.dispatch({ changes: { from: activeCorrection.from, to: activeCorrection.to, insert: suggestion } });
112
+ activeCorrection = null;
113
+ }
187
114
 
188
- // Notify parent
189
- onExternalChange?.(newContent);
190
- onChange?.(newContent);
115
+ function handleAddWordToDictionary() {
116
+ if (!activeCorrection) return;
117
+ spellCheckDictionary?.addWord(activeCorrection.original);
118
+ activeCorrection = null;
119
+ }
191
120
 
192
- // Update tracked modification time
193
- try { lastKnownMtimeMs = await getMtimeMs(filePath); } catch { /* keep prior value */ }
194
- } catch (err) {
195
- console.error('Failed to reload file:', err);
196
- saveError = err instanceof Error ? err.message : 'Failed to reload file';
197
- } finally {
198
- showExternalChangeDialog = false;
199
- }
121
+ function handleDismissCorrection() {
122
+ activeCorrection = null;
200
123
  }
201
124
 
202
- // Handle keep current (ignore external changes)
203
125
  function handleKeepCurrent() {
204
- showExternalChangeDialog = false;
205
- // Mark as dirty to indicate divergence from disk
206
- isDirty = true;
126
+ filePersistence.keepCurrent();
207
127
  }
208
128
 
209
- // Minimal editor setup (replaces basicSetup)
210
- const minimalSetup: Extension[] = [
211
- lineNumbers(),
212
- highlightActiveLineGutter(),
213
- highlightSpecialChars(),
214
- history(),
215
- keymap.of(defaultKeymap),
216
- keymap.of(historyKeymap),
217
- drawSelection(),
218
- dropCursor(),
219
- rectangularSelection(),
220
- crosshairCursor(),
221
- highlightActiveLine(),
222
- ];
223
-
224
- // Initialize CodeMirror
225
- onMount(() => {
226
- const extensions: Extension[] = [
227
- minimalSetup,
228
- markdown(),
229
- atelierTheme,
230
- saveKeymap,
231
- autoFormat(),
232
- expansion(),
233
- ...(completionSource ? [autocomplete({ source: completionSource })] : []),
234
- ...(spellCheckDictionary ? [spellcheck(spellCheckDictionary)] : []),
235
- EditorView.updateListener.of((update: ViewUpdate) => {
236
- if (update.docChanged) {
237
- content = update.state.doc.toString();
238
- isDirty = true;
239
- saveError = null;
240
- onChange?.(content);
241
- // Auto-save: debounced 800ms after last keystroke
242
- if (filePath) {
243
- if (autoSaveTimeout) clearTimeout(autoSaveTimeout);
244
- autoSaveTimeout = setTimeout(() => saveContent(), 800);
245
- }
246
- }
247
- })
248
- ];
249
-
250
- const state = EditorState.create({
251
- doc: initialContent,
252
- extensions
253
- });
254
-
255
- editorView = new EditorView({
256
- state,
257
- parent: editorContainer
258
- });
259
-
260
- // Seed the tracked modification time, then start external change
261
- // detection polling (every 2 seconds). Skipped without a backend.
262
- if (filePath && persistenceAvailable) {
263
- getMtimeMs(filePath)
264
- .then((m) => { lastKnownMtimeMs = m; })
265
- .catch(() => { /* file may not exist yet */ });
266
- }
267
- externalChangeCheckInterval = setInterval(checkExternalChanges, 2000);
268
-
269
- // Also check when window regains focus
270
- const handleWindowFocus = () => {
271
- checkExternalChanges();
272
- };
273
- window.addEventListener('focus', handleWindowFocus);
274
-
275
- return () => {
276
- window.removeEventListener('focus', handleWindowFocus);
277
- if (externalChangeCheckInterval) {
278
- clearInterval(externalChangeCheckInterval);
279
- externalChangeCheckInterval = null;
280
- }
281
- };
282
- });
283
-
284
- // Cleanup
285
- onDestroy(() => {
286
- editorView?.destroy();
287
- if (autoSaveTimeout) clearTimeout(autoSaveTimeout);
288
- if (externalChangeCheckInterval) {
289
- clearInterval(externalChangeCheckInterval);
290
- externalChangeCheckInterval = null;
291
- }
292
- });
129
+ function togglePreview() {
130
+ showPreview = !showPreview;
131
+ }
293
132
 
294
- // Update content when file changes
295
133
  $effect(() => {
296
- if (editorView && initialContent !== editorView.state.doc.toString()) {
297
- editorView.dispatch({
298
- changes: {
299
- from: 0,
300
- to: editorView.state.doc.length,
301
- insert: initialContent
302
- }
303
- });
134
+ if (editorView) {
135
+ codeMirror.syncContent(initialContent);
304
136
  content = initialContent;
305
137
  isDirty = false;
306
138
  saveError = null;
307
139
  }
308
140
  });
309
-
310
- // Save file
311
- async function saveContent() {
312
- if (!isDirty || isSaving || !filePath) return;
313
-
314
- // No backend → no write. The status chip reads "Not persisted"; do not
315
- // mark clean or fire onSave, and never pretend the save succeeded.
316
- if (!persistenceAvailable) return;
317
-
318
- isSaving = true;
319
- saveError = null;
320
-
321
- try {
322
- await writeTextFile(filePath, content);
323
- isDirty = false;
324
- onSave?.();
325
-
326
- // Update tracked modification time after save
327
- try { lastKnownMtimeMs = await getMtimeMs(filePath); } catch { /* keep prior value */ }
328
- } catch (err) {
329
- saveError = err instanceof Error ? err.message : String(err);
330
- console.error('Failed to save file:', err);
331
- } finally {
332
- isSaving = false;
333
- }
334
- }
335
-
336
- function togglePreview() {
337
- showPreview = !showPreview;
338
- }
339
-
340
- // Simple markdown preview
341
- function markdownToHtml(md: string): string {
342
- return md
343
- .replace(/^### (.*$)/gim, '<h3>$1</h3>')
344
- .replace(/^## (.*$)/gim, '<h2>$1</h2>')
345
- .replace(/^# (.*$)/gim, '<h1>$1</h1>')
346
- .replace(/\*\*(.*?)\*\*/gim, '<strong>$1</strong>')
347
- .replace(/\*(.*?)\*/gim, '<em>$1</em>')
348
- .replace(/`([^`]+)`/gim, '<code>$1</code>')
349
- .replace(/^- (.*$)/gim, '<li>$1</li>')
350
- .replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
351
- .replace(/\n/gim, '<br>');
352
- }
353
141
  </script>
354
142
 
355
143
  <div class="markdown-editor" data-testid="markdown-editor">
356
- <!-- Header -->
357
144
  <div class="editor-header" data-testid="editor-header">
358
145
  <div class="file-info">
359
146
  <span class="file-name" data-testid="editor-filename">{fileName}</span>
360
- <span class="save-status" class:dirty={isDirty} class:error={saveError} data-testid="save-status">
361
- {saveStatus}
362
- </span>
147
+ <span class="save-status" class:dirty={isDirty} class:error={!!saveError} data-testid="save-status">{saveStatus}</span>
363
148
  </div>
364
149
  <div class="editor-actions">
365
- <button
366
- class="action-btn"
367
- class:active={showPreview}
368
- onclick={togglePreview}
369
- >
370
- Preview
371
- </button>
372
- {#if onClose}
373
- <button
374
- class="action-btn"
375
- onclick={onClose}
376
- >
377
- Close
378
- </button>
379
- {/if}
380
- {#if filePath && persistenceAvailable}
381
- <button
382
- class="action-btn primary"
383
- onclick={saveContent}
384
- disabled={!isDirty || isSaving}
385
- >
386
- {isSaving ? 'Saving...' : 'Save'}
387
- </button>
150
+ <button class="action-btn" class:active={showPreview} onclick={togglePreview}>Preview</button>
151
+ {#if onClose}<button class="action-btn" onclick={onClose}>Close</button>{/if}
152
+ {#if filePath && filePersistence.persistenceAvailable}
153
+ <button class="action-btn primary" onclick={filePersistence.saveContent} disabled={!isDirty || isSaving}>{isSaving ? 'Saving...' : 'Save'}</button>
388
154
  {/if}
389
155
  </div>
390
156
  </div>
391
-
392
- <!-- Body -->
393
157
  <div class="editor-body" class:split={showPreview}>
394
158
  <div class="editor-pane" bind:this={editorContainer}>
395
- <!-- Find/Replace Dialog -->
396
- <FindReplaceDialog
397
- {editorView}
398
- isOpen={showFindReplace}
399
- onClose={closeFindReplace}
400
- />
159
+ <FindReplaceDialog {editorView} isOpen={showFindReplace} onClose={closeFindReplace} />
401
160
  </div>
402
-
403
161
  {#if showPreview}
404
- <div class="preview-pane">
405
- <div class="preview-content">
406
- {@html markdownToHtml(content)}
407
- </div>
408
- </div>
162
+ <div class="preview-pane"><div class="preview-content">{@html DOMPurify.sanitize(markdownToHtml(content))}</div></div>
409
163
  {/if}
410
164
  </div>
411
-
412
- {#if saveError}
413
- <div class="error-banner">
414
- Failed to save: {saveError}
415
- </div>
165
+ {#if activeCorrection}
166
+ <CorrectionTooltip correction={{ original: activeCorrection.original, suggestions: activeCorrection.suggestions }} position={activeCorrection.position} onAccept={handleAcceptCorrection} onDismiss={handleDismissCorrection} onAddToDictionary={handleAddWordToDictionary} />
416
167
  {/if}
168
+ {#if saveError}<div class="error-banner">Failed to save: {saveError}</div>{/if}
417
169
  </div>
170
+ {#if showExternalChangeDialog}<ExternalChangeDialog onReload={filePersistence.reloadFromDisk} onKeepCurrent={handleKeepCurrent} />{/if}
418
171
 
419
- <!-- External Change Detection Dialog -->
420
- {#if showExternalChangeDialog}
421
- <div class="dialog-overlay">
422
- <div class="dialog">
423
- <div class="dialog-header">
424
- <h3>File Changed Externally</h3>
425
- </div>
426
- <div class="dialog-body">
427
- <p>This file has been modified outside of Atelier.</p>
428
- <p>Would you like to reload it from disk, or keep your current changes?</p>
429
- </div>
430
- <div class="dialog-footer">
431
- <button
432
- class="action-btn"
433
- onclick={handleKeepCurrent}
434
- >
435
- Keep Current
436
- </button>
437
- <button
438
- class="action-btn primary"
439
- onclick={handleReloadFromDisk}
440
- >
441
- Reload from Disk
442
- </button>
443
- </div>
444
- </div>
445
- </div>
446
- {/if}
447
-
448
- <style>
449
- .markdown-editor {
450
- display: flex;
451
- flex-direction: column;
452
- height: 100%;
453
- background: var(--color-bg-canvas, #141414);
454
- color: var(--color-text-primary, #f5f2eb);
455
- }
456
-
457
- .editor-header {
458
- display: flex;
459
- align-items: center;
460
- justify-content: space-between;
461
- padding: 12px 16px;
462
- border-bottom: 1px solid var(--border-subtle, #2a2a2a);
463
- background: var(--color-bg-panel, #1a1a1a);
464
- }
465
-
466
- .file-info {
467
- display: flex;
468
- align-items: center;
469
- gap: var(--space-3);
470
- }
471
-
472
- .file-name {
473
- font-weight: 500;
474
- font-size: 14px;
475
- }
476
-
477
- .save-status {
478
- font-size: 12px;
479
- color: var(--color-text-tertiary, #6b6b6b);
480
- }
481
-
482
- .save-status.dirty {
483
- color: var(--color-accent);
484
- }
485
-
486
- .save-status.error {
487
- color: var(--color-error, #ef4444);
488
- }
489
-
490
- .editor-actions {
491
- display: flex;
492
- gap: var(--space-2);
493
- }
494
-
495
- .action-btn {
496
- padding: 6px 12px;
497
- border: 1px solid var(--border-subtle, #2a2a2a);
498
- background: var(--color-bg-panel-elevated, #252525);
499
- color: var(--color-text-secondary, #a0a0a0);
500
- border-radius: var(--radius-sm, 4px);
501
- cursor: pointer;
502
- font-size: 13px;
503
- transition: all 0.15s ease;
504
- }
505
-
506
- .action-btn:hover {
507
- background: var(--border-subtle);
508
- color: var(--color-text-primary, #f5f2eb);
509
- }
510
-
511
- .action-btn.active {
512
- background: var(--color-accent-subtle);
513
- color: var(--color-accent);
514
- border-color: var(--color-accent);
515
- }
516
-
517
- .action-btn.primary {
518
- background: var(--color-accent);
519
- color: white;
520
- border-color: var(--color-accent);
521
- }
522
-
523
- .action-btn.primary:hover {
524
- background: var(--color-accent-hover, #c9843d);
525
- }
526
-
527
- .action-btn:disabled {
528
- opacity: 0.5;
529
- cursor: not-allowed;
530
- }
531
-
532
- .editor-body {
533
- display: flex;
534
- flex: 1;
535
- overflow: hidden;
536
- }
537
-
538
- .editor-body.split .editor-pane,
539
- .editor-body.split .preview-pane {
540
- flex: 1;
541
- width: 50%;
542
- }
543
-
544
- .editor-pane {
545
- flex: 1;
546
- overflow: hidden;
547
- position: relative;
548
- :global(.cm-editor) {
549
- height: 100%;
550
- }
551
- :global(.cm-scroller) {
552
- overflow: auto;
553
- }
554
- }
555
-
556
- .preview-pane {
557
- border-left: 1px solid var(--border-subtle, #2a2a2a);
558
- overflow-y: auto;
559
- background: var(--color-bg-panel, #1a1a1a);
560
- }
561
-
562
- .preview-content {
563
- padding: 16px;
564
- font-size: 14px;
565
- line-height: 1.6;
566
- }
567
-
568
- .preview-content :global(h1),
569
- .preview-content :global(h2),
570
- .preview-content :global(h3) {
571
- margin-top: 0;
572
- margin-bottom: 16px;
573
- }
574
-
575
- .preview-content :global(p) {
576
- margin-bottom: 12px;
577
- }
578
-
579
- .preview-content :global(code) {
580
- background: var(--color-bg-panel-elevated, #252525);
581
- padding: 2px 6px;
582
- border-radius: var(--radius-sm, 3px);
583
- font-family: var(--font-mono, 'JetBrains Mono', monospace);
584
- font-size: 0.9em;
585
- }
586
-
587
- .preview-content :global(pre) {
588
- background: var(--color-bg-panel-elevated, #252525);
589
- padding: 12px;
590
- border-radius: var(--radius-md, 6px);
591
- overflow-x: auto;
592
- margin-bottom: 12px;
593
- }
594
-
595
- .preview-content :global(pre code) {
596
- background: none;
597
- padding: 0;
598
- }
599
-
600
- .preview-content :global(ul),
601
- .preview-content :global(ol) {
602
- margin-bottom: 12px;
603
- padding-left: 24px;
604
- }
605
-
606
- .preview-content :global(li) {
607
- margin-bottom: 4px;
608
- }
609
-
610
- .preview-content :global(blockquote) {
611
- border-left: 3px solid var(--color-accent);
612
- padding-left: 12px;
613
- margin-left: 0;
614
- margin-bottom: 12px;
615
- color: var(--color-text-secondary);
616
- }
617
-
618
- .preview-content :global(a) {
619
- color: var(--color-accent);
620
- text-decoration: none;
621
- }
622
-
623
- .preview-content :global(a:hover) {
624
- text-decoration: underline;
625
- }
626
-
627
- .error-banner {
628
- padding: 8px 16px;
629
- background: var(--color-error, #ef4444);
630
- color: white;
631
- font-size: 13px;
632
- }
633
-
634
- /* Dialog Styles */
635
- .dialog-overlay {
636
- position: fixed;
637
- inset: 0;
638
- background: rgba(0, 0, 0, 0.7);
639
- display: flex;
640
- align-items: center;
641
- justify-content: center;
642
- z-index: 1000;
643
- }
644
-
645
- .dialog {
646
- background: var(--color-bg-panel, #1a1a1a);
647
- border: 1px solid var(--border-subtle, #2a2a2a);
648
- border-radius: var(--radius-lg, 8px);
649
- min-width: 400px;
650
- max-width: 500px;
651
- box-shadow: var(--shadow-lg, 0 10px 40px rgba(0,0,0,0.5));
652
- }
653
-
654
- .dialog-header {
655
- padding: 16px 20px;
656
- border-bottom: 1px solid var(--border-subtle, #2a2a2a);
657
- }
658
-
659
- .dialog-header h3 {
660
- margin: 0;
661
- font-size: 16px;
662
- font-weight: 600;
663
- color: var(--color-text-primary, #f5f2eb);
664
- }
665
-
666
- .dialog-body {
667
- padding: 20px;
668
- }
669
-
670
- .dialog-body p {
671
- margin: 0 0 12px 0;
672
- color: var(--color-text-secondary, #a0a0a0);
673
- font-size: 14px;
674
- line-height: 1.5;
675
- }
676
-
677
- .dialog-body p:last-child {
678
- margin-bottom: 0;
679
- }
680
-
681
- .dialog-footer {
682
- display: flex;
683
- justify-content: flex-end;
684
- gap: var(--space-3);
685
- padding: 16px 20px;
686
- border-top: 1px solid var(--border-subtle, #2a2a2a);
687
- }
172
+ <!-- svelte-ignore css_unused_selector -->
173
+ <style>
174
+ .markdown-editor{display:flex;flex-direction:column;height:100%;background:var(--color-bg-canvas,#141414);color:var(--color-text-primary,#f5f2eb)}.editor-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--border-subtle,#2a2a2a);background:var(--color-bg-panel,#1a1a1a)}.file-info{display:flex;align-items:center;gap:var(--space-3)}.file-name{font-weight:500;font-size:14px}.save-status{font-size:12px;color:var(--color-text-tertiary,#6b6b6b)}.save-status.dirty{color:var(--color-accent)}.save-status.error{color:var(--color-error,#ef4444)}.editor-actions{display:flex;gap:var(--space-2)}.action-btn{padding:6px 12px;border:1px solid var(--border-subtle,#2a2a2a);background:var(--color-bg-panel-elevated,#252525);color:var(--color-text-secondary,#a0a0a0);border-radius:var(--radius-sm,4px);cursor:pointer;font-size:13px;transition:all .15s ease}.action-btn:hover{background:var(--border-subtle);color:var(--color-text-primary,#f5f2eb)}.action-btn.active{background:var(--color-accent-subtle);color:var(--color-accent);border-color:var(--color-accent)}.action-btn.primary{background:var(--color-accent);color:white;border-color:var(--color-accent)}.action-btn.primary:hover{background:var(--color-accent-hover,#c9843d)}.action-btn:disabled{opacity:.5;cursor:not-allowed}.editor-body{display:flex;flex:1;overflow:hidden}.editor-body.split>:is(.editor-pane,.preview-pane){flex:1;width:50%}.editor-pane{flex:1;overflow:hidden;position:relative}:global(.cm-editor){height:100%}:global(.cm-scroller){overflow:auto}.preview-pane{border-left:1px solid var(--border-subtle,#2a2a2a);overflow-y:auto;background:var(--color-bg-panel,#1a1a1a)}.preview-content{padding:16px;font-size:14px;line-height:1.6}.preview-content :is(h1,h2,h3){margin-top:0;margin-bottom:16px}.preview-content p,.preview-content ul,.preview-content ol,.preview-content blockquote{margin-bottom:12px}.preview-content ul,.preview-content ol{padding-left:24px}.preview-content li{margin-bottom:4px}.preview-content code{background:var(--color-bg-panel-elevated,#252525);padding:2px 6px;border-radius:var(--radius-sm,3px);font-family:var(--font-mono,'JetBrains Mono',monospace);font-size:.9em}.preview-content pre{background:var(--color-bg-panel-elevated,#252525);padding:12px;border-radius:var(--radius-md,6px);overflow-x:auto;margin-bottom:12px}.preview-content pre code{background:none;padding:0}.preview-content blockquote{border-left:3px solid var(--color-accent);padding-left:12px;margin-left:0;color:var(--color-text-secondary)}.preview-content a{color:var(--color-accent);text-decoration:none}.preview-content a:hover{text-decoration:underline}.error-banner{padding:8px 16px;background:var(--color-error,#ef4444);color:white;font-size:13px}
688
175
  </style>