@aprovan/patchwork-editor 0.1.2-dev.9d1cd22 → 0.1.2-dev.c1ac1dc

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.
@@ -10,4 +10,4 @@
10
10
  CLI Cleaning output folder
11
11
  ESM Build start
12
12
  ESM dist/index.js 104.93 KB
13
- ESM ⚡️ Build success in 266ms
13
+ ESM ⚡️ Build success in 450ms
@@ -1,5 +1,4 @@
1
- import type { Compiler } from '@aprovan/patchwork-compiler';
2
- import type { VirtualProject } from '@aprovan/patchwork-compiler';
1
+ import type { Compiler, VirtualProject } from '@aprovan/patchwork-compiler';
3
2
  interface CodePreviewProps {
4
3
  code: string;
5
4
  compiler: Compiler | null;
@@ -18,5 +17,5 @@ interface CodePreviewProps {
18
17
  initialProject: VirtualProject;
19
18
  }) => void;
20
19
  }
21
- export declare function CodePreview({ code: originalCode, compiler, services, filePath, entrypoint, onOpenEditSession, }: CodePreviewProps): import("react/jsx-runtime").JSX.Element;
20
+ export declare function CodePreview({ code: originalCode, compiler, services, filePath, entrypoint, onOpenEditSession, }: CodePreviewProps): import("react").JSX.Element;
22
21
  export {};
@@ -6,5 +6,5 @@ interface MarkdownEditorProps {
6
6
  disabled?: boolean;
7
7
  className?: string;
8
8
  }
9
- export declare function MarkdownEditor({ value, onChange, onSubmit, placeholder, disabled, className, }: MarkdownEditorProps): import("react/jsx-runtime").JSX.Element;
9
+ export declare function MarkdownEditor({ value, onChange, onSubmit, placeholder, disabled, className, }: MarkdownEditorProps): import("react").JSX.Element;
10
10
  export {};
@@ -4,5 +4,5 @@ interface MarkdownPreviewProps {
4
4
  editable?: boolean;
5
5
  className?: string;
6
6
  }
7
- export declare function MarkdownPreview({ value, onChange, editable, className, }: MarkdownPreviewProps): import("react/jsx-runtime").JSX.Element;
7
+ export declare function MarkdownPreview({ value, onChange, editable, className, }: MarkdownPreviewProps): import("react").JSX.Element;
8
8
  export {};
@@ -5,5 +5,5 @@ interface SaveStatusButtonProps {
5
5
  disabled?: boolean;
6
6
  tone: 'muted' | 'primary';
7
7
  }
8
- export declare function SaveStatusButton({ status, onClick, disabled, tone, }: SaveStatusButtonProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function SaveStatusButton({ status, onClick, disabled, tone, }: SaveStatusButtonProps): import("react").JSX.Element;
9
9
  export {};
@@ -44,5 +44,5 @@ interface ServicesInspectorProps {
44
44
  onClose?: () => void;
45
45
  }>;
46
46
  }
47
- export declare function ServicesInspector({ namespaces, services, BadgeComponent, DialogComponent, DialogHeaderComponent, DialogContentComponent, DialogCloseComponent, }: ServicesInspectorProps): import("react/jsx-runtime").JSX.Element;
47
+ export declare function ServicesInspector({ namespaces, services, BadgeComponent, DialogComponent, DialogHeaderComponent, DialogContentComponent, DialogCloseComponent, }: ServicesInspectorProps): import("react").JSX.Element;
48
48
  export {};
@@ -5,4 +5,4 @@ export interface WidgetPreviewProps {
5
5
  services?: string[];
6
6
  enabled?: boolean;
7
7
  }
8
- export declare function WidgetPreview({ code, compiler, services, enabled, }: WidgetPreviewProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function WidgetPreview({ code, compiler, services, enabled, }: WidgetPreviewProps): import("react").JSX.Element;
@@ -4,4 +4,4 @@ export interface CodeBlockViewProps {
4
4
  editable?: boolean;
5
5
  onChange?: (content: string) => void;
6
6
  }
7
- export declare function CodeBlockView({ content, language, editable, onChange }: CodeBlockViewProps): import("react/jsx-runtime").JSX.Element;
7
+ export declare function CodeBlockView({ content, language, editable, onChange }: CodeBlockViewProps): import("react").JSX.Element;
@@ -6,5 +6,5 @@ interface EditHistoryProps {
6
6
  pendingPrompt?: string | null;
7
7
  className?: string;
8
8
  }
9
- export declare function EditHistory({ entries, streamingNotes, isStreaming, pendingPrompt, className, }: EditHistoryProps): import("react/jsx-runtime").JSX.Element;
9
+ export declare function EditHistory({ entries, streamingNotes, isStreaming, pendingPrompt, className, }: EditHistoryProps): import("react").JSX.Element;
10
10
  export {};
@@ -18,4 +18,4 @@ export interface EditModalProps extends UseEditSessionOptions {
18
18
  previewError?: string | null;
19
19
  previewLoading?: boolean;
20
20
  }
21
- export declare function EditModal({ isOpen, onClose, onSave, onSaveProject, renderPreview, renderLoading, renderError, previewError, previewLoading, initialTreePath, initialState, hideFileTree, ...sessionOptions }: EditModalProps): import("react/jsx-runtime").JSX.Element;
21
+ export declare function EditModal({ isOpen, onClose, onSave, onSaveProject, renderPreview, renderLoading, renderError, previewError, previewLoading, initialTreePath, initialState, hideFileTree, ...sessionOptions }: EditModalProps): import("react").JSX.Element;
@@ -24,4 +24,4 @@ export interface FileTreeProps {
24
24
  pageSize?: number;
25
25
  reloadToken?: number;
26
26
  }
27
- export declare function FileTree({ files, activeFile, activePath, title, onSelectFile, onSelectDirectory, onReplaceFile, onOpenInEditor, openInEditorMode, openInEditorIcon, openInEditorTitle, pinnedPaths, onTogglePin, directoryLoader, pageSize, reloadToken, }: FileTreeProps): import("react/jsx-runtime").JSX.Element;
27
+ export declare function FileTree({ files, activeFile, activePath, title, onSelectFile, onSelectDirectory, onReplaceFile, onOpenInEditor, openInEditorMode, openInEditorIcon, openInEditorTitle, pinnedPaths, onTogglePin, directoryLoader, pageSize, reloadToken, }: FileTreeProps): import("react").JSX.Element;
@@ -3,4 +3,4 @@ export interface MediaPreviewProps {
3
3
  mimeType: string;
4
4
  fileName: string;
5
5
  }
6
- export declare function MediaPreview({ content, mimeType, fileName }: MediaPreviewProps): import("react/jsx-runtime").JSX.Element;
6
+ export declare function MediaPreview({ content, mimeType, fileName }: MediaPreviewProps): import("react").JSX.Element;
@@ -6,4 +6,4 @@ export interface SaveConfirmDialogProps {
6
6
  onDiscard: () => void;
7
7
  onCancel: () => void;
8
8
  }
9
- export declare function SaveConfirmDialog({ isOpen, isSaving, error, onSave, onDiscard, onCancel, }: SaveConfirmDialogProps): import("react/jsx-runtime").JSX.Element;
9
+ export declare function SaveConfirmDialog({ isOpen, isSaving, error, onSave, onDiscard, onCancel, }: SaveConfirmDialogProps): import("react").JSX.Element;
@@ -1,5 +1,5 @@
1
- import type { VirtualProject } from "@aprovan/patchwork-compiler";
2
1
  import type { EditSessionState, EditSessionActions, CompileFn } from "./types";
2
+ import type { VirtualProject } from "@aprovan/patchwork-compiler";
3
3
  export interface UseEditSessionOptions {
4
4
  originalCode?: string;
5
5
  originalProject?: VirtualProject;
package/dist/index.js CHANGED
@@ -2,17 +2,17 @@ import { Node, textblockTypeInputRule } from '@tiptap/core';
2
2
  import { ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent, useEditor, EditorContent } from '@tiptap/react';
3
3
  import { useRef, useCallback, useMemo, useState, useEffect } from 'react';
4
4
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
+ import { HttpBackend, VFSStore, createSingleFileProject, detectMainFile, createProjectFromFiles } from '@aprovan/patchwork-compiler';
5
6
  import { Folder, Pin, X, Loader2, AlertCircle, FileImage, FileVideo, Pencil, RotateCcw, FileCode, FolderTree, Eye, Code, Send, MessageSquare, Server, ChevronDown, File, PinOff, ChevronRight, ChevronsDown, Upload, AlertTriangle, Save } from 'lucide-react';
6
- import { createSingleFileProject, HttpBackend, VFSStore, detectMainFile, createProjectFromFiles } from '@aprovan/patchwork-compiler';
7
7
  import Markdown from 'react-markdown';
8
8
  import remarkGfm from 'remark-gfm';
9
- import StarterKit from '@tiptap/starter-kit';
9
+ import { Bobbin, serializeChangesToYAML } from '@aprovan/bobbin';
10
10
  import Placeholder from '@tiptap/extension-placeholder';
11
11
  import Typography from '@tiptap/extension-typography';
12
- import { Markdown as Markdown$1 } from 'tiptap-markdown';
13
12
  import { TextSelection } from '@tiptap/pm/state';
13
+ import StarterKit from '@tiptap/starter-kit';
14
+ import { Markdown as Markdown$1 } from 'tiptap-markdown';
14
15
  import { createHighlighter } from 'shiki';
15
- import { Bobbin, serializeChangesToYAML } from '@aprovan/bobbin';
16
16
  import { clsx } from 'clsx';
17
17
  import { twMerge } from 'tailwind-merge';
18
18
 
@@ -180,6 +180,69 @@ var CodeBlockExtension = Node.create({
180
180
  };
181
181
  }
182
182
  });
183
+ var VFS_BASE_URL = "/vfs";
184
+ var vfsConfigCache = null;
185
+ async function getVFSConfig() {
186
+ if (vfsConfigCache) return vfsConfigCache;
187
+ try {
188
+ const res = await fetch(`${VFS_BASE_URL}/config`);
189
+ if (res.ok) {
190
+ vfsConfigCache = await res.json();
191
+ return vfsConfigCache;
192
+ }
193
+ } catch {
194
+ }
195
+ return { usePaths: false };
196
+ }
197
+ var storeInstance = null;
198
+ function getVFSStore() {
199
+ if (!storeInstance) {
200
+ const provider = new HttpBackend({ baseUrl: VFS_BASE_URL });
201
+ storeInstance = new VFSStore(provider, {
202
+ sync: true,
203
+ conflictStrategy: "local-wins"
204
+ });
205
+ }
206
+ return storeInstance;
207
+ }
208
+ async function saveProject(project) {
209
+ const store = getVFSStore();
210
+ await store.saveProject(project);
211
+ }
212
+ async function loadProject(id) {
213
+ const store = getVFSStore();
214
+ return store.loadProject(id);
215
+ }
216
+ async function listProjects() {
217
+ const store = getVFSStore();
218
+ const files = await store.listFiles();
219
+ const projectIds = /* @__PURE__ */ new Set();
220
+ for (const file of files) {
221
+ const id = file.split("/")[0];
222
+ if (id) projectIds.add(id);
223
+ }
224
+ return Array.from(projectIds);
225
+ }
226
+ async function saveFile(path, content) {
227
+ const store = getVFSStore();
228
+ await store.writeFile(path, content);
229
+ }
230
+ async function loadFile(path, encoding) {
231
+ const store = getVFSStore();
232
+ return store.readFile(path, encoding);
233
+ }
234
+ function subscribeToChanges(callback) {
235
+ const store = getVFSStore();
236
+ return store.on("change", callback);
237
+ }
238
+ async function isVFSAvailable() {
239
+ try {
240
+ const res = await fetch(VFS_BASE_URL, { method: "HEAD" });
241
+ return res.ok || res.status === 404;
242
+ } catch {
243
+ return false;
244
+ }
245
+ }
183
246
 
184
247
  // src/components/edit/types.ts
185
248
  function getActiveContent(state) {
@@ -1010,6 +1073,154 @@ function SaveStatusButton({
1010
1073
  }
1011
1074
  );
1012
1075
  }
1076
+ var highlighterPromise = null;
1077
+ var COMMON_LANGUAGES = [
1078
+ "typescript",
1079
+ "javascript",
1080
+ "tsx",
1081
+ "jsx",
1082
+ "json",
1083
+ "html",
1084
+ "css",
1085
+ "markdown",
1086
+ "yaml",
1087
+ "python",
1088
+ "bash",
1089
+ "sql"
1090
+ ];
1091
+ function getHighlighter() {
1092
+ if (!highlighterPromise) {
1093
+ highlighterPromise = createHighlighter({
1094
+ themes: ["github-light"],
1095
+ langs: COMMON_LANGUAGES
1096
+ });
1097
+ }
1098
+ return highlighterPromise;
1099
+ }
1100
+ function normalizeLanguage(lang) {
1101
+ if (!lang) return "typescript";
1102
+ const normalized = lang.toLowerCase();
1103
+ const mapping = {
1104
+ ts: "typescript",
1105
+ tsx: "tsx",
1106
+ js: "javascript",
1107
+ jsx: "jsx",
1108
+ json: "json",
1109
+ html: "html",
1110
+ css: "css",
1111
+ md: "markdown",
1112
+ markdown: "markdown",
1113
+ yml: "yaml",
1114
+ yaml: "yaml",
1115
+ py: "python",
1116
+ python: "python",
1117
+ sh: "bash",
1118
+ bash: "bash",
1119
+ sql: "sql",
1120
+ typescript: "typescript",
1121
+ javascript: "javascript"
1122
+ };
1123
+ return mapping[normalized] || "typescript";
1124
+ }
1125
+ function CodeBlockView({ content, language, editable = false, onChange }) {
1126
+ const textareaRef = useRef(null);
1127
+ const containerRef = useRef(null);
1128
+ const [highlighter, setHighlighter] = useState(null);
1129
+ useEffect(() => {
1130
+ let mounted = true;
1131
+ getHighlighter().then((h) => {
1132
+ if (mounted) setHighlighter(h);
1133
+ });
1134
+ return () => {
1135
+ mounted = false;
1136
+ };
1137
+ }, []);
1138
+ useEffect(() => {
1139
+ if (textareaRef.current) {
1140
+ textareaRef.current.style.height = "auto";
1141
+ textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
1142
+ }
1143
+ }, [content]);
1144
+ const handleChange = useCallback(
1145
+ (e) => {
1146
+ onChange?.(e.target.value);
1147
+ },
1148
+ [onChange]
1149
+ );
1150
+ const handleKeyDown = useCallback(
1151
+ (e) => {
1152
+ if (e.key === "Tab") {
1153
+ e.preventDefault();
1154
+ const target = e.target;
1155
+ const start = target.selectionStart;
1156
+ const end = target.selectionEnd;
1157
+ const value = target.value;
1158
+ const newValue = value.substring(0, start) + " " + value.substring(end);
1159
+ onChange?.(newValue);
1160
+ requestAnimationFrame(() => {
1161
+ target.selectionStart = target.selectionEnd = start + 2;
1162
+ });
1163
+ }
1164
+ },
1165
+ [onChange]
1166
+ );
1167
+ const langLabel = language || "text";
1168
+ const shikiLang = useMemo(() => normalizeLanguage(language), [language]);
1169
+ const highlightedHtml = useMemo(() => {
1170
+ if (!highlighter) return null;
1171
+ try {
1172
+ return highlighter.codeToHtml(content, {
1173
+ lang: shikiLang,
1174
+ theme: "github-light"
1175
+ });
1176
+ } catch {
1177
+ return null;
1178
+ }
1179
+ }, [highlighter, content, shikiLang]);
1180
+ return /* @__PURE__ */ jsxs("div", { className: "h-full flex flex-col bg-[#ffffff]", children: [
1181
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between px-4 py-2 bg-[#f6f8fa] border-b border-[#d0d7de] text-xs", children: /* @__PURE__ */ jsx("span", { className: "font-mono text-[#57606a]", children: langLabel }) }),
1182
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto overflow-x-hidden", children: editable ? /* @__PURE__ */ jsxs("div", { className: "relative min-h-full", children: [
1183
+ /* @__PURE__ */ jsx(
1184
+ "div",
1185
+ {
1186
+ ref: containerRef,
1187
+ className: "absolute top-0 left-0 right-0 pointer-events-none p-4",
1188
+ "aria-hidden": "true",
1189
+ children: highlightedHtml ? /* @__PURE__ */ jsx(
1190
+ "div",
1191
+ {
1192
+ className: "highlighted-code font-mono text-xs leading-relaxed whitespace-pre-wrap break-words [&_pre]:!bg-transparent [&_pre]:!m-0 [&_pre]:!p-0 [&_pre]:whitespace-pre-wrap [&_code]:!bg-transparent [&_code]:whitespace-pre-wrap [&_code]:break-words",
1193
+ dangerouslySetInnerHTML: { __html: highlightedHtml }
1194
+ }
1195
+ ) : /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono whitespace-pre-wrap break-words text-[#24292f] m-0 leading-relaxed", children: /* @__PURE__ */ jsx("code", { children: content }) })
1196
+ }
1197
+ ),
1198
+ /* @__PURE__ */ jsx(
1199
+ "textarea",
1200
+ {
1201
+ ref: textareaRef,
1202
+ value: content,
1203
+ onChange: handleChange,
1204
+ onKeyDown: handleKeyDown,
1205
+ className: "relative w-full min-h-full font-mono text-xs leading-relaxed bg-transparent border-none outline-none resize-none p-4 text-transparent whitespace-pre-wrap break-words",
1206
+ spellCheck: false,
1207
+ style: {
1208
+ tabSize: 2,
1209
+ caretColor: "#24292f",
1210
+ wordBreak: "break-word",
1211
+ overflowWrap: "break-word"
1212
+ }
1213
+ }
1214
+ )
1215
+ ] }) : /* @__PURE__ */ jsx("div", { className: "p-4", children: highlightedHtml ? /* @__PURE__ */ jsx(
1216
+ "div",
1217
+ {
1218
+ className: "highlighted-code font-mono text-xs leading-relaxed whitespace-pre-wrap break-words [&_pre]:!bg-transparent [&_pre]:!m-0 [&_pre]:!p-0 [&_pre]:whitespace-pre-wrap [&_code]:!bg-transparent [&_code]:whitespace-pre-wrap [&_code]:break-words",
1219
+ dangerouslySetInnerHTML: { __html: highlightedHtml }
1220
+ }
1221
+ ) : /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono whitespace-pre-wrap break-words m-0 leading-relaxed text-[#24292f]", children: /* @__PURE__ */ jsx("code", { children: content }) }) }) })
1222
+ ] });
1223
+ }
1013
1224
 
1014
1225
  // src/components/edit/fileTypes.ts
1015
1226
  var COMPILABLE_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"];
@@ -1693,203 +1904,6 @@ function FileTree({
1693
1904
  ) })
1694
1905
  ] });
1695
1906
  }
1696
- function SaveConfirmDialog({
1697
- isOpen,
1698
- isSaving,
1699
- error,
1700
- onSave,
1701
- onDiscard,
1702
- onCancel
1703
- }) {
1704
- if (!isOpen) return null;
1705
- return /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-[60] flex items-center justify-center bg-black/80", children: /* @__PURE__ */ jsxs("div", { className: "bg-background border rounded-lg shadow-lg w-full max-w-md", children: [
1706
- /* @__PURE__ */ jsxs("div", { className: "p-6", children: [
1707
- /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold leading-none tracking-tight", children: "Unsaved Changes" }),
1708
- /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mt-2", children: "You have unsaved changes. Would you like to save them before closing?" }),
1709
- error && /* @__PURE__ */ jsxs("p", { className: "text-sm text-destructive mt-3", children: [
1710
- "Save failed: ",
1711
- error
1712
- ] })
1713
- ] }),
1714
- /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2 p-6 pt-0", children: [
1715
- /* @__PURE__ */ jsx(
1716
- "button",
1717
- {
1718
- onClick: onCancel,
1719
- disabled: isSaving,
1720
- className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 py-2 border border-input bg-background hover:bg-accent hover:text-accent-foreground disabled:opacity-50",
1721
- children: "Cancel"
1722
- }
1723
- ),
1724
- /* @__PURE__ */ jsx(
1725
- "button",
1726
- {
1727
- onClick: onDiscard,
1728
- disabled: isSaving,
1729
- className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 py-2 border border-input bg-background text-destructive hover:bg-destructive/10 disabled:opacity-50",
1730
- children: "Discard"
1731
- }
1732
- ),
1733
- /* @__PURE__ */ jsx(
1734
- "button",
1735
- {
1736
- onClick: onSave,
1737
- disabled: isSaving,
1738
- className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 py-2 bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50",
1739
- children: isSaving ? "Saving..." : "Save"
1740
- }
1741
- )
1742
- ] })
1743
- ] }) });
1744
- }
1745
- var highlighterPromise = null;
1746
- var COMMON_LANGUAGES = [
1747
- "typescript",
1748
- "javascript",
1749
- "tsx",
1750
- "jsx",
1751
- "json",
1752
- "html",
1753
- "css",
1754
- "markdown",
1755
- "yaml",
1756
- "python",
1757
- "bash",
1758
- "sql"
1759
- ];
1760
- function getHighlighter() {
1761
- if (!highlighterPromise) {
1762
- highlighterPromise = createHighlighter({
1763
- themes: ["github-light"],
1764
- langs: COMMON_LANGUAGES
1765
- });
1766
- }
1767
- return highlighterPromise;
1768
- }
1769
- function normalizeLanguage(lang) {
1770
- if (!lang) return "typescript";
1771
- const normalized = lang.toLowerCase();
1772
- const mapping = {
1773
- ts: "typescript",
1774
- tsx: "tsx",
1775
- js: "javascript",
1776
- jsx: "jsx",
1777
- json: "json",
1778
- html: "html",
1779
- css: "css",
1780
- md: "markdown",
1781
- markdown: "markdown",
1782
- yml: "yaml",
1783
- yaml: "yaml",
1784
- py: "python",
1785
- python: "python",
1786
- sh: "bash",
1787
- bash: "bash",
1788
- sql: "sql",
1789
- typescript: "typescript",
1790
- javascript: "javascript"
1791
- };
1792
- return mapping[normalized] || "typescript";
1793
- }
1794
- function CodeBlockView({ content, language, editable = false, onChange }) {
1795
- const textareaRef = useRef(null);
1796
- const containerRef = useRef(null);
1797
- const [highlighter, setHighlighter] = useState(null);
1798
- useEffect(() => {
1799
- let mounted = true;
1800
- getHighlighter().then((h) => {
1801
- if (mounted) setHighlighter(h);
1802
- });
1803
- return () => {
1804
- mounted = false;
1805
- };
1806
- }, []);
1807
- useEffect(() => {
1808
- if (textareaRef.current) {
1809
- textareaRef.current.style.height = "auto";
1810
- textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
1811
- }
1812
- }, [content]);
1813
- const handleChange = useCallback(
1814
- (e) => {
1815
- onChange?.(e.target.value);
1816
- },
1817
- [onChange]
1818
- );
1819
- const handleKeyDown = useCallback(
1820
- (e) => {
1821
- if (e.key === "Tab") {
1822
- e.preventDefault();
1823
- const target = e.target;
1824
- const start = target.selectionStart;
1825
- const end = target.selectionEnd;
1826
- const value = target.value;
1827
- const newValue = value.substring(0, start) + " " + value.substring(end);
1828
- onChange?.(newValue);
1829
- requestAnimationFrame(() => {
1830
- target.selectionStart = target.selectionEnd = start + 2;
1831
- });
1832
- }
1833
- },
1834
- [onChange]
1835
- );
1836
- const langLabel = language || "text";
1837
- const shikiLang = useMemo(() => normalizeLanguage(language), [language]);
1838
- const highlightedHtml = useMemo(() => {
1839
- if (!highlighter) return null;
1840
- try {
1841
- return highlighter.codeToHtml(content, {
1842
- lang: shikiLang,
1843
- theme: "github-light"
1844
- });
1845
- } catch {
1846
- return null;
1847
- }
1848
- }, [highlighter, content, shikiLang]);
1849
- return /* @__PURE__ */ jsxs("div", { className: "h-full flex flex-col bg-[#ffffff]", children: [
1850
- /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between px-4 py-2 bg-[#f6f8fa] border-b border-[#d0d7de] text-xs", children: /* @__PURE__ */ jsx("span", { className: "font-mono text-[#57606a]", children: langLabel }) }),
1851
- /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto overflow-x-hidden", children: editable ? /* @__PURE__ */ jsxs("div", { className: "relative min-h-full", children: [
1852
- /* @__PURE__ */ jsx(
1853
- "div",
1854
- {
1855
- ref: containerRef,
1856
- className: "absolute top-0 left-0 right-0 pointer-events-none p-4",
1857
- "aria-hidden": "true",
1858
- children: highlightedHtml ? /* @__PURE__ */ jsx(
1859
- "div",
1860
- {
1861
- className: "highlighted-code font-mono text-xs leading-relaxed whitespace-pre-wrap break-words [&_pre]:!bg-transparent [&_pre]:!m-0 [&_pre]:!p-0 [&_pre]:whitespace-pre-wrap [&_code]:!bg-transparent [&_code]:whitespace-pre-wrap [&_code]:break-words",
1862
- dangerouslySetInnerHTML: { __html: highlightedHtml }
1863
- }
1864
- ) : /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono whitespace-pre-wrap break-words text-[#24292f] m-0 leading-relaxed", children: /* @__PURE__ */ jsx("code", { children: content }) })
1865
- }
1866
- ),
1867
- /* @__PURE__ */ jsx(
1868
- "textarea",
1869
- {
1870
- ref: textareaRef,
1871
- value: content,
1872
- onChange: handleChange,
1873
- onKeyDown: handleKeyDown,
1874
- className: "relative w-full min-h-full font-mono text-xs leading-relaxed bg-transparent border-none outline-none resize-none p-4 text-transparent whitespace-pre-wrap break-words",
1875
- spellCheck: false,
1876
- style: {
1877
- tabSize: 2,
1878
- caretColor: "#24292f",
1879
- wordBreak: "break-word",
1880
- overflowWrap: "break-word"
1881
- }
1882
- }
1883
- )
1884
- ] }) : /* @__PURE__ */ jsx("div", { className: "p-4", children: highlightedHtml ? /* @__PURE__ */ jsx(
1885
- "div",
1886
- {
1887
- className: "highlighted-code font-mono text-xs leading-relaxed whitespace-pre-wrap break-words [&_pre]:!bg-transparent [&_pre]:!m-0 [&_pre]:!p-0 [&_pre]:whitespace-pre-wrap [&_code]:!bg-transparent [&_code]:whitespace-pre-wrap [&_code]:break-words",
1888
- dangerouslySetInnerHTML: { __html: highlightedHtml }
1889
- }
1890
- ) : /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono whitespace-pre-wrap break-words m-0 leading-relaxed text-[#24292f]", children: /* @__PURE__ */ jsx("code", { children: content }) }) }) })
1891
- ] });
1892
- }
1893
1907
  function formatFileSize(bytes) {
1894
1908
  if (bytes < 1024) return `${bytes} B`;
1895
1909
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -1980,6 +1994,55 @@ function MediaPreview({ content, mimeType, fileName }) {
1980
1994
  ] })
1981
1995
  ] });
1982
1996
  }
1997
+ function SaveConfirmDialog({
1998
+ isOpen,
1999
+ isSaving,
2000
+ error,
2001
+ onSave,
2002
+ onDiscard,
2003
+ onCancel
2004
+ }) {
2005
+ if (!isOpen) return null;
2006
+ return /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-[60] flex items-center justify-center bg-black/80", children: /* @__PURE__ */ jsxs("div", { className: "bg-background border rounded-lg shadow-lg w-full max-w-md", children: [
2007
+ /* @__PURE__ */ jsxs("div", { className: "p-6", children: [
2008
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold leading-none tracking-tight", children: "Unsaved Changes" }),
2009
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mt-2", children: "You have unsaved changes. Would you like to save them before closing?" }),
2010
+ error && /* @__PURE__ */ jsxs("p", { className: "text-sm text-destructive mt-3", children: [
2011
+ "Save failed: ",
2012
+ error
2013
+ ] })
2014
+ ] }),
2015
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2 p-6 pt-0", children: [
2016
+ /* @__PURE__ */ jsx(
2017
+ "button",
2018
+ {
2019
+ onClick: onCancel,
2020
+ disabled: isSaving,
2021
+ className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 py-2 border border-input bg-background hover:bg-accent hover:text-accent-foreground disabled:opacity-50",
2022
+ children: "Cancel"
2023
+ }
2024
+ ),
2025
+ /* @__PURE__ */ jsx(
2026
+ "button",
2027
+ {
2028
+ onClick: onDiscard,
2029
+ disabled: isSaving,
2030
+ className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 py-2 border border-input bg-background text-destructive hover:bg-destructive/10 disabled:opacity-50",
2031
+ children: "Discard"
2032
+ }
2033
+ ),
2034
+ /* @__PURE__ */ jsx(
2035
+ "button",
2036
+ {
2037
+ onClick: onSave,
2038
+ disabled: isSaving,
2039
+ className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 py-2 bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50",
2040
+ children: isSaving ? "Saving..." : "Save"
2041
+ }
2042
+ )
2043
+ ] })
2044
+ ] }) });
2045
+ }
1983
2046
  function hashCode(str) {
1984
2047
  let hash = 0;
1985
2048
  for (let i = 0; i < str.length; i++) {
@@ -2416,69 +2479,6 @@ function WidgetPreview({
2416
2479
  /* @__PURE__ */ jsx("div", { ref: containerRef, className: "w-full" })
2417
2480
  ] });
2418
2481
  }
2419
- var VFS_BASE_URL = "/vfs";
2420
- var vfsConfigCache = null;
2421
- async function getVFSConfig() {
2422
- if (vfsConfigCache) return vfsConfigCache;
2423
- try {
2424
- const res = await fetch(`${VFS_BASE_URL}/config`);
2425
- if (res.ok) {
2426
- vfsConfigCache = await res.json();
2427
- return vfsConfigCache;
2428
- }
2429
- } catch {
2430
- }
2431
- return { usePaths: false };
2432
- }
2433
- var storeInstance = null;
2434
- function getVFSStore() {
2435
- if (!storeInstance) {
2436
- const provider = new HttpBackend({ baseUrl: VFS_BASE_URL });
2437
- storeInstance = new VFSStore(provider, {
2438
- sync: true,
2439
- conflictStrategy: "local-wins"
2440
- });
2441
- }
2442
- return storeInstance;
2443
- }
2444
- async function saveProject(project) {
2445
- const store = getVFSStore();
2446
- await store.saveProject(project);
2447
- }
2448
- async function loadProject(id) {
2449
- const store = getVFSStore();
2450
- return store.loadProject(id);
2451
- }
2452
- async function listProjects() {
2453
- const store = getVFSStore();
2454
- const files = await store.listFiles();
2455
- const projectIds = /* @__PURE__ */ new Set();
2456
- for (const file of files) {
2457
- const id = file.split("/")[0];
2458
- if (id) projectIds.add(id);
2459
- }
2460
- return Array.from(projectIds);
2461
- }
2462
- async function saveFile(path, content) {
2463
- const store = getVFSStore();
2464
- await store.writeFile(path, content);
2465
- }
2466
- async function loadFile(path, encoding) {
2467
- const store = getVFSStore();
2468
- return store.readFile(path, encoding);
2469
- }
2470
- function subscribeToChanges(callback) {
2471
- const store = getVFSStore();
2472
- return store.on("change", callback);
2473
- }
2474
- async function isVFSAvailable() {
2475
- try {
2476
- const res = await fetch(VFS_BASE_URL, { method: "HEAD" });
2477
- return res.ok || res.status === 404;
2478
- } catch {
2479
- return false;
2480
- }
2481
- }
2482
2482
  function createManifest2(services) {
2483
2483
  return {
2484
2484
  name: "preview",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aprovan/patchwork-editor",
3
- "version": "0.1.2-dev.9d1cd22",
3
+ "version": "0.1.2-dev.c1ac1dc",
4
4
  "description": "Components for facilitating widget generation and editing",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,8 +25,8 @@
25
25
  "shiki": "^3.22.0",
26
26
  "tailwind-merge": "^3.4.0",
27
27
  "tiptap-markdown": "^0.9.0",
28
- "@aprovan/bobbin": "0.1.0-dev.9d1cd22",
29
- "@aprovan/patchwork-compiler": "0.1.2-dev.9d1cd22"
28
+ "@aprovan/bobbin": "0.1.0-dev.c1ac1dc",
29
+ "@aprovan/patchwork-compiler": "0.1.2-dev.c1ac1dc"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "react": "^18.0.0 || ^19.0.0",
@@ -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;
@@ -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,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,6 +1,5 @@
1
- import { useState, useCallback, useMemo, useEffect, useRef } from "react";
2
- import type { VirtualProject } from "@aprovan/patchwork-compiler";
3
1
  import { createSingleFileProject } from "@aprovan/patchwork-compiler";
2
+ import { useState, useCallback, useMemo, useEffect, useRef } from "react";
4
3
  import { sendEditRequest } from "./api";
5
4
  import type {
6
5
  EditHistoryEntry,
@@ -8,6 +7,7 @@ import type {
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;
@@ -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