@stephen-netu/design-system 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +66 -0
  2. package/package.json +130 -0
  3. package/src/b0-app-shell/clarification-overlay/ClarificationOverlay.svelte +221 -0
  4. package/src/b0-app-shell/clarification-overlay/index.ts +1 -0
  5. package/src/b0-app-shell/command-bar/CommandBar.svelte +565 -0
  6. package/src/b0-app-shell/index.ts +14 -0
  7. package/src/base.css +152 -0
  8. package/src/d0-data-viz/BarChart.svelte +236 -0
  9. package/src/d0-data-viz/CheckpointBar.svelte +574 -0
  10. package/src/d0-data-viz/CheckpointPill.svelte +297 -0
  11. package/src/d0-data-viz/MetricCard.svelte +159 -0
  12. package/src/d0-data-viz/SparkLine.svelte +66 -0
  13. package/src/d0-data-viz/StorageMap.svelte +182 -0
  14. package/src/d0-data-viz/Timeline.svelte +188 -0
  15. package/src/d0-data-viz/checkpoint-types.ts +162 -0
  16. package/src/d0-data-viz/d0-tokens.css +32 -0
  17. package/src/d0-data-viz/index.ts +58 -0
  18. package/src/d0-data-viz/storage-map-types.ts +142 -0
  19. package/src/d0-data-viz/theme-integration.ts +134 -0
  20. package/src/d0-data-viz/types.ts +53 -0
  21. package/src/demos/BspSizingInfographic.svelte +386 -0
  22. package/src/demos/BspZoneDemo.svelte +255 -0
  23. package/src/demos/ControlRoomDemo.svelte +570 -0
  24. package/src/demos/NodepadBlocksShowcase.svelte +189 -0
  25. package/src/demos/index.ts +7 -0
  26. package/src/editor/CodeEditor.svelte +188 -0
  27. package/src/editor/FindReplaceDialog.svelte +516 -0
  28. package/src/editor/MarkdownEditor.svelte +660 -0
  29. package/src/editor/index.ts +2 -0
  30. package/src/entity-graph-view/WorldView.svelte +171 -0
  31. package/src/entity-graph-view/index.ts +6 -0
  32. package/src/entity-graph-view/types.ts +364 -0
  33. package/src/epistemic/index.ts +2 -0
  34. package/src/epistemic/types.ts +134 -0
  35. package/src/epistemic/utils.ts +32 -0
  36. package/src/f0-forms/checkbox/Checkbox.svelte +158 -0
  37. package/src/f0-forms/field/FormField.svelte +91 -0
  38. package/src/f0-forms/index.ts +11 -0
  39. package/src/f0-forms/radio/RadioGroup.svelte +192 -0
  40. package/src/f0-forms/search/SearchBar.svelte +99 -0
  41. package/src/f0-forms/select/Select.svelte +198 -0
  42. package/src/f0-forms/textarea/TextArea.svelte +170 -0
  43. package/src/f0-forms/toggle/FormToggle.svelte +147 -0
  44. package/src/f0-forms/validation/validate.ts +47 -0
  45. package/src/fabric/ai/ChatPanel.svelte +648 -0
  46. package/src/fabric/ai/index.ts +11 -0
  47. package/src/fabric/data/index.ts +21 -0
  48. package/src/fabric/feedback/StatusBar.svelte +274 -0
  49. package/src/fabric/feedback/ToastContainer.svelte +122 -0
  50. package/src/fabric/feedback/index.ts +14 -0
  51. package/src/fabric/index.ts +37 -0
  52. package/src/fabric/input/CommandBar.svelte +79 -0
  53. package/src/fabric/input/KeyboardShortcuts.svelte +388 -0
  54. package/src/fabric/input/index.ts +12 -0
  55. package/src/fabric/layout/GlassBox.svelte +193 -0
  56. package/src/fabric/layout/KanbanBoard.svelte +158 -0
  57. package/src/fabric/layout/KanbanCard.svelte +226 -0
  58. package/src/fabric/layout/KanbanColumn.svelte +278 -0
  59. package/src/fabric/layout/index.ts +17 -0
  60. package/src/fabric/navigation/ActivityRail.svelte +271 -0
  61. package/src/fabric/navigation/index.ts +11 -0
  62. package/src/g0-geo/SovereignMap.svelte +149 -0
  63. package/src/g0-geo/index.ts +2 -0
  64. package/src/index.ts +68 -0
  65. package/src/l0-layout/block-writer/BlockConnectionLines.svelte +98 -0
  66. package/src/l0-layout/block-writer/BlockContent.svelte +213 -0
  67. package/src/l0-layout/block-writer/BlockHeader.svelte +230 -0
  68. package/src/l0-layout/block-writer/BlockItem.svelte +177 -0
  69. package/src/l0-layout/block-writer/BlockWriter.svelte +219 -0
  70. package/src/l0-layout/block-writer/block-writer-types.ts +63 -0
  71. package/src/l0-layout/block-writer/index.ts +24 -0
  72. package/src/l0-layout/block-writer/markdown.ts +110 -0
  73. package/src/l0-layout/chat-view/ChatViewLayout.svelte +61 -0
  74. package/src/l0-layout/epistemic/AllayDetailPanel.svelte +113 -0
  75. package/src/l0-layout/epistemic/AssayDetailPanel.svelte +164 -0
  76. package/src/l0-layout/epistemic/AvenueTrace.svelte +253 -0
  77. package/src/l0-layout/epistemic/KnowledgeDetailPanel.svelte +204 -0
  78. package/src/l0-layout/epistemic/ParadigmLens.svelte +170 -0
  79. package/src/l0-layout/epistemic/ProvenanceChain.svelte +164 -0
  80. package/src/l0-layout/epistemic/TrustView.svelte +195 -0
  81. package/src/l0-layout/epistemic/index.ts +7 -0
  82. package/src/l0-layout/grove/AccordCard.svelte +156 -0
  83. package/src/l0-layout/grove/GroveCard.svelte +197 -0
  84. package/src/l0-layout/grove/MemberBadge.svelte +96 -0
  85. package/src/l0-layout/grove/MemberList.svelte +59 -0
  86. package/src/l0-layout/grove/grove-types.ts +29 -0
  87. package/src/l0-layout/grove/index.ts +14 -0
  88. package/src/l0-layout/index.ts +45 -0
  89. package/src/l0-layout/kanban/KanbanBoard.svelte +71 -0
  90. package/src/l0-layout/kanban/KanbanCard.svelte +276 -0
  91. package/src/l0-layout/kanban/KanbanColumn.svelte +198 -0
  92. package/src/l0-layout/kanban/KanbanViewLayout.svelte +61 -0
  93. package/src/l0-layout/workspace/WorkspaceLayout.svelte +93 -0
  94. package/src/n0-node-graph/GraphEditor.svelte +342 -0
  95. package/src/n0-node-graph/GraphHistory.svelte +122 -0
  96. package/src/n0-node-graph/Node.svelte +269 -0
  97. package/src/n0-node-graph/NodeCanvas.svelte +187 -0
  98. package/src/n0-node-graph/NodeEdge.svelte +152 -0
  99. package/src/n0-node-graph/NodePalette.svelte +318 -0
  100. package/src/n0-node-graph/NodePort.svelte +128 -0
  101. package/src/n0-node-graph/index.ts +33 -0
  102. package/src/n0-node-graph/n0-tokens.css +29 -0
  103. package/src/n0-node-graph/node-port-types.ts +19 -0
  104. package/src/n0-node-graph/types.ts +96 -0
  105. package/src/n0-node-graph/validate.test.ts +722 -0
  106. package/src/n0-node-graph/validate.ts +371 -0
  107. package/src/n1-flow-canvas/FlowCanvas.svelte +202 -0
  108. package/src/n1-flow-canvas/graph-state.svelte.ts +95 -0
  109. package/src/n1-flow-canvas/index.ts +21 -0
  110. package/src/n1-flow-canvas/layout.ts +118 -0
  111. package/src/n1-flow-canvas/layout.worker.ts +128 -0
  112. package/src/n1-flow-canvas/types.ts +102 -0
  113. package/src/n1-flow-canvas/utils.ts +146 -0
  114. package/src/n2-manifold-disk/ManifoldDisk.svelte +314 -0
  115. package/src/n2-manifold-disk/index.ts +6 -0
  116. package/src/n2-manifold-disk/poincare.ts +184 -0
  117. package/src/n3-force-graph/ForceGraph.svelte +180 -0
  118. package/src/n3-force-graph/GraphTooltip.svelte +99 -0
  119. package/src/n3-force-graph/animated-edge.ts +107 -0
  120. package/src/n3-force-graph/health-node.ts +128 -0
  121. package/src/n3-force-graph/index.ts +31 -0
  122. package/src/n3-force-graph/types.ts +94 -0
  123. package/src/n4-adaptive-manifold/AdaptiveManifold.svelte +1165 -0
  124. package/src/n4-adaptive-manifold/DetailCard.svelte +212 -0
  125. package/src/n4-adaptive-manifold/SuggestionIndicator.svelte +91 -0
  126. package/src/n4-adaptive-manifold/classifier.ts +139 -0
  127. package/src/n4-adaptive-manifold/index.ts +45 -0
  128. package/src/n4-adaptive-manifold/n4-tokens.css +155 -0
  129. package/src/n4-adaptive-manifold/orchestrator.ts +301 -0
  130. package/src/n4-adaptive-manifold/phase-theme.ts +92 -0
  131. package/src/n4-adaptive-manifold/pinnacle-events.ts +135 -0
  132. package/src/n4-adaptive-manifold/providers/gravitational-provider.ts +191 -0
  133. package/src/n4-adaptive-manifold/providers/manifold-provider.ts +276 -0
  134. package/src/n4-adaptive-manifold/transition.ts +90 -0
  135. package/src/n4-adaptive-manifold/types.ts +313 -0
  136. package/src/p0-primitives/index.ts +6 -0
  137. package/src/p0-primitives/tokens/control-room.css +309 -0
  138. package/src/p0-primitives/tokens/index.ts +6 -0
  139. package/src/p0-primitives/tokens/tokens.css +140 -0
  140. package/src/p0-primitives/tokens/tokens.json +132 -0
  141. package/src/p0-primitives/types/index.ts +123 -0
  142. package/src/p0-primitives/types/plugin-types.ts +74 -0
  143. package/src/p0-primitives/utils/index.ts +39 -0
  144. package/src/platform/hud-protocol.ts +20 -0
  145. package/src/platform/index.ts +3 -0
  146. package/src/s0-state/create-store.ts +84 -0
  147. package/src/s0-state/derived.ts +41 -0
  148. package/src/s0-state/index.ts +6 -0
  149. package/src/showcase/Showcase.svelte +401 -0
  150. package/src/spatial/BspTilingCanvas.svelte +232 -0
  151. package/src/spatial/ForceCanvas.svelte +289 -0
  152. package/src/spatial/LodRenderer.svelte +334 -0
  153. package/src/spatial/SnapZoomCamera.svelte +185 -0
  154. package/src/spatial/ZoneTiler.svelte +311 -0
  155. package/src/spatial/camera-types.ts +45 -0
  156. package/src/spatial/content-proportional-bsp.ts +253 -0
  157. package/src/spatial/force-canvas-types.ts +102 -0
  158. package/src/spatial/index.ts +15 -0
  159. package/src/spatial/lod-types.ts +70 -0
  160. package/src/spatial/zone-tiler-types.ts +177 -0
  161. package/src/stores/vaultStore.svelte +14 -0
  162. package/src/t0-transport/connection.ts +34 -0
  163. package/src/t0-transport/error.ts +37 -0
  164. package/src/t0-transport/events.ts +50 -0
  165. package/src/t0-transport/index.ts +6 -0
  166. package/src/t0-transport/invoke.ts +30 -0
  167. package/src/tauri/index.ts +13 -0
  168. package/src/u0-primitives/allay-gauge/AllayGauge.svelte +90 -0
  169. package/src/u0-primitives/allay-gauge/index.ts +1 -0
  170. package/src/u0-primitives/animated-icon/AnimatedIcon.svelte +273 -0
  171. package/src/u0-primitives/animated-icon/README.md +53 -0
  172. package/src/u0-primitives/animated-icon/index.ts +31 -0
  173. package/src/u0-primitives/assay-badge/AssayBadge.svelte +121 -0
  174. package/src/u0-primitives/assay-badge/AssaySignalBar.svelte +83 -0
  175. package/src/u0-primitives/assay-badge/index.ts +2 -0
  176. package/src/u0-primitives/avatar/Avatar.svelte +177 -0
  177. package/src/u0-primitives/badge/Badge.svelte +149 -0
  178. package/src/u0-primitives/button/Button.svelte +304 -0
  179. package/src/u0-primitives/card/Card.svelte +182 -0
  180. package/src/u0-primitives/dropdown/Dropdown.svelte +305 -0
  181. package/src/u0-primitives/expert-badge/ExpertBadge.svelte +89 -0
  182. package/src/u0-primitives/expert-badge/index.ts +1 -0
  183. package/src/u0-primitives/index.ts +35 -0
  184. package/src/u0-primitives/input/Input.svelte +237 -0
  185. package/src/u0-primitives/modal/Modal.svelte +331 -0
  186. package/src/u0-primitives/modal/index.ts +1 -0
  187. package/src/u0-primitives/spinner/Spinner.svelte +129 -0
  188. package/src/u0-primitives/tabs/Tabs.svelte +225 -0
  189. package/src/u0-primitives/tabs/tabs-types.ts +11 -0
  190. package/src/u0-primitives/toggle/Toggle.svelte +161 -0
  191. package/src/u0-primitives/tooltip/Tooltip.svelte +234 -0
  192. package/src/v0-diagrams/DiagramPanel.svelte +68 -0
  193. package/src/v0-diagrams/MarkmapDiagram.svelte +56 -0
  194. package/src/v0-diagrams/MermaidDiagram.svelte +53 -0
  195. package/src/v0-diagrams/index.ts +4 -0
  196. package/src/v0-diagrams/types.ts +28 -0
  197. package/src/v0-diagrams/v0-tokens.css +6 -0
  198. package/src/vite-env.d.ts +7 -0
  199. package/src/x0-enchanted-blocks/ENCHANTED_BLOCKS_SPEC.md +457 -0
  200. package/src/x0-enchanted-blocks/components/EnchantedBlock.svelte +485 -0
  201. package/src/x0-enchanted-blocks/components/NodepadBlock.svelte +262 -0
  202. package/src/x0-enchanted-blocks/core/block-layout.ts +211 -0
  203. package/src/x0-enchanted-blocks/core/block-types.ts +227 -0
  204. package/src/x0-enchanted-blocks/index.ts +46 -0
@@ -0,0 +1,660 @@
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
+
9
+ import { onMount, onDestroy, untrack } from 'svelte';
10
+ import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightSpecialChars,
11
+ drawSelection, dropCursor, rectangularSelection, crosshairCursor,
12
+ highlightActiveLine } from '@codemirror/view';
13
+ import { EditorState, type Extension } from '@codemirror/state';
14
+ import { markdown } from '@codemirror/lang-markdown';
15
+ import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
16
+ import { search } from '@codemirror/search';
17
+ import { writeFile, readFile } from '../tauri';
18
+ import { vaultStore } from '../stores/vaultStore.svelte';
19
+ import FindReplaceDialog from './FindReplaceDialog.svelte';
20
+
21
+ interface Props {
22
+ filePath: string | null;
23
+ initialContent: string;
24
+ onChange?: (content: string) => void;
25
+ onSave?: () => void;
26
+ onClose?: () => void;
27
+ onExternalChange?: (newContent: string) => void;
28
+ }
29
+
30
+ const props : Props = $props();
31
+
32
+ // Editor state — untrack() tells Svelte we intentionally capture only the initial prop value
33
+ let content = $state(untrack(() => props.initialContent));
34
+ let isDirty = $state(false);
35
+ let isSaving = $state(false);
36
+ let saveError = $state<string | null>(null);
37
+ let showPreview = $state(false);
38
+
39
+ // Auto-save
40
+ let autoSaveTimeout: ReturnType<typeof setTimeout> | null = null;
41
+
42
+ // External change detection
43
+ let showExternalChangeDialog = $state(false);
44
+ let externalChangeCheckInterval: ReturnType<typeof setInterval> | null = null;
45
+ let isCheckingExternalChanges = $state(false);
46
+
47
+ // CodeMirror instance
48
+ let editorContainer: HTMLDivElement;
49
+ let editorView = $state<EditorView | null>(null);
50
+
51
+ // Find/Replace state
52
+ let showFindReplace = $state(false);
53
+
54
+ // Computed
55
+ let fileName = $derived(props.filePath?.split('/').pop() || props.filePath || 'Untitled');
56
+ let saveStatus = $derived(
57
+ isSaving ? 'Saving...' :
58
+ saveError ? 'Error!' :
59
+ isDirty ? 'Unsaved' : 'Saved'
60
+ );
61
+
62
+ // Custom keymap for save and find/replace
63
+ const saveKeymap = keymap.of([{
64
+ key: 'Ctrl-s',
65
+ mac: 'Cmd-s',
66
+ run: () => {
67
+ saveContent();
68
+ return true;
69
+ }
70
+ }, {
71
+ key: 'Ctrl-f',
72
+ mac: 'Cmd-f',
73
+ run: () => {
74
+ openFindReplace(false);
75
+ return true;
76
+ }
77
+ }, {
78
+ key: 'Ctrl-Shift-f',
79
+ mac: 'Cmd-Shift-f',
80
+ run: () => {
81
+ openFindReplace(true);
82
+ return true;
83
+ }
84
+ }]);
85
+
86
+ // Open find/replace dialog
87
+ function openFindReplace(withReplace: boolean) {
88
+ showFindReplace = true;
89
+ // The dialog will handle toggling replace mode via the button
90
+ // We could pass this as a prop if we want to open directly to replace mode
91
+ }
92
+
93
+ // Close find/replace dialog
94
+ function closeFindReplace() {
95
+ showFindReplace = false;
96
+ // Focus back to editor
97
+ editorView?.focus();
98
+ }
99
+
100
+ // Custom dark theme matching Atelier - uses CSS variables via style injection
101
+ const atelierTheme = EditorView.theme({
102
+ '&': {
103
+ backgroundColor: 'var(--bg-canvas)',
104
+ color: 'var(--text-primary)',
105
+ fontSize: '14px',
106
+ fontFamily: "var(--font-mono)"
107
+ },
108
+ '.cm-content': {
109
+ caretColor: 'var(--accent-primary)',
110
+ padding: '16px'
111
+ },
112
+ '.cm-cursor': {
113
+ borderLeftColor: 'var(--accent-primary)'
114
+ },
115
+ '.cm-activeLine': {
116
+ backgroundColor: 'var(--accent-subtle)'
117
+ },
118
+ '.cm-gutters': {
119
+ backgroundColor: 'var(--bg-app)',
120
+ borderRight: '1px solid var(--border-subtle)',
121
+ color: 'var(--text-tertiary)'
122
+ },
123
+ '.cm-activeLineGutter': {
124
+ backgroundColor: 'var(--accent-subtle)',
125
+ color: 'var(--accent-primary)'
126
+ },
127
+ '.cm-selectionBackground': {
128
+ backgroundColor: 'var(--accent-subtle)'
129
+ }
130
+ }, { dark: true });
131
+
132
+ // Check for external file changes
133
+ async function checkExternalChanges() {
134
+ if (!props.filePath || isCheckingExternalChanges || showExternalChangeDialog || !isDirty) return;
135
+
136
+ isCheckingExternalChanges = true;
137
+ try {
138
+ const hasChanged = await vaultStore.hasFileChangedExternally(props.filePath);
139
+ if (hasChanged) {
140
+ showExternalChangeDialog = true;
141
+ }
142
+ } catch (err) {
143
+ // Silently fail - file might not exist
144
+ console.debug('External change check failed:', err);
145
+ } finally {
146
+ isCheckingExternalChanges = false;
147
+ }
148
+ }
149
+
150
+ // Handle reload from disk
151
+ async function handleReloadFromDisk() {
152
+ if (!props.filePath) return;
153
+
154
+ try {
155
+ const newContent = await readFile(props.filePath);
156
+
157
+ // Update editor content
158
+ if (editorView) {
159
+ editorView.dispatch({
160
+ changes: {
161
+ from: 0,
162
+ to: editorView.state.doc.length,
163
+ insert: newContent
164
+ }
165
+ });
166
+ }
167
+
168
+ content = newContent;
169
+ isDirty = false;
170
+ saveError = null;
171
+
172
+ // Notify parent
173
+ props.onExternalChange?.(newContent);
174
+ props.onChange?.(newContent);
175
+
176
+ // Update tracked state in vaultStore
177
+ const modifiedAt = await vaultStore.getFileModifiedTime(props.filePath);
178
+ vaultStore.updateFileState(props.filePath, newContent, modifiedAt);
179
+ } catch (err) {
180
+ console.error('Failed to reload file:', err);
181
+ saveError = err instanceof Error ? err.message : 'Failed to reload file';
182
+ } finally {
183
+ showExternalChangeDialog = false;
184
+ }
185
+ }
186
+
187
+ // Handle keep current (ignore external changes)
188
+ function handleKeepCurrent() {
189
+ showExternalChangeDialog = false;
190
+ // Mark as dirty to indicate divergence from disk
191
+ isDirty = true;
192
+ }
193
+
194
+ // Minimal editor setup (replaces basicSetup)
195
+ const minimalSetup: Extension[] = [
196
+ lineNumbers(),
197
+ highlightActiveLineGutter(),
198
+ highlightSpecialChars(),
199
+ history(),
200
+ keymap.of(defaultKeymap),
201
+ keymap.of(historyKeymap),
202
+ drawSelection(),
203
+ dropCursor(),
204
+ rectangularSelection(),
205
+ crosshairCursor(),
206
+ highlightActiveLine(),
207
+ ];
208
+
209
+ // Initialize CodeMirror
210
+ onMount(() => {
211
+ const extensions: Extension[] = [
212
+ minimalSetup,
213
+ markdown(),
214
+ atelierTheme,
215
+ saveKeymap,
216
+ EditorView.updateListener.of((update) => {
217
+ if (update.docChanged) {
218
+ content = update.state.doc.toString();
219
+ isDirty = true;
220
+ saveError = null;
221
+ props.onChange?.(content);
222
+ // Auto-save: debounced 800ms after last keystroke
223
+ if (props.filePath) {
224
+ if (autoSaveTimeout) clearTimeout(autoSaveTimeout);
225
+ autoSaveTimeout = setTimeout(() => saveContent(), 800);
226
+ }
227
+ }
228
+ })
229
+ ];
230
+
231
+ const state = EditorState.create({
232
+ doc: props.initialContent,
233
+ extensions
234
+ });
235
+
236
+ editorView = new EditorView({
237
+ state,
238
+ parent: editorContainer
239
+ });
240
+
241
+ // Start external change detection polling (every 2 seconds)
242
+ externalChangeCheckInterval = setInterval(checkExternalChanges, 2000);
243
+
244
+ // Also check when window regains focus
245
+ const handleWindowFocus = () => {
246
+ checkExternalChanges();
247
+ };
248
+ window.addEventListener('focus', handleWindowFocus);
249
+
250
+ return () => {
251
+ window.removeEventListener('focus', handleWindowFocus);
252
+ if (externalChangeCheckInterval) {
253
+ clearInterval(externalChangeCheckInterval);
254
+ externalChangeCheckInterval = null;
255
+ }
256
+ };
257
+ });
258
+
259
+ // Cleanup
260
+ onDestroy(() => {
261
+ editorView?.destroy();
262
+ if (autoSaveTimeout) clearTimeout(autoSaveTimeout);
263
+ if (externalChangeCheckInterval) {
264
+ clearInterval(externalChangeCheckInterval);
265
+ externalChangeCheckInterval = null;
266
+ }
267
+ });
268
+
269
+ // Update content when file changes
270
+ $effect(() => {
271
+ if (editorView && props.initialContent !== editorView.state.doc.toString()) {
272
+ editorView.dispatch({
273
+ changes: {
274
+ from: 0,
275
+ to: editorView.state.doc.length,
276
+ insert: props.initialContent
277
+ }
278
+ });
279
+ content = props.initialContent;
280
+ isDirty = false;
281
+ saveError = null;
282
+ }
283
+ });
284
+
285
+ // Save file
286
+ async function saveContent() {
287
+ if (!isDirty || isSaving || !props.filePath) return;
288
+
289
+ isSaving = true;
290
+ saveError = null;
291
+
292
+ try {
293
+ await writeFile(props.filePath, content);
294
+ isDirty = false;
295
+ props.onSave?.();
296
+
297
+ // Update tracked modification time after save
298
+ const modifiedAt = await vaultStore.getFileModifiedTime(props.filePath);
299
+ vaultStore.updateFileState(props.filePath, content, modifiedAt);
300
+ } catch (err) {
301
+ saveError = err instanceof Error ? err.message : String(err);
302
+ console.error('Failed to save file:', err);
303
+ } finally {
304
+ isSaving = false;
305
+ }
306
+ }
307
+
308
+ function togglePreview() {
309
+ showPreview = !showPreview;
310
+ }
311
+
312
+ // Simple markdown preview
313
+ function markdownToHtml(md: string): string {
314
+ return md
315
+ .replace(/^### (.*$)/gim, '<h3>$1</h3>')
316
+ .replace(/^## (.*$)/gim, '<h2>$1</h2>')
317
+ .replace(/^# (.*$)/gim, '<h1>$1</h1>')
318
+ .replace(/\*\*(.*?)\*\*/gim, '<strong>$1</strong>')
319
+ .replace(/\*(.*?)\*/gim, '<em>$1</em>')
320
+ .replace(/`([^`]+)`/gim, '<code>$1</code>')
321
+ .replace(/^- (.*$)/gim, '<li>$1</li>')
322
+ .replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
323
+ .replace(/\n/gim, '<br>');
324
+ }
325
+ </script>
326
+
327
+ <div class="markdown-editor" data-testid="markdown-editor">
328
+ <!-- Header -->
329
+ <div class="editor-header" data-testid="editor-header">
330
+ <div class="file-info">
331
+ <span class="file-name" data-testid="editor-filename">{fileName}</span>
332
+ <span class="save-status" class:dirty={isDirty} class:error={saveError} data-testid="save-status">
333
+ {saveStatus}
334
+ </span>
335
+ </div>
336
+ <div class="editor-actions">
337
+ <button
338
+ class="action-btn"
339
+ class:active={showPreview}
340
+ onclick={togglePreview}
341
+ >
342
+ Preview
343
+ </button>
344
+ {#if props.onClose}
345
+ <button
346
+ class="action-btn"
347
+ onclick={props.onClose}
348
+ >
349
+ Close
350
+ </button>
351
+ {/if}
352
+ {#if !props.filePath}
353
+ <button
354
+ class="action-btn primary"
355
+ onclick={saveContent}
356
+ disabled={!isDirty || isSaving}
357
+ >
358
+ {isSaving ? 'Saving...' : 'Save'}
359
+ </button>
360
+ {/if}
361
+ </div>
362
+ </div>
363
+
364
+ <!-- Body -->
365
+ <div class="editor-body" class:split={showPreview}>
366
+ <div class="editor-pane" bind:this={editorContainer}>
367
+ <!-- Find/Replace Dialog -->
368
+ <FindReplaceDialog
369
+ {editorView}
370
+ isOpen={showFindReplace}
371
+ onClose={closeFindReplace}
372
+ />
373
+ </div>
374
+
375
+ {#if showPreview}
376
+ <div class="preview-pane">
377
+ <div class="preview-content">
378
+ {@html markdownToHtml(content)}
379
+ </div>
380
+ </div>
381
+ {/if}
382
+ </div>
383
+
384
+ {#if saveError}
385
+ <div class="error-banner">
386
+ Failed to save: {saveError}
387
+ </div>
388
+ {/if}
389
+ </div>
390
+
391
+ <!-- External Change Detection Dialog -->
392
+ {#if showExternalChangeDialog}
393
+ <div class="dialog-overlay">
394
+ <div class="dialog">
395
+ <div class="dialog-header">
396
+ <h3>File Changed Externally</h3>
397
+ </div>
398
+ <div class="dialog-body">
399
+ <p>This file has been modified outside of Atelier.</p>
400
+ <p>Would you like to reload it from disk, or keep your current changes?</p>
401
+ </div>
402
+ <div class="dialog-footer">
403
+ <button
404
+ class="action-btn"
405
+ onclick={handleKeepCurrent}
406
+ >
407
+ Keep Current
408
+ </button>
409
+ <button
410
+ class="action-btn primary"
411
+ onclick={handleReloadFromDisk}
412
+ >
413
+ Reload from Disk
414
+ </button>
415
+ </div>
416
+ </div>
417
+ </div>
418
+ {/if}
419
+
420
+ <style>
421
+ .markdown-editor {
422
+ display: flex;
423
+ flex-direction: column;
424
+ height: 100%;
425
+ background: var(--bg-canvas, #141414);
426
+ color: var(--text-primary, #f5f2eb);
427
+ }
428
+
429
+ .editor-header {
430
+ display: flex;
431
+ align-items: center;
432
+ justify-content: space-between;
433
+ padding: 12px 16px;
434
+ border-bottom: 1px solid var(--border-subtle, #2a2a2a);
435
+ background: var(--bg-panel, #1a1a1a);
436
+ }
437
+
438
+ .file-info {
439
+ display: flex;
440
+ align-items: center;
441
+ gap: 12px;
442
+ }
443
+
444
+ .file-name {
445
+ font-weight: 500;
446
+ font-size: 14px;
447
+ }
448
+
449
+ .save-status {
450
+ font-size: 12px;
451
+ color: var(--text-tertiary, #6b6b6b);
452
+ }
453
+
454
+ .save-status.dirty {
455
+ color: var(--accent-primary, #b87333);
456
+ }
457
+
458
+ .save-status.error {
459
+ color: var(--color-error, #ef4444);
460
+ }
461
+
462
+ .editor-actions {
463
+ display: flex;
464
+ gap: 8px;
465
+ }
466
+
467
+ .action-btn {
468
+ padding: 6px 12px;
469
+ border: 1px solid var(--border-subtle, #2a2a2a);
470
+ background: var(--bg-panel-elevated, #252525);
471
+ color: var(--text-secondary, #a0a0a0);
472
+ border-radius: var(--radius-sm, 4px);
473
+ cursor: pointer;
474
+ font-size: 13px;
475
+ transition: all 0.15s ease;
476
+ }
477
+
478
+ .action-btn:hover {
479
+ background: var(--border-subtle);
480
+ color: var(--text-primary, #f5f2eb);
481
+ }
482
+
483
+ .action-btn.active {
484
+ background: var(--accent-subtle, rgba(184, 115, 51, 0.15));
485
+ color: var(--accent-primary, #b87333);
486
+ border-color: var(--accent-primary, #b87333);
487
+ }
488
+
489
+ .action-btn.primary {
490
+ background: var(--accent-primary, #b87333);
491
+ color: white;
492
+ border-color: var(--accent-primary, #b87333);
493
+ }
494
+
495
+ .action-btn.primary:hover {
496
+ background: var(--accent-hover, #c9843d);
497
+ }
498
+
499
+ .action-btn:disabled {
500
+ opacity: 0.5;
501
+ cursor: not-allowed;
502
+ }
503
+
504
+ .editor-body {
505
+ display: flex;
506
+ flex: 1;
507
+ overflow: hidden;
508
+ }
509
+
510
+ .editor-body.split .editor-pane,
511
+ .editor-body.split .preview-pane {
512
+ flex: 1;
513
+ width: 50%;
514
+ }
515
+
516
+ .editor-pane {
517
+ flex: 1;
518
+ overflow: hidden;
519
+ position: relative;
520
+ :global(.cm-editor) {
521
+ height: 100%;
522
+ }
523
+ :global(.cm-scroller) {
524
+ overflow: auto;
525
+ }
526
+ }
527
+
528
+ .preview-pane {
529
+ border-left: 1px solid var(--border-subtle, #2a2a2a);
530
+ overflow-y: auto;
531
+ background: var(--bg-panel, #1a1a1a);
532
+ }
533
+
534
+ .preview-content {
535
+ padding: 16px;
536
+ font-size: 14px;
537
+ line-height: 1.6;
538
+ }
539
+
540
+ .preview-content :global(h1),
541
+ .preview-content :global(h2),
542
+ .preview-content :global(h3) {
543
+ margin-top: 0;
544
+ margin-bottom: 16px;
545
+ }
546
+
547
+ .preview-content :global(p) {
548
+ margin-bottom: 12px;
549
+ }
550
+
551
+ .preview-content :global(code) {
552
+ background: var(--bg-panel-elevated, #252525);
553
+ padding: 2px 6px;
554
+ border-radius: var(--radius-sm, 3px);
555
+ font-family: var(--font-mono, 'JetBrains Mono', monospace);
556
+ font-size: 0.9em;
557
+ }
558
+
559
+ .preview-content :global(pre) {
560
+ background: var(--bg-panel-elevated, #252525);
561
+ padding: 12px;
562
+ border-radius: var(--radius-md, 6px);
563
+ overflow-x: auto;
564
+ margin-bottom: 12px;
565
+ }
566
+
567
+ .preview-content :global(pre code) {
568
+ background: none;
569
+ padding: 0;
570
+ }
571
+
572
+ .preview-content :global(ul),
573
+ .preview-content :global(ol) {
574
+ margin-bottom: 12px;
575
+ padding-left: 24px;
576
+ }
577
+
578
+ .preview-content :global(li) {
579
+ margin-bottom: 4px;
580
+ }
581
+
582
+ .preview-content :global(blockquote) {
583
+ border-left: 3px solid var(--accent-primary);
584
+ padding-left: 12px;
585
+ margin-left: 0;
586
+ margin-bottom: 12px;
587
+ color: var(--text-secondary);
588
+ }
589
+
590
+ .preview-content :global(a) {
591
+ color: var(--accent-primary);
592
+ text-decoration: none;
593
+ }
594
+
595
+ .preview-content :global(a:hover) {
596
+ text-decoration: underline;
597
+ }
598
+
599
+ .error-banner {
600
+ padding: 8px 16px;
601
+ background: var(--color-error, #ef4444);
602
+ color: white;
603
+ font-size: 13px;
604
+ }
605
+
606
+ /* Dialog Styles */
607
+ .dialog-overlay {
608
+ position: fixed;
609
+ inset: 0;
610
+ background: rgba(0, 0, 0, 0.7);
611
+ display: flex;
612
+ align-items: center;
613
+ justify-content: center;
614
+ z-index: 1000;
615
+ }
616
+
617
+ .dialog {
618
+ background: var(--bg-panel, #1a1a1a);
619
+ border: 1px solid var(--border-subtle, #2a2a2a);
620
+ border-radius: var(--radius-lg, 8px);
621
+ min-width: 400px;
622
+ max-width: 500px;
623
+ box-shadow: var(--shadow-lg, 0 10px 40px rgba(0,0,0,0.5));
624
+ }
625
+
626
+ .dialog-header {
627
+ padding: 16px 20px;
628
+ border-bottom: 1px solid var(--border-subtle, #2a2a2a);
629
+ }
630
+
631
+ .dialog-header h3 {
632
+ margin: 0;
633
+ font-size: 16px;
634
+ font-weight: 600;
635
+ color: var(--text-primary, #f5f2eb);
636
+ }
637
+
638
+ .dialog-body {
639
+ padding: 20px;
640
+ }
641
+
642
+ .dialog-body p {
643
+ margin: 0 0 12px 0;
644
+ color: var(--text-secondary, #a0a0a0);
645
+ font-size: 14px;
646
+ line-height: 1.5;
647
+ }
648
+
649
+ .dialog-body p:last-child {
650
+ margin-bottom: 0;
651
+ }
652
+
653
+ .dialog-footer {
654
+ display: flex;
655
+ justify-content: flex-end;
656
+ gap: 12px;
657
+ padding: 16px 20px;
658
+ border-top: 1px solid var(--border-subtle, #2a2a2a);
659
+ }
660
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default as MarkdownEditor } from './MarkdownEditor.svelte';
2
+ export { default as CodeEditor } from './CodeEditor.svelte';