@aprovan/patchwork-editor 0.1.2-dev.68f15f3 → 0.1.2-dev.78c0b14

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 (41) hide show
  1. package/.turbo/turbo-build.log +3 -3
  2. package/dist/components/CodePreview.d.ts +2 -3
  3. package/dist/components/MarkdownEditor.d.ts +1 -1
  4. package/dist/components/MarkdownPreview.d.ts +1 -1
  5. package/dist/components/SaveStatusButton.d.ts +1 -1
  6. package/dist/components/ServicesInspector.d.ts +1 -1
  7. package/dist/components/WidgetPreview.d.ts +1 -1
  8. package/dist/components/edit/CodeBlockView.d.ts +1 -1
  9. package/dist/components/edit/EditHistory.d.ts +1 -1
  10. package/dist/components/edit/EditModal.d.ts +1 -1
  11. package/dist/components/edit/FileTree.d.ts +1 -1
  12. package/dist/components/edit/MediaPreview.d.ts +1 -1
  13. package/dist/components/edit/SaveConfirmDialog.d.ts +1 -1
  14. package/dist/components/edit/index.d.ts +1 -0
  15. package/dist/components/edit/useEditSession.d.ts +1 -1
  16. package/dist/components/edit/useProjectState.d.ts +17 -0
  17. package/dist/index.d.ts +11 -3
  18. package/dist/index.js +414 -403
  19. package/dist/lib/vfs.d.ts +2 -3
  20. package/package.json +6 -4
  21. package/src/components/CodePreview.tsx +7 -10
  22. package/src/components/MarkdownEditor.tsx +3 -3
  23. package/src/components/MarkdownPreview.tsx +2 -2
  24. package/src/components/ServicesInspector.tsx +1 -1
  25. package/src/components/WidgetPreview.tsx +1 -1
  26. package/src/components/edit/CodeBlockView.tsx +6 -2
  27. package/src/components/edit/EditHistory.tsx +1 -1
  28. package/src/components/edit/EditModal.tsx +6 -6
  29. package/src/components/edit/FileTree.tsx +2 -3
  30. package/src/components/edit/MediaPreview.tsx +1 -1
  31. package/src/components/edit/index.ts +4 -0
  32. package/src/components/edit/useEditSession.ts +19 -109
  33. package/src/components/edit/useProjectState.ts +122 -0
  34. package/src/index.ts +15 -18
  35. package/src/lib/code-extractor.ts +1 -1
  36. package/src/lib/vfs.ts +4 -5
  37. package/tsconfig.json +2 -1
  38. package/dist/components/index.d.ts +0 -7
  39. package/dist/lib/index.d.ts +0 -4
  40. package/src/components/index.ts +0 -7
  41. package/src/lib/index.ts +0 -4
package/dist/lib/vfs.d.ts CHANGED
@@ -8,11 +8,10 @@ export declare function getVFSConfig(): Promise<{
8
8
  }>;
9
9
  /**
10
10
  * Get the VFS store instance (creates one if needed).
11
- * Store uses HttpBackend to persist to the stitchery server.
12
11
  */
13
12
  export declare function getVFSStore(): VFSStore;
14
13
  /**
15
- * Save a virtual project to disk via the stitchery server.
14
+ * Save a virtual project to disk via the VFS server.
16
15
  * Projects are saved under their ID in the VFS directory.
17
16
  */
18
17
  export declare function saveProject(project: VirtualProject): Promise<void>;
@@ -31,6 +30,6 @@ export declare function saveFile(path: string, content: string): Promise<void>;
31
30
  export declare function loadFile(path: string, encoding?: 'utf8' | 'base64'): Promise<string>;
32
31
  export declare function subscribeToChanges(callback: (record: ChangeRecord) => void): () => void;
33
32
  /**
34
- * Check if VFS is available (stitchery server is running with vfs-dir enabled).
33
+ * Check if VFS is available (backend server has /vfs routes enabled).
35
34
  */
36
35
  export declare function isVFSAvailable(): Promise<boolean>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aprovan/patchwork-editor",
3
- "version": "0.1.2-dev.68f15f3",
3
+ "version": "0.1.2-dev.78c0b14",
4
4
  "description": "Components for facilitating widget generation and editing",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,15 +18,17 @@
18
18
  "@tiptap/pm": "^3.19.0",
19
19
  "@tiptap/react": "^3.19.0",
20
20
  "@tiptap/starter-kit": "^3.19.0",
21
+ "@types/dompurify": "^3.2.0",
21
22
  "clsx": "^2.1.1",
23
+ "dompurify": "^3.4.8",
22
24
  "lucide-react": "^0.511.0",
23
25
  "react-markdown": "^10.1.0",
24
26
  "remark-gfm": "^4.0.1",
25
27
  "shiki": "^3.22.0",
26
28
  "tailwind-merge": "^3.4.0",
27
29
  "tiptap-markdown": "^0.9.0",
28
- "@aprovan/bobbin": "0.1.0-dev.68f15f3",
29
- "@aprovan/patchwork-compiler": "0.1.2-dev.68f15f3"
30
+ "@aprovan/patchwork-compiler": "0.1.2-dev.78c0b14",
31
+ "@aprovan/bobbin": "0.1.0-dev.78c0b14"
30
32
  },
31
33
  "peerDependencies": {
32
34
  "react": "^18.0.0 || ^19.0.0",
@@ -39,7 +41,7 @@
39
41
  "typescript": "^5.9.3"
40
42
  },
41
43
  "scripts": {
42
- "build": "tsup && tsc --declaration --emitDeclarationOnly --outDir dist --jsx react-jsx --lib ES2022,DOM --skipLibCheck src/index.ts",
44
+ "build": "tsup && tsc -p tsconfig.json --emitDeclarationOnly",
43
45
  "dev": "tsup --watch",
44
46
  "typecheck": "tsc --noEmit"
45
47
  }
@@ -1,13 +1,12 @@
1
- import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
2
- import { Code, Eye, Pencil, RotateCcw, MessageSquare } from 'lucide-react';
3
- import type { Compiler, Manifest } from '@aprovan/patchwork-compiler';
4
1
  import { createSingleFileProject } from '@aprovan/patchwork-compiler';
2
+ import { Code, Eye, Pencil, RotateCcw, MessageSquare } from 'lucide-react';
3
+ import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
4
+ import { saveProject, getVFSConfig, loadFile, subscribeToChanges } from '../lib/vfs';
5
5
  import { EditModal, type CompileFn, CodeBlockView, MediaPreview, getFileType } from './edit';
6
+ import { MarkdownPreview } from './MarkdownPreview';
6
7
  import { SaveStatusButton, type SaveStatus } from './SaveStatusButton';
7
8
  import { WidgetPreview } from './WidgetPreview';
8
- import { MarkdownPreview } from './MarkdownPreview';
9
- import { saveProject, getVFSConfig, loadFile, subscribeToChanges } from '../lib/vfs';
10
- import type { VirtualProject } from '@aprovan/patchwork-compiler';
9
+ import type { Compiler, Manifest , VirtualProject } from '@aprovan/patchwork-compiler';
11
10
 
12
11
  interface CodePreviewProps {
13
12
  code: string;
@@ -157,8 +156,7 @@ export function CodePreview({
157
156
  await saveProject(project);
158
157
  setLastSavedCode(currentCode);
159
158
  setSaveStatus('saved');
160
- } catch (err) {
161
- console.warn('[VFS] Failed to save project:', err);
159
+ } catch {
162
160
  setSaveStatus('error');
163
161
  }
164
162
  }, [currentCode, getProjectId, getEntryFile]);
@@ -340,8 +338,7 @@ export function CodePreview({
340
338
  await saveProject(project);
341
339
  setLastSavedCode(finalCode);
342
340
  setSaveStatus('saved');
343
- } catch (err) {
344
- console.warn('[VFS] Failed to save project:', err);
341
+ } catch {
345
342
  setSaveStatus('error');
346
343
  }
347
344
  })();
@@ -1,10 +1,10 @@
1
- import { useEditor, EditorContent } from '@tiptap/react';
2
- import StarterKit from '@tiptap/starter-kit';
3
1
  import Placeholder from '@tiptap/extension-placeholder';
4
2
  import Typography from '@tiptap/extension-typography';
5
- import { Markdown } from 'tiptap-markdown';
6
3
  import { TextSelection } from '@tiptap/pm/state';
4
+ import { useEditor, EditorContent } from '@tiptap/react';
5
+ import StarterKit from '@tiptap/starter-kit';
7
6
  import { useEffect, useCallback, useRef } from 'react';
7
+ import { Markdown } from 'tiptap-markdown';
8
8
  import { CodeBlockExtension } from './CodeBlockExtension';
9
9
 
10
10
  interface MarkdownEditorProps {
@@ -1,8 +1,8 @@
1
+ import Typography from '@tiptap/extension-typography';
1
2
  import { useEditor, EditorContent } from '@tiptap/react';
2
3
  import StarterKit from '@tiptap/starter-kit';
3
- import Typography from '@tiptap/extension-typography';
4
- import { Markdown } from 'tiptap-markdown';
5
4
  import { useEffect, useCallback, useRef, useState } from 'react';
5
+ import { Markdown } from 'tiptap-markdown';
6
6
  import { CodeBlockExtension } from './CodeBlockExtension';
7
7
 
8
8
  function parseFrontmatter(content: string): { frontmatter: string; body: string } {
@@ -1,5 +1,5 @@
1
- import { useState } from "react";
2
1
  import { ChevronDown, Server } from "lucide-react";
2
+ import { useState } from "react";
3
3
 
4
4
  export interface ServiceInfo {
5
5
  name: string;
@@ -1,5 +1,5 @@
1
- import { useEffect, useRef, useState } from 'react';
2
1
  import { AlertCircle, Loader2 } from 'lucide-react';
2
+ import { useEffect, useRef, useState } from 'react';
3
3
  import type { Compiler, Manifest, MountedWidget } from '@aprovan/patchwork-compiler';
4
4
 
5
5
  export interface WidgetPreviewProps {
@@ -1,3 +1,4 @@
1
+ import DOMPurify from 'dompurify';
1
2
  import { useCallback, useRef, useEffect, useState, useMemo } from 'react';
2
3
  import { createHighlighter, type Highlighter, type BundledLanguage } from 'shiki';
3
4
 
@@ -117,12 +118,15 @@ export function CodeBlockView({ content, language, editable = false, onChange }:
117
118
  const highlightedHtml = useMemo(() => {
118
119
  if (!highlighter) return null;
119
120
  try {
120
- return highlighter.codeToHtml(content, {
121
+ const raw = highlighter.codeToHtml(content, {
121
122
  lang: shikiLang,
122
123
  theme: 'github-light',
123
124
  });
125
+ return DOMPurify.sanitize(raw, {
126
+ ALLOWED_TAGS: ['pre', 'code', 'span', 'div'],
127
+ ALLOWED_ATTR: ['class', 'style'],
128
+ });
124
129
  } catch {
125
- // Fallback if language is not supported
126
130
  return null;
127
131
  }
128
132
  }, [highlighter, content, shikiLang]);
@@ -1,5 +1,5 @@
1
- import { useRef, useEffect } from 'react';
2
1
  import { Loader2 } from 'lucide-react';
2
+ import { useRef, useEffect } from 'react';
3
3
  import Markdown from 'react-markdown';
4
4
  import remarkGfm from 'remark-gfm';
5
5
  import type { EditHistoryEntry } from './types';
@@ -1,4 +1,4 @@
1
- import { useState, useCallback, useMemo, useRef, useEffect, type ReactNode } from 'react';
1
+ import { Bobbin, serializeChangesToYAML, type Change } from '@aprovan/bobbin';
2
2
  import {
3
3
  Code,
4
4
  Eye,
@@ -11,18 +11,18 @@ import {
11
11
  FolderTree,
12
12
  FileCode,
13
13
  } from 'lucide-react';
14
+ import { useState, useCallback, useMemo, useRef, useEffect, type ReactNode } from 'react';
14
15
  import { MarkdownEditor } from '../MarkdownEditor';
15
16
  import { MarkdownPreview } from '../MarkdownPreview';
16
17
  import { SaveStatusButton, type SaveStatus } from '../SaveStatusButton';
18
+ import { CodeBlockView } from './CodeBlockView';
17
19
  import { EditHistory } from './EditHistory';
18
20
  import { FileTree } from './FileTree';
19
- import { SaveConfirmDialog } from './SaveConfirmDialog';
20
- import { CodeBlockView } from './CodeBlockView';
21
+ import { getFileType, isCompilable, isMarkdownFile, getMimeType } from './fileTypes';
21
22
  import { MediaPreview } from './MediaPreview';
22
- import { useEditSession, type UseEditSessionOptions } from './useEditSession';
23
+ import { SaveConfirmDialog } from './SaveConfirmDialog';
23
24
  import { getActiveContent, getFiles } from './types';
24
- import { getFileType, isCompilable, isMarkdownFile, getMimeType } from './fileTypes';
25
- import { Bobbin, serializeChangesToYAML, type Change } from '@aprovan/bobbin';
25
+ import { useEditSession, type UseEditSessionOptions } from './useEditSession';
26
26
  import type { VirtualProject } from '@aprovan/patchwork-compiler';
27
27
 
28
28
  // Simple hash for React key to force re-render on code changes
@@ -1,4 +1,3 @@
1
- import { useMemo, useState, useRef, useCallback, useEffect, type ReactNode } from 'react';
2
1
  import {
3
2
  ChevronRight,
4
3
  ChevronDown,
@@ -12,8 +11,9 @@ import {
12
11
  PinOff,
13
12
  X,
14
13
  } from 'lucide-react';
15
- import type { VirtualFile } from '@aprovan/patchwork-compiler';
14
+ import { useMemo, useState, useRef, useCallback, useEffect, type ReactNode } from 'react';
16
15
  import { isMediaFile } from './fileTypes';
16
+ import type { VirtualFile } from '@aprovan/patchwork-compiler';
17
17
 
18
18
  interface TreeNode {
19
19
  name: string;
@@ -384,7 +384,6 @@ function LazyTreeNode({
384
384
  .catch((err) => setLoadError(err instanceof Error ? err.message : 'Failed to load directory'))
385
385
  .finally(() => setLoading(false));
386
386
  }
387
- // eslint-disable-next-line react-hooks/exhaustive-deps
388
387
  }, [reloadToken]);
389
388
 
390
389
  const isActive = entry.path === activePath;
@@ -1,5 +1,5 @@
1
- import { useState, useEffect } from 'react';
2
1
  import { FileImage, FileVideo, AlertCircle } from 'lucide-react';
2
+ import { useState, useEffect } from 'react';
3
3
  import { isImageFile, isVideoFile } from './fileTypes';
4
4
 
5
5
  export interface MediaPreviewProps {
@@ -1,5 +1,9 @@
1
+ // Intentional public API boundary for the edit sub-module.
2
+ // Consumers: src/index.ts, CodePreview.tsx, and internal edit components.
3
+
1
4
  export * from './types';
2
5
  export * from './api';
6
+ export * from './useProjectState';
3
7
  export * from './useEditSession';
4
8
  export * from './EditHistory';
5
9
  export * from './EditModal';
@@ -1,13 +1,13 @@
1
- import { useState, useCallback, useMemo, useEffect, useRef } from "react";
2
- import type { VirtualProject } from "@aprovan/patchwork-compiler";
3
- import { createSingleFileProject } from "@aprovan/patchwork-compiler";
1
+ import { useState, useCallback } from "react";
4
2
  import { sendEditRequest } from "./api";
3
+ import { useProjectState } from "./useProjectState";
5
4
  import type {
6
5
  EditHistoryEntry,
7
6
  EditSessionState,
8
7
  EditSessionActions,
9
8
  CompileFn,
10
9
  } from "./types";
10
+ import type { VirtualProject } from "@aprovan/patchwork-compiler";
11
11
 
12
12
  export interface UseEditSessionOptions {
13
13
  originalCode?: string;
@@ -17,67 +17,20 @@ export interface UseEditSessionOptions {
17
17
  apiEndpoint?: string;
18
18
  }
19
19
 
20
- function cloneProject(project: VirtualProject): VirtualProject {
21
- return {
22
- ...project,
23
- files: new Map(project.files),
24
- };
25
- }
26
-
27
20
  export function useEditSession(
28
21
  options: UseEditSessionOptions,
29
22
  ): EditSessionState & EditSessionActions {
30
23
  const {
31
- originalCode,
32
- originalProject: providedProject,
33
- initialActiveFile,
34
24
  compile,
35
25
  apiEndpoint,
36
26
  } = options;
37
27
 
38
- console.log(
39
- "[useEditSession] providedProject:",
40
- providedProject?.id,
41
- "files:",
42
- providedProject ? Array.from(providedProject.files.keys()) : "none",
43
- );
44
-
45
- const originalProject = useMemo(
46
- () => providedProject ?? createSingleFileProject(originalCode ?? ""),
47
- [providedProject, originalCode],
48
- );
49
-
50
- // Track the last project we synced from to detect reference changes
51
- const lastSyncedProjectRef = useRef<VirtualProject>(originalProject);
52
-
53
- const [project, setProject] = useState<VirtualProject>(originalProject);
54
- const [activeFile, setActiveFile] = useState(
55
- initialActiveFile && originalProject.files.has(initialActiveFile)
56
- ? initialActiveFile
57
- : originalProject.entry,
58
- );
59
- const [history, setHistory] = useState<EditHistoryEntry[]>([]);
28
+ const state = useProjectState(options);
60
29
  const [isApplying, setIsApplying] = useState(false);
61
30
  const [error, setError] = useState<string | null>(null);
62
31
  const [streamingNotes, setStreamingNotes] = useState<string[]>([]);
63
32
  const [pendingPrompt, setPendingPrompt] = useState<string | null>(null);
64
33
 
65
- // Sync state when the original project reference changes (new project or files loaded)
66
- useEffect(() => {
67
- if (originalProject !== lastSyncedProjectRef.current) {
68
- lastSyncedProjectRef.current = originalProject;
69
- setProject(originalProject);
70
- setActiveFile(
71
- initialActiveFile && originalProject.files.has(initialActiveFile)
72
- ? initialActiveFile
73
- : originalProject.entry,
74
- );
75
- setHistory([]);
76
- setError(null);
77
- setStreamingNotes([]);
78
- }
79
- }, [originalProject, initialActiveFile]);
80
-
81
34
  const performEdit = useCallback(
82
35
  async (
83
36
  currentCode: string,
@@ -122,11 +75,6 @@ export function useEditSession(
122
75
  [compile, apiEndpoint],
123
76
  );
124
77
 
125
- const currentCode = useMemo(
126
- () => project.files.get(activeFile)?.content ?? "",
127
- [project, activeFile],
128
- );
129
-
130
78
  const submitEdit = useCallback(
131
79
  async (prompt: string) => {
132
80
  if (!prompt.trim() || isApplying) return;
@@ -137,16 +85,16 @@ export function useEditSession(
137
85
  setPendingPrompt(prompt);
138
86
 
139
87
  try {
140
- const result = await performEdit(currentCode, prompt);
141
- setProject((prev) => {
142
- const updated = cloneProject(prev);
143
- const file = updated.files.get(activeFile);
88
+ const result = await performEdit(state.currentCode, prompt);
89
+ state.setProject((prev: VirtualProject) => {
90
+ const updated = { ...prev, files: new Map(prev.files) };
91
+ const file = updated.files.get(state.activeFile);
144
92
  if (file) {
145
- updated.files.set(activeFile, { ...file, content: result.newCode });
93
+ updated.files.set(state.activeFile, { ...file, content: result.newCode });
146
94
  }
147
95
  return updated;
148
96
  });
149
- setHistory((prev) => [...prev, ...result.entries]);
97
+ state.setHistory((prev: EditHistoryEntry[]) => [...prev, ...result.entries]);
150
98
  } catch (err) {
151
99
  setError(err instanceof Error ? err.message : "Edit failed");
152
100
  } finally {
@@ -155,45 +103,7 @@ export function useEditSession(
155
103
  setPendingPrompt(null);
156
104
  }
157
105
  },
158
- [currentCode, activeFile, isApplying, performEdit],
159
- );
160
-
161
- const revert = useCallback(() => {
162
- setProject(originalProject);
163
- setActiveFile(originalProject.entry);
164
- setHistory([]);
165
- setError(null);
166
- setStreamingNotes([]);
167
- }, [originalProject]);
168
-
169
- const updateActiveFile = useCallback(
170
- (content: string) => {
171
- setProject((prev) => {
172
- const updated = cloneProject(prev);
173
- const file = updated.files.get(activeFile);
174
- if (file) {
175
- updated.files.set(activeFile, { ...file, content });
176
- }
177
- return updated;
178
- });
179
- },
180
- [activeFile],
181
- );
182
-
183
- const replaceFile = useCallback(
184
- (path: string, content: string, encoding: "utf8" | "base64" = "utf8") => {
185
- setProject((prev) => {
186
- const updated = cloneProject(prev);
187
- const file = updated.files.get(path);
188
- if (file) {
189
- updated.files.set(path, { ...file, content, encoding });
190
- } else {
191
- updated.files.set(path, { path, content, encoding });
192
- }
193
- return updated;
194
- });
195
- },
196
- [],
106
+ [state.currentCode, state.activeFile, state.setProject, state.setHistory, isApplying, performEdit],
197
107
  );
198
108
 
199
109
  const clearError = useCallback(() => {
@@ -201,19 +111,19 @@ export function useEditSession(
201
111
  }, []);
202
112
 
203
113
  return {
204
- project,
205
- originalProject,
206
- activeFile,
207
- history,
114
+ project: state.project,
115
+ originalProject: state.originalProject,
116
+ activeFile: state.activeFile,
117
+ history: state.history,
208
118
  isApplying,
209
119
  error,
210
120
  streamingNotes,
211
121
  pendingPrompt,
212
122
  submitEdit,
213
- revert,
214
- updateActiveFile,
215
- setActiveFile,
123
+ revert: state.revert,
124
+ updateActiveFile: state.updateActiveFile,
125
+ setActiveFile: state.setActiveFile,
216
126
  clearError,
217
- replaceFile,
127
+ replaceFile: state.replaceFile,
218
128
  };
219
129
  }
@@ -0,0 +1,122 @@
1
+ import { createSingleFileProject } from "@aprovan/patchwork-compiler";
2
+ import { useState, useCallback, useMemo, useEffect, useRef } from "react";
3
+ import type {
4
+ EditHistoryEntry,
5
+ EditSessionState,
6
+ } from "./types";
7
+ import type { VirtualProject } from "@aprovan/patchwork-compiler";
8
+
9
+ export interface UseProjectStateOptions {
10
+ originalCode?: string;
11
+ originalProject?: VirtualProject;
12
+ initialActiveFile?: string;
13
+ }
14
+
15
+ function cloneProject(project: VirtualProject): VirtualProject {
16
+ return {
17
+ ...project,
18
+ files: new Map(project.files),
19
+ };
20
+ }
21
+
22
+ export function useProjectState(
23
+ options: UseProjectStateOptions,
24
+ ): Omit<EditSessionState, "isApplying" | "error" | "streamingNotes" | "pendingPrompt"> & {
25
+ setProject: React.Dispatch<React.SetStateAction<VirtualProject>>;
26
+ setActiveFile: React.Dispatch<React.SetStateAction<string>>;
27
+ history: EditHistoryEntry[];
28
+ setHistory: React.Dispatch<React.SetStateAction<EditHistoryEntry[]>>;
29
+ revert: () => void;
30
+ updateActiveFile: (content: string) => void;
31
+ replaceFile: (path: string, content: string, encoding?: "utf8" | "base64") => void;
32
+ currentCode: string;
33
+ } {
34
+ const {
35
+ originalCode,
36
+ originalProject: providedProject,
37
+ initialActiveFile,
38
+ } = options;
39
+
40
+ const originalProject = useMemo(
41
+ () => providedProject ?? createSingleFileProject(originalCode ?? ""),
42
+ [providedProject, originalCode],
43
+ );
44
+
45
+ const lastSyncedProjectRef = useRef<VirtualProject>(originalProject);
46
+
47
+ const [project, setProject] = useState<VirtualProject>(originalProject);
48
+ const [activeFile, setActiveFile] = useState(
49
+ initialActiveFile && originalProject.files.has(initialActiveFile)
50
+ ? initialActiveFile
51
+ : originalProject.entry,
52
+ );
53
+ const [history, setHistory] = useState<EditHistoryEntry[]>([]);
54
+
55
+ useEffect(() => {
56
+ if (originalProject !== lastSyncedProjectRef.current) {
57
+ lastSyncedProjectRef.current = originalProject;
58
+ setProject(originalProject);
59
+ setActiveFile(
60
+ initialActiveFile && originalProject.files.has(initialActiveFile)
61
+ ? initialActiveFile
62
+ : originalProject.entry,
63
+ );
64
+ setHistory([]);
65
+ }
66
+ }, [originalProject, initialActiveFile]);
67
+
68
+ const currentCode = useMemo(
69
+ () => project.files.get(activeFile)?.content ?? "",
70
+ [project, activeFile],
71
+ );
72
+
73
+ const revert = useCallback(() => {
74
+ setProject(originalProject);
75
+ setActiveFile(originalProject.entry);
76
+ setHistory([]);
77
+ }, [originalProject]);
78
+
79
+ const updateActiveFile = useCallback(
80
+ (content: string) => {
81
+ setProject((prev: VirtualProject) => {
82
+ const updated = cloneProject(prev);
83
+ const file = updated.files.get(activeFile);
84
+ if (file) {
85
+ updated.files.set(activeFile, { ...file, content });
86
+ }
87
+ return updated;
88
+ });
89
+ },
90
+ [activeFile],
91
+ );
92
+
93
+ const replaceFile = useCallback(
94
+ (path: string, content: string, encoding: "utf8" | "base64" = "utf8") => {
95
+ setProject((prev: VirtualProject) => {
96
+ const updated = cloneProject(prev);
97
+ const file = updated.files.get(path);
98
+ if (file) {
99
+ updated.files.set(path, { ...file, content, encoding });
100
+ } else {
101
+ updated.files.set(path, { path, content, encoding });
102
+ }
103
+ return updated;
104
+ });
105
+ },
106
+ [],
107
+ );
108
+
109
+ return {
110
+ project,
111
+ originalProject,
112
+ activeFile,
113
+ setActiveFile,
114
+ history,
115
+ setHistory,
116
+ setProject,
117
+ revert,
118
+ updateActiveFile,
119
+ replaceFile,
120
+ currentCode,
121
+ };
122
+ }
package/src/index.ts CHANGED
@@ -1,15 +1,11 @@
1
- // Components
2
- export {
3
- CodeBlockExtension,
4
- CodePreview,
5
- WidgetPreview,
6
- MarkdownEditor,
7
- MarkdownPreview,
8
- ServicesInspector,
9
- type ServiceInfo,
10
- } from "./components";
1
+ export { CodeBlockExtension } from "./components/CodeBlockExtension";
2
+ export { CodePreview } from "./components/CodePreview";
3
+ export { WidgetPreview } from "./components/WidgetPreview";
4
+ export { MarkdownEditor } from "./components/MarkdownEditor";
5
+ export { MarkdownPreview } from "./components/MarkdownPreview";
6
+ export { ServicesInspector, type ServiceInfo } from "./components/ServicesInspector";
11
7
 
12
- // Edit components
8
+ // Edit components — intentional public API boundary (see components/edit/index.ts)
13
9
  export {
14
10
  EditModal,
15
11
  EditHistory,
@@ -18,9 +14,11 @@ export {
18
14
  CodeBlockView,
19
15
  MediaPreview,
20
16
  useEditSession,
17
+ useProjectState,
21
18
  sendEditRequest,
22
19
  type EditModalProps,
23
20
  type UseEditSessionOptions,
21
+ type UseProjectStateOptions,
24
22
  type EditHistoryEntry,
25
23
  type EditSessionState,
26
24
  type EditSessionActions,
@@ -50,9 +48,7 @@ export {
50
48
  getMimeType,
51
49
  } from "./components/edit";
52
50
 
53
- // Lib utilities
54
51
  export {
55
- // Code extractor
56
52
  extractCodeBlocks,
57
53
  findFirstCodeBlock,
58
54
  hasCodeBlock,
@@ -62,8 +58,9 @@ export {
62
58
  type CodePart,
63
59
  type ParsedPart,
64
60
  type ExtractOptions,
61
+ } from "./lib/code-extractor";
65
62
 
66
- // Diff utilities
63
+ export {
67
64
  parseCodeBlockAttributes,
68
65
  parseCodeBlocks,
69
66
  findDiffMarkers,
@@ -78,8 +75,9 @@ export {
78
75
  type CodeBlock,
79
76
  type DiffBlock,
80
77
  type ParsedEditResponse,
78
+ } from "./lib/diff";
81
79
 
82
- // VFS utilities
80
+ export {
83
81
  getVFSConfig,
84
82
  getVFSStore,
85
83
  saveProject,
@@ -87,7 +85,6 @@ export {
87
85
  listProjects,
88
86
  saveFile,
89
87
  isVFSAvailable,
88
+ } from "./lib/vfs";
90
89
 
91
- // General utilities
92
- cn,
93
- } from "./lib";
90
+ export { cn } from "./lib/utils";
@@ -1,5 +1,5 @@
1
- import type { VirtualFile, VirtualProject } from '@aprovan/patchwork-compiler';
2
1
  import { createProjectFromFiles, detectMainFile } from '@aprovan/patchwork-compiler';
2
+ import type { VirtualFile, VirtualProject } from '@aprovan/patchwork-compiler';
3
3
 
4
4
  // Matches fenced code blocks with optional attributes: ```language attr="value"\n...content...```
5
5
  // Captures: [1] = language (optional), [2] = attributes (optional), [3] = content