@aprovan/patchwork-editor 0.1.1-dev.6bd527d → 0.1.2-dev.223e5f7

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.
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ 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 { AlertCircle, FileImage, FileVideo, Pencil, Loader2, RotateCcw, FileCode, FolderTree, Eye, Code, Save, X, Send, MessageSquare, Cloud, Check, Server, ChevronDown, ChevronRight, Folder, File, Upload } from 'lucide-react';
5
+ 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
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';
@@ -11,6 +11,7 @@ import Placeholder from '@tiptap/extension-placeholder';
11
11
  import Typography from '@tiptap/extension-typography';
12
12
  import { Markdown as Markdown$1 } from 'tiptap-markdown';
13
13
  import { TextSelection } from '@tiptap/pm/state';
14
+ import { createHighlighter } from 'shiki';
14
15
  import { Bobbin, serializeChangesToYAML } from '@aprovan/bobbin';
15
16
  import { clsx } from 'clsx';
16
17
  import { twMerge } from 'tailwind-merge';
@@ -370,7 +371,11 @@ async function sendEditRequest(request, options = {}) {
370
371
  }
371
372
  const text = await streamResponse(response, onProgress);
372
373
  if (!hasDiffBlocks(text)) {
373
- throw new Error("No valid diffs in response");
374
+ return {
375
+ newCode: request.code,
376
+ summary: text.trim(),
377
+ progressNotes: []
378
+ };
374
379
  }
375
380
  const parsed = parseEditResponse(text);
376
381
  const result = applyDiffs(request.code, parsed.diffs, { sanitize });
@@ -439,6 +444,7 @@ function useEditSession(options) {
439
444
  const {
440
445
  originalCode,
441
446
  originalProject: providedProject,
447
+ initialActiveFile,
442
448
  compile,
443
449
  apiEndpoint
444
450
  } = options;
@@ -454,7 +460,9 @@ function useEditSession(options) {
454
460
  );
455
461
  const lastSyncedProjectRef = useRef(originalProject);
456
462
  const [project, setProject] = useState(originalProject);
457
- const [activeFile, setActiveFile] = useState(originalProject.entry);
463
+ const [activeFile, setActiveFile] = useState(
464
+ initialActiveFile && originalProject.files.has(initialActiveFile) ? initialActiveFile : originalProject.entry
465
+ );
458
466
  const [history, setHistory] = useState([]);
459
467
  const [isApplying, setIsApplying] = useState(false);
460
468
  const [error, setError] = useState(null);
@@ -464,12 +472,14 @@ function useEditSession(options) {
464
472
  if (originalProject !== lastSyncedProjectRef.current) {
465
473
  lastSyncedProjectRef.current = originalProject;
466
474
  setProject(originalProject);
467
- setActiveFile(originalProject.entry);
475
+ setActiveFile(
476
+ initialActiveFile && originalProject.files.has(initialActiveFile) ? initialActiveFile : originalProject.entry
477
+ );
468
478
  setHistory([]);
469
479
  setError(null);
470
480
  setStreamingNotes([]);
471
481
  }
472
- }, [originalProject]);
482
+ }, [originalProject, initialActiveFile]);
473
483
  const performEdit = useCallback(
474
484
  async (currentCode2, prompt, isRetry = false) => {
475
485
  const entries = [];
@@ -850,11 +860,181 @@ function MarkdownEditor({
850
860
  }
851
861
  );
852
862
  }
863
+ function parseFrontmatter(content) {
864
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
865
+ if (!match) return { frontmatter: "", body: content };
866
+ return { frontmatter: match[1], body: match[2] };
867
+ }
868
+ function assembleFrontmatter(frontmatter, body) {
869
+ if (!frontmatter.trim()) return body;
870
+ return `---
871
+ ${frontmatter}
872
+ ---
873
+ ${body}`;
874
+ }
875
+ function MarkdownPreview({
876
+ value,
877
+ onChange,
878
+ editable = false,
879
+ className = ""
880
+ }) {
881
+ const { frontmatter, body } = parseFrontmatter(value);
882
+ const [fm, setFm] = useState(frontmatter);
883
+ const fmRef = useRef(frontmatter);
884
+ const bodyRef = useRef(body);
885
+ const textareaRef = useRef(null);
886
+ useEffect(() => {
887
+ const parsed = parseFrontmatter(value);
888
+ fmRef.current = parsed.frontmatter;
889
+ bodyRef.current = parsed.body;
890
+ setFm(parsed.frontmatter);
891
+ }, [value]);
892
+ const emitChange = useCallback(
893
+ (newFm, newBody) => {
894
+ onChange?.(assembleFrontmatter(newFm, newBody));
895
+ },
896
+ [onChange]
897
+ );
898
+ const handleFmChange = useCallback(
899
+ (e) => {
900
+ const newFm = e.target.value;
901
+ setFm(newFm);
902
+ fmRef.current = newFm;
903
+ emitChange(newFm, bodyRef.current);
904
+ },
905
+ [emitChange]
906
+ );
907
+ useEffect(() => {
908
+ if (textareaRef.current) {
909
+ textareaRef.current.style.height = "auto";
910
+ textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
911
+ }
912
+ }, [fm]);
913
+ const editor = useEditor({
914
+ extensions: [
915
+ StarterKit.configure({
916
+ heading: { levels: [1, 2, 3, 4, 5, 6] },
917
+ bulletList: { keepMarks: true, keepAttributes: false },
918
+ orderedList: { keepMarks: true, keepAttributes: false },
919
+ codeBlock: false,
920
+ code: {
921
+ HTMLAttributes: {
922
+ class: "bg-muted rounded px-1 py-0.5 font-mono text-sm"
923
+ }
924
+ },
925
+ blockquote: {
926
+ HTMLAttributes: {
927
+ class: "border-l-4 border-muted-foreground/30 pl-4 italic"
928
+ }
929
+ },
930
+ hardBreak: { keepMarks: false }
931
+ }),
932
+ CodeBlockExtension,
933
+ Typography,
934
+ Markdown$1.configure({
935
+ html: false,
936
+ transformPastedText: true,
937
+ transformCopiedText: true
938
+ })
939
+ ],
940
+ content: body,
941
+ editable,
942
+ editorProps: {
943
+ attributes: {
944
+ class: `outline-none ${className}`
945
+ }
946
+ },
947
+ onUpdate: ({ editor: editor2 }) => {
948
+ const markdownStorage = editor2.storage.markdown;
949
+ const newBody = markdownStorage?.getMarkdown?.() ?? editor2.getText();
950
+ bodyRef.current = newBody;
951
+ emitChange(fmRef.current, newBody);
952
+ }
953
+ });
954
+ useEffect(() => {
955
+ editor?.setEditable(editable);
956
+ }, [editor, editable]);
957
+ useEffect(() => {
958
+ if (!editor) return;
959
+ const parsed = parseFrontmatter(value);
960
+ const markdownStorage = editor.storage.markdown;
961
+ const current = markdownStorage?.getMarkdown?.() ?? editor.getText();
962
+ if (parsed.body !== current) {
963
+ editor.commands.setContent(parsed.body);
964
+ }
965
+ }, [editor, value]);
966
+ return /* @__PURE__ */ jsxs("div", { className: "markdown-preview", children: [
967
+ frontmatter && /* @__PURE__ */ jsxs("div", { className: "mb-4 rounded-md border border-border bg-muted/40 overflow-hidden", children: [
968
+ /* @__PURE__ */ jsx("div", { className: "px-3 py-1.5 text-xs font-mono text-muted-foreground border-b border-border bg-muted/60 select-none", children: "yml" }),
969
+ /* @__PURE__ */ jsx(
970
+ "textarea",
971
+ {
972
+ ref: textareaRef,
973
+ value: fm,
974
+ onChange: handleFmChange,
975
+ readOnly: !editable,
976
+ className: "w-full bg-transparent px-3 py-2 font-mono text-sm outline-none resize-none",
977
+ spellCheck: false
978
+ }
979
+ )
980
+ ] }),
981
+ /* @__PURE__ */ jsx(EditorContent, { editor, className: "markdown-editor" })
982
+ ] });
983
+ }
984
+ function getToneClass(tone, status) {
985
+ if (status === "error") {
986
+ return tone === "muted" ? "text-destructive hover:bg-muted" : "text-destructive hover:bg-destructive/10";
987
+ }
988
+ if (status === "saved") {
989
+ return tone === "muted" ? "text-muted-foreground/50 hover:bg-muted" : "text-primary/50 hover:bg-primary/10";
990
+ }
991
+ return tone === "muted" ? "text-muted-foreground hover:bg-muted" : "text-primary hover:bg-primary/20";
992
+ }
993
+ function SaveStatusButton({
994
+ status,
995
+ onClick,
996
+ disabled = false,
997
+ tone
998
+ }) {
999
+ return /* @__PURE__ */ jsxs(
1000
+ "button",
1001
+ {
1002
+ onClick,
1003
+ disabled,
1004
+ className: `px-2 py-1 text-xs rounded flex items-center gap-1 disabled:opacity-50 ${getToneClass(tone, status)}`,
1005
+ title: "Save",
1006
+ children: [
1007
+ /* @__PURE__ */ jsx("span", { className: "inline-flex h-3 w-3 items-center justify-center shrink-0", children: status === "saving" ? /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : status === "error" ? /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Save, { className: `h-3 w-3 ${status === "saved" ? "opacity-60" : "opacity-100"}` }) }),
1008
+ "Save"
1009
+ ]
1010
+ }
1011
+ );
1012
+ }
853
1013
 
854
1014
  // src/components/edit/fileTypes.ts
855
1015
  var COMPILABLE_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"];
856
- var MEDIA_EXTENSIONS = [".svg", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".mp4", ".mov", ".webm"];
857
- var TEXT_EXTENSIONS = [".json", ".yaml", ".yml", ".md", ".txt", ".css", ".html", ".xml", ".toml"];
1016
+ var MEDIA_EXTENSIONS = [
1017
+ ".svg",
1018
+ ".png",
1019
+ ".jpg",
1020
+ ".jpeg",
1021
+ ".gif",
1022
+ ".webp",
1023
+ ".mp4",
1024
+ ".mov",
1025
+ ".webm"
1026
+ ];
1027
+ var TEXT_EXTENSIONS = [
1028
+ ".json",
1029
+ ".yaml",
1030
+ ".yml",
1031
+ ".md",
1032
+ ".txt",
1033
+ ".css",
1034
+ ".html",
1035
+ ".xml",
1036
+ ".toml"
1037
+ ];
858
1038
  var EXTENSION_TO_LANGUAGE = {
859
1039
  ".tsx": "tsx",
860
1040
  ".jsx": "jsx",
@@ -938,6 +1118,12 @@ function isMediaFile(path) {
938
1118
  function isTextFile(path) {
939
1119
  return TEXT_EXTENSIONS.includes(getExtension(path));
940
1120
  }
1121
+ function isMarkdownFile(path) {
1122
+ return getExtension(path) === ".md";
1123
+ }
1124
+ function isPreviewable(path) {
1125
+ return isCompilable(path) || isMarkdownFile(path);
1126
+ }
941
1127
  function getLanguageFromExt(path) {
942
1128
  const ext = getExtension(path);
943
1129
  return EXTENSION_TO_LANGUAGE[ext] ?? null;
@@ -954,6 +1140,17 @@ function isVideoFile(path) {
954
1140
  const ext = getExtension(path);
955
1141
  return [".mp4", ".mov", ".webm"].includes(ext);
956
1142
  }
1143
+ function sortNodes(nodes) {
1144
+ nodes.sort((a, b) => {
1145
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
1146
+ return a.name.localeCompare(b.name);
1147
+ });
1148
+ for (const node of nodes) {
1149
+ if (node.children.length > 0) {
1150
+ sortNodes(node.children);
1151
+ }
1152
+ }
1153
+ }
957
1154
  function buildTree(files) {
958
1155
  const root = { name: "", path: "", isDir: true, children: [] };
959
1156
  for (const file of files) {
@@ -976,14 +1173,26 @@ function buildTree(files) {
976
1173
  current = child;
977
1174
  }
978
1175
  }
979
- root.children.sort((a, b) => {
980
- if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
981
- return a.name.localeCompare(b.name);
982
- });
1176
+ sortNodes(root.children);
983
1177
  return root;
984
1178
  }
985
- function TreeNodeComponent({ node, activeFile, onSelect, onReplaceFile, depth = 0 }) {
986
- const [expanded, setExpanded] = useState(true);
1179
+ function TreeNodeComponent({
1180
+ node,
1181
+ activePath,
1182
+ onSelect,
1183
+ onSelectDirectory,
1184
+ onReplaceFile,
1185
+ onOpenInEditor,
1186
+ openInEditorMode = "files",
1187
+ openInEditorIcon,
1188
+ openInEditorTitle = "Open in editor",
1189
+ pinnedPaths,
1190
+ onTogglePin,
1191
+ pageSize = 10,
1192
+ depth = 0
1193
+ }) {
1194
+ const [expanded, setExpanded] = useState(false);
1195
+ const [visibleCount, setVisibleCount] = useState(pageSize);
987
1196
  const [isHovered, setIsHovered] = useState(false);
988
1197
  const fileInputRef = useRef(null);
989
1198
  const handleUploadClick = useCallback((e) => {
@@ -1002,48 +1211,119 @@ function TreeNodeComponent({ node, activeFile, onSelect, onReplaceFile, depth =
1002
1211
  reader.readAsDataURL(file);
1003
1212
  e.target.value = "";
1004
1213
  }, [node.path, onReplaceFile]);
1214
+ const isPinned = pinnedPaths?.has(node.path) ?? false;
1215
+ const showPin = onTogglePin && isHovered;
1216
+ const handleTogglePin = useCallback((e) => {
1217
+ e.stopPropagation();
1218
+ onTogglePin?.(node.path, node.isDir);
1219
+ }, [node.path, node.isDir, onTogglePin]);
1005
1220
  if (!node.name) {
1006
1221
  return /* @__PURE__ */ jsx(Fragment, { children: node.children.map((child) => /* @__PURE__ */ jsx(
1007
1222
  TreeNodeComponent,
1008
1223
  {
1009
1224
  node: child,
1010
- activeFile,
1225
+ activePath,
1011
1226
  onSelect,
1227
+ onSelectDirectory,
1012
1228
  onReplaceFile,
1229
+ onOpenInEditor,
1230
+ openInEditorMode,
1231
+ openInEditorIcon,
1232
+ openInEditorTitle,
1233
+ pinnedPaths,
1234
+ onTogglePin,
1235
+ pageSize,
1013
1236
  depth
1014
1237
  },
1015
1238
  child.path
1016
1239
  )) });
1017
1240
  }
1018
- const isActive = node.path === activeFile;
1241
+ const isActive = node.path === activePath;
1019
1242
  const isMedia = !node.isDir && isMediaFile(node.path);
1020
1243
  const showUpload = isMedia && isHovered && onReplaceFile;
1244
+ const showOpenInEditor = !!onOpenInEditor && isHovered && (openInEditorMode === "all" || (openInEditorMode === "directories" ? node.isDir : !node.isDir));
1245
+ const handleOpenInEditor = useCallback(
1246
+ (e) => {
1247
+ e.stopPropagation();
1248
+ onOpenInEditor?.(node.path, node.isDir);
1249
+ },
1250
+ [node.path, node.isDir, onOpenInEditor]
1251
+ );
1021
1252
  if (node.isDir) {
1022
1253
  return /* @__PURE__ */ jsxs("div", { children: [
1023
1254
  /* @__PURE__ */ jsxs(
1024
1255
  "button",
1025
1256
  {
1026
- onClick: () => setExpanded(!expanded),
1027
- className: "flex items-center gap-1 w-full px-2 py-1 text-left text-sm hover:bg-muted/50 rounded",
1257
+ onClick: () => {
1258
+ onSelectDirectory?.(node.path);
1259
+ setExpanded(!expanded);
1260
+ },
1261
+ className: `flex items-center gap-1 w-full px-2 py-1 text-left text-sm hover:bg-muted/50 rounded ${isActive ? "bg-primary/10 text-primary" : ""}`,
1262
+ onMouseEnter: () => setIsHovered(true),
1263
+ onMouseLeave: () => setIsHovered(false),
1028
1264
  style: { paddingLeft: `${depth * 12 + 8}px` },
1029
1265
  children: [
1030
1266
  expanded ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 shrink-0" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 shrink-0" }),
1031
1267
  /* @__PURE__ */ jsx(Folder, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
1032
- /* @__PURE__ */ jsx("span", { className: "truncate", children: node.name })
1268
+ /* @__PURE__ */ jsx("span", { className: "truncate flex-1 flex pl-2", children: node.name }),
1269
+ (showPin || isPinned) && /* @__PURE__ */ jsx(
1270
+ "span",
1271
+ {
1272
+ onClick: handleTogglePin,
1273
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1274
+ title: isPinned ? "Unpin" : "Pin",
1275
+ children: isPinned ? /* @__PURE__ */ jsx(PinOff, { className: "h-3 w-3 text-primary" }) : /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3 text-muted-foreground" })
1276
+ }
1277
+ ),
1278
+ showOpenInEditor && /* @__PURE__ */ jsx(
1279
+ "span",
1280
+ {
1281
+ onClick: handleOpenInEditor,
1282
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1283
+ title: openInEditorTitle,
1284
+ children: openInEditorIcon ?? /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 text-primary" })
1285
+ }
1286
+ )
1033
1287
  ]
1034
1288
  }
1035
1289
  ),
1036
- expanded && /* @__PURE__ */ jsx("div", { children: node.children.map((child) => /* @__PURE__ */ jsx(
1037
- TreeNodeComponent,
1038
- {
1039
- node: child,
1040
- activeFile,
1041
- onSelect,
1042
- onReplaceFile,
1043
- depth: depth + 1
1044
- },
1045
- child.path
1046
- )) })
1290
+ expanded && /* @__PURE__ */ jsxs("div", { children: [
1291
+ node.children.slice(0, visibleCount).map((child) => /* @__PURE__ */ jsx(
1292
+ TreeNodeComponent,
1293
+ {
1294
+ node: child,
1295
+ activePath,
1296
+ onSelect,
1297
+ onSelectDirectory,
1298
+ onReplaceFile,
1299
+ onOpenInEditor,
1300
+ openInEditorMode,
1301
+ openInEditorIcon,
1302
+ openInEditorTitle,
1303
+ pinnedPaths,
1304
+ onTogglePin,
1305
+ pageSize,
1306
+ depth: depth + 1
1307
+ },
1308
+ child.path
1309
+ )),
1310
+ node.children.length > visibleCount && /* @__PURE__ */ jsxs(
1311
+ "button",
1312
+ {
1313
+ onClick: () => setVisibleCount((prev) => prev + pageSize),
1314
+ className: "flex items-center gap-1 w-full px-2 py-1 text-left text-xs hover:bg-muted/50 rounded text-muted-foreground",
1315
+ style: { paddingLeft: `${(depth + 1) * 12 + 20}px` },
1316
+ children: [
1317
+ /* @__PURE__ */ jsx(ChevronsDown, { className: "h-3 w-3 shrink-0" }),
1318
+ /* @__PURE__ */ jsxs("span", { children: [
1319
+ "Show ",
1320
+ Math.min(pageSize, node.children.length - visibleCount),
1321
+ " more"
1322
+ ] })
1323
+ ]
1324
+ }
1325
+ )
1326
+ ] })
1047
1327
  ] });
1048
1328
  }
1049
1329
  return /* @__PURE__ */ jsxs(
@@ -1061,12 +1341,30 @@ function TreeNodeComponent({ node, activeFile, onSelect, onReplaceFile, depth =
1061
1341
  style: { paddingLeft: `${depth * 12 + 20}px` },
1062
1342
  children: [
1063
1343
  /* @__PURE__ */ jsx(File, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
1064
- /* @__PURE__ */ jsx("span", { className: "truncate flex-1", children: node.name }),
1344
+ /* @__PURE__ */ jsx("span", { className: "truncate flex-1 flex pl-2", children: node.name }),
1345
+ (showPin || isPinned) && /* @__PURE__ */ jsx(
1346
+ "span",
1347
+ {
1348
+ onClick: handleTogglePin,
1349
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1350
+ title: isPinned ? "Unpin" : "Pin",
1351
+ children: isPinned ? /* @__PURE__ */ jsx(PinOff, { className: "h-3 w-3 text-primary" }) : /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3 text-muted-foreground" })
1352
+ }
1353
+ ),
1354
+ showOpenInEditor && /* @__PURE__ */ jsx(
1355
+ "span",
1356
+ {
1357
+ onClick: handleOpenInEditor,
1358
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1359
+ title: openInEditorTitle,
1360
+ children: openInEditorIcon ?? /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 text-primary" })
1361
+ }
1362
+ ),
1065
1363
  showUpload && /* @__PURE__ */ jsx(
1066
1364
  "span",
1067
1365
  {
1068
1366
  onClick: handleUploadClick,
1069
- className: "p-0.5 hover:bg-primary/20 rounded cursor-pointer",
1367
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1070
1368
  title: "Replace file",
1071
1369
  children: /* @__PURE__ */ jsx(Upload, { className: "h-3 w-3 text-primary" })
1072
1370
  }
@@ -1088,17 +1386,309 @@ function TreeNodeComponent({ node, activeFile, onSelect, onReplaceFile, depth =
1088
1386
  }
1089
1387
  );
1090
1388
  }
1091
- function FileTree({ files, activeFile, onSelectFile, onReplaceFile }) {
1389
+ function LazyTreeNode({
1390
+ entry,
1391
+ activePath,
1392
+ onSelectFile,
1393
+ onSelectDirectory,
1394
+ onOpenInEditor,
1395
+ openInEditorMode = "files",
1396
+ openInEditorIcon,
1397
+ openInEditorTitle = "Open in editor",
1398
+ pinnedPaths,
1399
+ onTogglePin,
1400
+ directoryLoader,
1401
+ pageSize,
1402
+ depth = 0,
1403
+ reloadToken
1404
+ }) {
1405
+ const [expanded, setExpanded] = useState(false);
1406
+ const [loading, setLoading] = useState(false);
1407
+ const [loadError, setLoadError] = useState(null);
1408
+ const [isHovered, setIsHovered] = useState(false);
1409
+ const [children, setChildren] = useState(null);
1410
+ const [visibleCount, setVisibleCount] = useState(pageSize);
1411
+ useEffect(() => {
1412
+ setChildren(null);
1413
+ setVisibleCount(pageSize);
1414
+ if (expanded) {
1415
+ setLoading(true);
1416
+ setLoadError(null);
1417
+ directoryLoader(entry.path).then((loaded) => setChildren(loaded)).catch((err) => setLoadError(err instanceof Error ? err.message : "Failed to load directory")).finally(() => setLoading(false));
1418
+ }
1419
+ }, [reloadToken]);
1420
+ const isActive = entry.path === activePath;
1421
+ const isPinned = pinnedPaths?.has(entry.path) ?? false;
1422
+ const showPin = onTogglePin && isHovered;
1423
+ const showOpenInEditor = !!onOpenInEditor && isHovered && (openInEditorMode === "all" || (openInEditorMode === "directories" ? entry.isDir : !entry.isDir));
1424
+ const handleOpenInEditor = useCallback(
1425
+ (e) => {
1426
+ e.stopPropagation();
1427
+ onOpenInEditor?.(entry.path, entry.isDir);
1428
+ },
1429
+ [entry.path, entry.isDir, onOpenInEditor]
1430
+ );
1431
+ const handleTogglePin = useCallback(
1432
+ (e) => {
1433
+ e.stopPropagation();
1434
+ onTogglePin?.(entry.path, entry.isDir);
1435
+ },
1436
+ [entry.path, entry.isDir, onTogglePin]
1437
+ );
1438
+ const toggleDirectory = useCallback(async () => {
1439
+ if (!entry.isDir) return;
1440
+ onSelectDirectory?.(entry.path);
1441
+ if (!expanded && children === null) {
1442
+ setLoading(true);
1443
+ setLoadError(null);
1444
+ try {
1445
+ const loaded = await directoryLoader(entry.path);
1446
+ setChildren(loaded);
1447
+ } catch (err) {
1448
+ setLoadError(err instanceof Error ? err.message : "Failed to load directory");
1449
+ } finally {
1450
+ setLoading(false);
1451
+ }
1452
+ }
1453
+ setExpanded((prev) => !prev);
1454
+ }, [entry.isDir, entry.path, onSelectDirectory, expanded, children, directoryLoader]);
1455
+ if (!entry.isDir) {
1456
+ return /* @__PURE__ */ jsx(
1457
+ "div",
1458
+ {
1459
+ className: "relative",
1460
+ onMouseEnter: () => setIsHovered(true),
1461
+ onMouseLeave: () => setIsHovered(false),
1462
+ children: /* @__PURE__ */ jsxs(
1463
+ "button",
1464
+ {
1465
+ onClick: () => onSelectFile(entry.path),
1466
+ className: `flex items-center gap-1 w-full px-2 py-1 text-left text-sm hover:bg-muted/50 rounded ${isActive ? "bg-primary/10 text-primary" : ""}`,
1467
+ style: { paddingLeft: `${depth * 12 + 20}px` },
1468
+ children: [
1469
+ /* @__PURE__ */ jsx(File, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
1470
+ /* @__PURE__ */ jsx("span", { className: "truncate flex-1 flex pl-2", children: entry.name }),
1471
+ (showPin || isPinned) && /* @__PURE__ */ jsx(
1472
+ "span",
1473
+ {
1474
+ onClick: handleTogglePin,
1475
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1476
+ title: isPinned ? "Unpin" : "Pin",
1477
+ children: isPinned ? /* @__PURE__ */ jsx(PinOff, { className: "h-3 w-3 text-primary" }) : /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3 text-muted-foreground" })
1478
+ }
1479
+ ),
1480
+ showOpenInEditor && /* @__PURE__ */ jsx(
1481
+ "span",
1482
+ {
1483
+ onClick: handleOpenInEditor,
1484
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1485
+ title: openInEditorTitle,
1486
+ children: openInEditorIcon ?? /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 text-primary" })
1487
+ }
1488
+ )
1489
+ ]
1490
+ }
1491
+ )
1492
+ }
1493
+ );
1494
+ }
1495
+ return /* @__PURE__ */ jsxs("div", { children: [
1496
+ /* @__PURE__ */ jsxs(
1497
+ "button",
1498
+ {
1499
+ onClick: () => void toggleDirectory(),
1500
+ className: `flex items-center gap-1 w-full px-2 py-1 text-left text-sm hover:bg-muted/50 rounded ${isActive ? "bg-primary/10 text-primary" : ""}`,
1501
+ onMouseEnter: () => setIsHovered(true),
1502
+ onMouseLeave: () => setIsHovered(false),
1503
+ style: { paddingLeft: `${depth * 12 + 8}px` },
1504
+ children: [
1505
+ expanded ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 shrink-0" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 shrink-0" }),
1506
+ /* @__PURE__ */ jsx(Folder, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
1507
+ /* @__PURE__ */ jsx("span", { className: "truncate flex-1 flex pl-2", children: entry.name }),
1508
+ loading && /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin text-muted-foreground" }),
1509
+ (showPin || isPinned) && /* @__PURE__ */ jsx(
1510
+ "span",
1511
+ {
1512
+ onClick: handleTogglePin,
1513
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1514
+ title: isPinned ? "Unpin" : "Pin",
1515
+ children: isPinned ? /* @__PURE__ */ jsx(PinOff, { className: "h-3 w-3 text-primary" }) : /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3 text-muted-foreground" })
1516
+ }
1517
+ ),
1518
+ showOpenInEditor && /* @__PURE__ */ jsx(
1519
+ "span",
1520
+ {
1521
+ onClick: handleOpenInEditor,
1522
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1523
+ title: openInEditorTitle,
1524
+ children: openInEditorIcon ?? /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 text-primary" })
1525
+ }
1526
+ )
1527
+ ]
1528
+ }
1529
+ ),
1530
+ expanded && /* @__PURE__ */ jsxs("div", { children: [
1531
+ loadError && /* @__PURE__ */ jsx(
1532
+ "div",
1533
+ {
1534
+ className: "px-2 py-1 text-xs text-destructive",
1535
+ style: { paddingLeft: `${(depth + 1) * 12 + 20}px` },
1536
+ children: loadError
1537
+ }
1538
+ ),
1539
+ (children ?? []).slice(0, visibleCount).map((child) => /* @__PURE__ */ jsx(
1540
+ LazyTreeNode,
1541
+ {
1542
+ entry: child,
1543
+ activePath,
1544
+ onSelectFile,
1545
+ onSelectDirectory,
1546
+ onOpenInEditor,
1547
+ openInEditorMode,
1548
+ openInEditorIcon,
1549
+ openInEditorTitle,
1550
+ pinnedPaths,
1551
+ onTogglePin,
1552
+ directoryLoader,
1553
+ pageSize,
1554
+ depth: depth + 1,
1555
+ reloadToken
1556
+ },
1557
+ child.path
1558
+ )),
1559
+ (children?.length ?? 0) > visibleCount && /* @__PURE__ */ jsxs(
1560
+ "button",
1561
+ {
1562
+ onClick: () => setVisibleCount((prev) => prev + pageSize),
1563
+ className: "flex items-center gap-1 w-full px-2 py-1 text-left text-xs hover:bg-muted/50 rounded text-muted-foreground",
1564
+ style: { paddingLeft: `${(depth + 1) * 12 + 20}px` },
1565
+ children: [
1566
+ /* @__PURE__ */ jsx(ChevronsDown, { className: "h-3 w-3 shrink-0" }),
1567
+ /* @__PURE__ */ jsxs("span", { children: [
1568
+ "Show ",
1569
+ Math.min(pageSize, (children?.length ?? 0) - visibleCount),
1570
+ " more"
1571
+ ] })
1572
+ ]
1573
+ }
1574
+ )
1575
+ ] })
1576
+ ] });
1577
+ }
1578
+ function FileTree({
1579
+ files = [],
1580
+ activeFile,
1581
+ activePath,
1582
+ title = "Files",
1583
+ onSelectFile,
1584
+ onSelectDirectory,
1585
+ onReplaceFile,
1586
+ onOpenInEditor,
1587
+ openInEditorMode,
1588
+ openInEditorIcon,
1589
+ openInEditorTitle,
1590
+ pinnedPaths,
1591
+ onTogglePin,
1592
+ directoryLoader,
1593
+ pageSize = 10,
1594
+ reloadToken
1595
+ }) {
1092
1596
  const tree = useMemo(() => buildTree(files), [files]);
1093
- return /* @__PURE__ */ jsxs("div", { className: "w-48 border-r bg-muted/30 overflow-auto text-foreground", children: [
1094
- /* @__PURE__ */ jsx("div", { className: "p-2 border-b text-xs font-medium text-muted-foreground uppercase tracking-wide", children: "Files" }),
1095
- /* @__PURE__ */ jsx("div", { className: "p-1", children: /* @__PURE__ */ jsx(
1597
+ const selectedPath = activePath ?? activeFile ?? "";
1598
+ const [rootEntries, setRootEntries] = useState([]);
1599
+ const [rootLoading, setRootLoading] = useState(false);
1600
+ const [rootError, setRootError] = useState(null);
1601
+ useEffect(() => {
1602
+ if (!directoryLoader) return;
1603
+ let cancelled = false;
1604
+ const loadRoot = async () => {
1605
+ setRootLoading(true);
1606
+ setRootError(null);
1607
+ try {
1608
+ const entries = await directoryLoader("");
1609
+ if (!cancelled) {
1610
+ setRootEntries(entries);
1611
+ }
1612
+ } catch (err) {
1613
+ if (!cancelled) {
1614
+ setRootError(err instanceof Error ? err.message : "Failed to load files");
1615
+ }
1616
+ } finally {
1617
+ if (!cancelled) {
1618
+ setRootLoading(false);
1619
+ }
1620
+ }
1621
+ };
1622
+ void loadRoot();
1623
+ return () => {
1624
+ cancelled = true;
1625
+ };
1626
+ }, [directoryLoader, reloadToken]);
1627
+ return /* @__PURE__ */ jsxs("div", { className: "min-w-48 border-r bg-muted/30 overflow-auto text-foreground", children: [
1628
+ /* @__PURE__ */ jsx("div", { className: "p-2 border-b text-xs font-medium text-muted-foreground uppercase tracking-wide", children: title }),
1629
+ pinnedPaths && pinnedPaths.size > 0 && /* @__PURE__ */ jsx("div", { className: "px-2 py-1 border-b flex flex-wrap gap-1", children: Array.from(pinnedPaths).map(([p, isDir]) => /* @__PURE__ */ jsxs(
1630
+ "button",
1631
+ {
1632
+ onClick: () => isDir ? onSelectDirectory?.(p) : onSelectFile(p),
1633
+ className: `inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs hover:bg-muted/50 ${(activePath ?? activeFile ?? "") === p ? "bg-primary/10 text-primary" : "text-muted-foreground"}`,
1634
+ children: [
1635
+ isDir ? /* @__PURE__ */ jsx(Folder, { className: "h-2.5 w-2.5 shrink-0" }) : /* @__PURE__ */ jsx(Pin, { className: "h-2.5 w-2.5 shrink-0" }),
1636
+ /* @__PURE__ */ jsx("span", { className: "truncate max-w-[120px]", children: p.split("/").pop() }),
1637
+ onTogglePin && /* @__PURE__ */ jsx(
1638
+ "span",
1639
+ {
1640
+ onClick: (e) => {
1641
+ e.stopPropagation();
1642
+ onTogglePin(p, isDir);
1643
+ },
1644
+ className: "hover:text-destructive",
1645
+ children: /* @__PURE__ */ jsx(X, { className: "h-2.5 w-2.5" })
1646
+ }
1647
+ )
1648
+ ]
1649
+ },
1650
+ p
1651
+ )) }),
1652
+ /* @__PURE__ */ jsx("div", { className: "p-1", children: directoryLoader ? /* @__PURE__ */ jsxs(Fragment, { children: [
1653
+ rootLoading && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-2 py-1 text-xs text-muted-foreground", children: [
1654
+ /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }),
1655
+ /* @__PURE__ */ jsx("span", { children: "Loading..." })
1656
+ ] }),
1657
+ rootError && /* @__PURE__ */ jsx("div", { className: "px-2 py-1 text-xs text-destructive", children: rootError }),
1658
+ rootEntries.map((entry) => /* @__PURE__ */ jsx(
1659
+ LazyTreeNode,
1660
+ {
1661
+ entry,
1662
+ activePath: selectedPath,
1663
+ onSelectFile,
1664
+ onSelectDirectory,
1665
+ onOpenInEditor,
1666
+ openInEditorMode,
1667
+ openInEditorIcon,
1668
+ openInEditorTitle,
1669
+ pinnedPaths,
1670
+ onTogglePin,
1671
+ directoryLoader,
1672
+ pageSize,
1673
+ reloadToken
1674
+ },
1675
+ entry.path
1676
+ ))
1677
+ ] }) : /* @__PURE__ */ jsx(
1096
1678
  TreeNodeComponent,
1097
1679
  {
1098
1680
  node: tree,
1099
- activeFile,
1681
+ activePath: selectedPath,
1100
1682
  onSelect: onSelectFile,
1101
- onReplaceFile
1683
+ onSelectDirectory,
1684
+ onReplaceFile,
1685
+ onOpenInEditor,
1686
+ openInEditorMode,
1687
+ openInEditorIcon,
1688
+ openInEditorTitle,
1689
+ pinnedPaths,
1690
+ onTogglePin,
1691
+ pageSize
1102
1692
  }
1103
1693
  ) })
1104
1694
  ] });
@@ -1152,8 +1742,68 @@ function SaveConfirmDialog({
1152
1742
  ] })
1153
1743
  ] }) });
1154
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
+ }
1155
1794
  function CodeBlockView({ content, language, editable = false, onChange }) {
1156
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
+ }, []);
1157
1807
  useEffect(() => {
1158
1808
  if (textareaRef.current) {
1159
1809
  textareaRef.current.style.height = "auto";
@@ -1184,23 +1834,60 @@ function CodeBlockView({ content, language, editable = false, onChange }) {
1184
1834
  [onChange]
1185
1835
  );
1186
1836
  const langLabel = language || "text";
1187
- return /* @__PURE__ */ jsxs("div", { className: "h-full flex flex-col bg-muted/10", children: [
1188
- /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between px-4 py-2 bg-muted/30 border-b text-xs", children: /* @__PURE__ */ jsx("span", { className: "font-mono text-muted-foreground", children: langLabel }) }),
1189
- editable ? /* @__PURE__ */ jsx(
1190
- "textarea",
1191
- {
1192
- ref: textareaRef,
1193
- value: content,
1194
- onChange: handleChange,
1195
- onKeyDown: handleKeyDown,
1196
- className: "w-full min-h-full font-mono text-xs leading-relaxed bg-transparent border-none outline-none resize-none",
1197
- spellCheck: false,
1198
- style: {
1199
- tabSize: 2,
1200
- WebkitTextFillColor: "inherit"
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 }) })
1201
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 }
1202
1889
  }
1203
- ) : /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono whitespace-pre-wrap break-words m-0 leading-relaxed", children: /* @__PURE__ */ jsx("code", { children: content }) })
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 }) }) }) })
1204
1891
  ] });
1205
1892
  }
1206
1893
  function formatFileSize(bytes) {
@@ -1208,10 +1895,16 @@ function formatFileSize(bytes) {
1208
1895
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1209
1896
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1210
1897
  }
1898
+ function isUrl(content) {
1899
+ return content.startsWith("/") || content.startsWith("http://") || content.startsWith("https://") || content.startsWith("./") || content.startsWith("../");
1900
+ }
1211
1901
  function getDataUrl(content, mimeType) {
1212
1902
  if (content.startsWith("data:")) {
1213
1903
  return content;
1214
1904
  }
1905
+ if (isUrl(content)) {
1906
+ return content;
1907
+ }
1215
1908
  return `data:${mimeType};base64,${content}`;
1216
1909
  }
1217
1910
  function MediaPreview({ content, mimeType, fileName }) {
@@ -1220,8 +1913,8 @@ function MediaPreview({ content, mimeType, fileName }) {
1220
1913
  const dataUrl = getDataUrl(content, mimeType);
1221
1914
  const isImage = isImageFile(fileName);
1222
1915
  const isVideo = isVideoFile(fileName);
1223
- content.length;
1224
- const estimatedBytes = content.startsWith("data:") ? Math.floor((content.split(",")[1]?.length ?? 0) * 0.75) : Math.floor(content.length * 0.75);
1916
+ const isUrlContent = isUrl(content);
1917
+ const estimatedBytes = isUrlContent ? null : content.startsWith("data:") ? Math.floor((content.split(",")[1]?.length ?? 0) * 0.75) : Math.floor(content.length * 0.75);
1225
1918
  useEffect(() => {
1226
1919
  setDimensions(null);
1227
1920
  setError(null);
@@ -1281,7 +1974,7 @@ function MediaPreview({ content, mimeType, fileName }) {
1281
1974
  dimensions.height,
1282
1975
  " px"
1283
1976
  ] }),
1284
- /* @__PURE__ */ jsx("span", { children: formatFileSize(estimatedBytes) }),
1977
+ estimatedBytes !== null && /* @__PURE__ */ jsx("span", { children: formatFileSize(estimatedBytes) }),
1285
1978
  /* @__PURE__ */ jsx("span", { className: "text-muted-foreground/60", children: mimeType })
1286
1979
  ] })
1287
1980
  ] })
@@ -1304,6 +1997,7 @@ function EditModal({
1304
1997
  renderError,
1305
1998
  previewError,
1306
1999
  previewLoading,
2000
+ initialTreePath,
1307
2001
  initialState = {},
1308
2002
  hideFileTree = false,
1309
2003
  ...sessionOptions
@@ -1315,19 +2009,30 @@ function EditModal({
1315
2009
  const [editInput, setEditInput] = useState("");
1316
2010
  const [bobbinChanges, setBobbinChanges] = useState([]);
1317
2011
  const [previewContainer, setPreviewContainer] = useState(null);
2012
+ const [pillContainer, setPillContainer] = useState(null);
1318
2013
  const [showConfirm, setShowConfirm] = useState(false);
1319
2014
  const [isSaving, setIsSaving] = useState(false);
2015
+ const [saveStatus, setSaveStatus] = useState("saved");
2016
+ const [lastSavedSnapshot, setLastSavedSnapshot] = useState("");
1320
2017
  const [saveError, setSaveError] = useState(null);
1321
2018
  const [pendingClose, setPendingClose] = useState(null);
2019
+ const [treePath, setTreePath] = useState(initialTreePath ?? "");
2020
+ const wasOpenRef = useRef(false);
1322
2021
  const currentCodeRef = useRef("");
1323
2022
  const session = useEditSession(sessionOptions);
1324
2023
  const code = getActiveContent(session);
2024
+ const effectiveTreePath = treePath || session.activeFile;
1325
2025
  currentCodeRef.current = code;
1326
2026
  const files = useMemo(() => getFiles(session.project), [session.project]);
2027
+ const projectSnapshot = useMemo(
2028
+ () => Array.from(session.project.files.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([path, file]) => `${path}\0${file.content}`).join(""),
2029
+ [session.project]
2030
+ );
1327
2031
  const hasChanges = code !== (session.originalProject.files.get(session.activeFile)?.content ?? "");
1328
2032
  const fileType = useMemo(() => getFileType(session.activeFile), [session.activeFile]);
1329
2033
  const isCompilableFile = isCompilable(session.activeFile);
1330
- const showPreviewToggle = isCompilableFile;
2034
+ const isMarkdown = isMarkdownFile(session.activeFile);
2035
+ const showPreviewToggle = isCompilableFile || isMarkdown;
1331
2036
  const handleBobbinChanges = useCallback((changes) => {
1332
2037
  setBobbinChanges(changes);
1333
2038
  }, []);
@@ -1349,6 +2054,26 @@ ${bobbinYaml}
1349
2054
  setBobbinChanges([]);
1350
2055
  };
1351
2056
  const hasSaveHandler = onSave || onSaveProject;
2057
+ useEffect(() => {
2058
+ if (isOpen && !wasOpenRef.current) {
2059
+ setLastSavedSnapshot(projectSnapshot);
2060
+ setSaveStatus("saved");
2061
+ setSaveError(null);
2062
+ }
2063
+ wasOpenRef.current = isOpen;
2064
+ }, [isOpen, projectSnapshot]);
2065
+ useEffect(() => {
2066
+ if (!hasSaveHandler) return;
2067
+ if (projectSnapshot === lastSavedSnapshot) {
2068
+ if (saveStatus !== "saving" && saveStatus !== "saved") {
2069
+ setSaveStatus("saved");
2070
+ }
2071
+ return;
2072
+ }
2073
+ if (saveStatus === "saved" || saveStatus === "error") {
2074
+ setSaveStatus("unsaved");
2075
+ }
2076
+ }, [projectSnapshot, lastSavedSnapshot, saveStatus, hasSaveHandler]);
1352
2077
  const handleClose = useCallback(() => {
1353
2078
  const editCount = session.history.length;
1354
2079
  const finalCode = code;
@@ -1365,6 +2090,7 @@ ${bobbinYaml}
1365
2090
  const handleSaveAndClose = useCallback(async () => {
1366
2091
  if (!pendingClose || !hasSaveHandler) return;
1367
2092
  setIsSaving(true);
2093
+ setSaveStatus("saving");
1368
2094
  setSaveError(null);
1369
2095
  try {
1370
2096
  if (onSaveProject) {
@@ -1372,6 +2098,8 @@ ${bobbinYaml}
1372
2098
  } else if (onSave) {
1373
2099
  await onSave(pendingClose.code);
1374
2100
  }
2101
+ setLastSavedSnapshot(projectSnapshot);
2102
+ setSaveStatus("saved");
1375
2103
  setShowConfirm(false);
1376
2104
  setEditInput("");
1377
2105
  session.clearError();
@@ -1379,10 +2107,11 @@ ${bobbinYaml}
1379
2107
  setPendingClose(null);
1380
2108
  } catch (e) {
1381
2109
  setSaveError(e instanceof Error ? e.message : "Save failed");
2110
+ setSaveStatus("error");
1382
2111
  } finally {
1383
2112
  setIsSaving(false);
1384
2113
  }
1385
- }, [pendingClose, onSave, onSaveProject, session, onClose]);
2114
+ }, [pendingClose, onSave, onSaveProject, session, onClose, projectSnapshot, hasSaveHandler]);
1386
2115
  const handleDiscard = useCallback(() => {
1387
2116
  if (!pendingClose) return;
1388
2117
  setShowConfirm(false);
@@ -1399,6 +2128,7 @@ ${bobbinYaml}
1399
2128
  const handleDirectSave = useCallback(async () => {
1400
2129
  if (!hasSaveHandler) return;
1401
2130
  setIsSaving(true);
2131
+ setSaveStatus("saving");
1402
2132
  setSaveError(null);
1403
2133
  try {
1404
2134
  if (onSaveProject) {
@@ -1406,12 +2136,15 @@ ${bobbinYaml}
1406
2136
  } else if (onSave && currentCodeRef.current) {
1407
2137
  await onSave(currentCodeRef.current);
1408
2138
  }
2139
+ setLastSavedSnapshot(projectSnapshot);
2140
+ setSaveStatus("saved");
1409
2141
  } catch (e) {
1410
2142
  setSaveError(e instanceof Error ? e.message : "Save failed");
2143
+ setSaveStatus("error");
1411
2144
  } finally {
1412
2145
  setIsSaving(false);
1413
2146
  }
1414
- }, [onSave, onSaveProject, session.project]);
2147
+ }, [onSave, onSaveProject, session.project, hasSaveHandler, projectSnapshot]);
1415
2148
  if (!isOpen) return null;
1416
2149
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1417
2150
  /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-8", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col bg-background rounded-lg shadow-xl w-full h-full max-w-6xl max-h-[90vh] overflow-hidden", children: [
@@ -1440,30 +2173,26 @@ ${bobbinYaml}
1440
2173
  children: showTree ? /* @__PURE__ */ jsx(FileCode, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(FolderTree, { className: "h-3 w-3" })
1441
2174
  }
1442
2175
  ),
2176
+ hasSaveHandler && /* @__PURE__ */ jsx(
2177
+ SaveStatusButton,
2178
+ {
2179
+ status: saveStatus,
2180
+ onClick: handleDirectSave,
2181
+ disabled: isSaving,
2182
+ tone: "primary"
2183
+ }
2184
+ ),
1443
2185
  showPreviewToggle && /* @__PURE__ */ jsxs(
1444
2186
  "button",
1445
2187
  {
1446
2188
  onClick: () => setShowPreview(!showPreview),
1447
- className: `px-2 py-1 text-xs rounded flex items-center gap-1 ${showPreview ? "bg-primary text-primary-foreground" : "hover:bg-primary/20 text-primary"}`,
2189
+ className: `w-[5rem] px-2 py-1 text-xs rounded flex items-center gap-1 ${showPreview ? "bg-primary text-primary-foreground" : "hover:bg-primary/20 text-primary"}`,
1448
2190
  children: [
1449
2191
  showPreview ? /* @__PURE__ */ jsx(Eye, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Code, { className: "h-3 w-3" }),
1450
2192
  showPreview ? "Preview" : "Code"
1451
2193
  ]
1452
2194
  }
1453
2195
  ),
1454
- hasSaveHandler && /* @__PURE__ */ jsxs(
1455
- "button",
1456
- {
1457
- onClick: handleDirectSave,
1458
- disabled: isSaving,
1459
- className: "px-2 py-1 text-xs rounded flex items-center gap-1 hover:bg-primary/20 text-primary disabled:opacity-50",
1460
- title: "Save changes",
1461
- children: [
1462
- isSaving ? /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : /* @__PURE__ */ jsx(Save, { className: "h-3 w-3" }),
1463
- "Save"
1464
- ]
1465
- }
1466
- ),
1467
2196
  /* @__PURE__ */ jsxs(
1468
2197
  "button",
1469
2198
  {
@@ -1484,11 +2213,16 @@ ${bobbinYaml}
1484
2213
  {
1485
2214
  files,
1486
2215
  activeFile: session.activeFile,
1487
- onSelectFile: session.setActiveFile,
2216
+ activePath: effectiveTreePath,
2217
+ onSelectFile: (path) => {
2218
+ setTreePath(path);
2219
+ session.setActiveFile(path);
2220
+ },
2221
+ onSelectDirectory: (path) => setTreePath(path),
1488
2222
  onReplaceFile: session.replaceFile
1489
2223
  }
1490
2224
  ),
1491
- /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-auto", children: fileType.category === "compilable" && showPreview ? /* @__PURE__ */ jsxs("div", { className: "bg-white h-full relative", ref: setPreviewContainer, children: [
2225
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-auto", ref: setPillContainer, children: fileType.category === "compilable" && showPreview ? /* @__PURE__ */ jsxs("div", { className: "bg-white h-full relative", ref: setPreviewContainer, children: [
1492
2226
  previewError && renderError ? renderError(previewError) : previewError ? /* @__PURE__ */ jsxs("div", { className: "p-4 text-sm text-destructive flex items-center gap-2", children: [
1493
2227
  /* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0" }),
1494
2228
  /* @__PURE__ */ jsx("span", { children: previewError })
@@ -1500,7 +2234,7 @@ ${bobbinYaml}
1500
2234
  Bobbin,
1501
2235
  {
1502
2236
  container: previewContainer,
1503
- pillContainer: previewContainer,
2237
+ pillContainer,
1504
2238
  defaultActive: false,
1505
2239
  showInspector: true,
1506
2240
  onChanges: handleBobbinChanges,
@@ -1515,7 +2249,14 @@ ${bobbinYaml}
1515
2249
  editable: true,
1516
2250
  onChange: session.updateActiveFile
1517
2251
  }
1518
- ) : fileType.category === "text" ? /* @__PURE__ */ jsx(
2252
+ ) : isMarkdown && showPreview ? /* @__PURE__ */ jsx("div", { className: "p-4 prose prose-sm dark:prose-invert max-w-none h-full overflow-auto", children: /* @__PURE__ */ jsx(
2253
+ MarkdownPreview,
2254
+ {
2255
+ value: code,
2256
+ editable: true,
2257
+ onChange: session.updateActiveFile
2258
+ }
2259
+ ) }) : fileType.category === "text" ? /* @__PURE__ */ jsx(
1519
2260
  CodeBlockView,
1520
2261
  {
1521
2262
  content: code,
@@ -1530,7 +2271,15 @@ ${bobbinYaml}
1530
2271
  mimeType: getMimeType(session.activeFile),
1531
2272
  fileName: session.activeFile.split("/").pop() ?? session.activeFile
1532
2273
  }
1533
- ) : /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-muted-foreground", children: /* @__PURE__ */ jsx("p", { className: "text-sm", children: "Preview not available for this file type" }) }) })
2274
+ ) : /* @__PURE__ */ jsx(
2275
+ CodeBlockView,
2276
+ {
2277
+ content: code,
2278
+ language: fileType.language,
2279
+ editable: true,
2280
+ onChange: session.updateActiveFile
2281
+ }
2282
+ ) })
1534
2283
  ] }),
1535
2284
  /* @__PURE__ */ jsx(
1536
2285
  EditHistory,
@@ -1596,6 +2345,77 @@ ${bobbinYaml}
1596
2345
  )
1597
2346
  ] });
1598
2347
  }
2348
+ function createManifest(services) {
2349
+ return {
2350
+ name: "preview",
2351
+ version: "1.0.0",
2352
+ platform: "browser",
2353
+ image: "@aprovan/patchwork-image-shadcn",
2354
+ services
2355
+ };
2356
+ }
2357
+ function WidgetPreview({
2358
+ code,
2359
+ compiler,
2360
+ services,
2361
+ enabled = true
2362
+ }) {
2363
+ const [loading, setLoading] = useState(false);
2364
+ const [error, setError] = useState(null);
2365
+ const containerRef = useRef(null);
2366
+ const mountedRef = useRef(null);
2367
+ useEffect(() => {
2368
+ if (!enabled || !compiler || !containerRef.current) return;
2369
+ let cancelled = false;
2370
+ const compileAndMount = async () => {
2371
+ setLoading(true);
2372
+ setError(null);
2373
+ try {
2374
+ if (mountedRef.current) {
2375
+ compiler.unmount(mountedRef.current);
2376
+ mountedRef.current = null;
2377
+ }
2378
+ const widget = await compiler.compile(code, createManifest(services), {
2379
+ typescript: true
2380
+ });
2381
+ if (cancelled || !containerRef.current) return;
2382
+ const mounted = await compiler.mount(widget, {
2383
+ target: containerRef.current,
2384
+ mode: "embedded"
2385
+ });
2386
+ mountedRef.current = mounted;
2387
+ } catch (err) {
2388
+ if (!cancelled) {
2389
+ setError(err instanceof Error ? err.message : "Failed to render preview");
2390
+ }
2391
+ } finally {
2392
+ if (!cancelled) {
2393
+ setLoading(false);
2394
+ }
2395
+ }
2396
+ };
2397
+ void compileAndMount();
2398
+ return () => {
2399
+ cancelled = true;
2400
+ if (mountedRef.current && compiler) {
2401
+ compiler.unmount(mountedRef.current);
2402
+ mountedRef.current = null;
2403
+ }
2404
+ };
2405
+ }, [code, compiler, enabled, services]);
2406
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2407
+ error && /* @__PURE__ */ jsxs("div", { className: "text-sm text-destructive flex items-center gap-2 p-3", children: [
2408
+ /* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0" }),
2409
+ /* @__PURE__ */ jsx("span", { children: error })
2410
+ ] }),
2411
+ loading && /* @__PURE__ */ jsxs("div", { className: "p-3 flex items-center gap-2 text-muted-foreground", children: [
2412
+ /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
2413
+ /* @__PURE__ */ jsx("span", { className: "text-sm", children: "Rendering preview..." })
2414
+ ] }),
2415
+ !compiler && enabled && !loading && !error && /* @__PURE__ */ jsx("div", { className: "p-3 text-sm text-muted-foreground", children: "Compiler not initialized" }),
2416
+ /* @__PURE__ */ jsx("div", { ref: containerRef, className: "w-full" })
2417
+ ] });
2418
+ }
1599
2419
  var VFS_BASE_URL = "/vfs";
1600
2420
  var vfsConfigCache = null;
1601
2421
  async function getVFSConfig() {
@@ -1659,7 +2479,7 @@ async function isVFSAvailable() {
1659
2479
  return false;
1660
2480
  }
1661
2481
  }
1662
- function createManifest(services) {
2482
+ function createManifest2(services) {
1663
2483
  return {
1664
2484
  name: "preview",
1665
2485
  version: "1.0.0",
@@ -1668,63 +2488,19 @@ function createManifest(services) {
1668
2488
  services
1669
2489
  };
1670
2490
  }
1671
- function useCodeCompiler(compiler, code, enabled, services) {
1672
- const [loading, setLoading] = useState(false);
1673
- const [error, setError] = useState(null);
1674
- const containerRef = useRef(null);
1675
- const mountedRef = useRef(null);
1676
- useEffect(() => {
1677
- if (!enabled || !compiler || !containerRef.current) return;
1678
- let cancelled = false;
1679
- async function compileAndMount() {
1680
- if (!containerRef.current || !compiler) return;
1681
- setLoading(true);
1682
- setError(null);
1683
- try {
1684
- if (mountedRef.current) {
1685
- compiler.unmount(mountedRef.current);
1686
- mountedRef.current = null;
1687
- }
1688
- const widget = await compiler.compile(
1689
- code,
1690
- createManifest(services),
1691
- { typescript: true }
1692
- );
1693
- if (cancelled) {
1694
- return;
1695
- }
1696
- const mounted = await compiler.mount(widget, {
1697
- target: containerRef.current,
1698
- mode: "embedded"
1699
- });
1700
- mountedRef.current = mounted;
1701
- } catch (err) {
1702
- if (!cancelled) {
1703
- setError(err instanceof Error ? err.message : "Failed to render JSX");
1704
- }
1705
- } finally {
1706
- if (!cancelled) {
1707
- setLoading(false);
1708
- }
1709
- }
1710
- }
1711
- compileAndMount();
1712
- return () => {
1713
- cancelled = true;
1714
- if (mountedRef.current && compiler) {
1715
- compiler.unmount(mountedRef.current);
1716
- mountedRef.current = null;
1717
- }
1718
- };
1719
- }, [code, compiler, enabled, services]);
1720
- return { containerRef, loading, error };
1721
- }
1722
- function CodePreview({ code: originalCode, compiler, services, filePath, entrypoint = "index.ts" }) {
2491
+ function CodePreview({
2492
+ code: originalCode,
2493
+ compiler,
2494
+ services,
2495
+ filePath,
2496
+ entrypoint = "index.ts",
2497
+ onOpenEditSession
2498
+ }) {
1723
2499
  const [isEditing, setIsEditing] = useState(false);
1724
2500
  const [showPreview, setShowPreview] = useState(true);
1725
2501
  const [currentCode, setCurrentCode] = useState(originalCode);
1726
2502
  const [editCount, setEditCount] = useState(0);
1727
- const [saveStatus, setSaveStatus] = useState("unsaved");
2503
+ const [saveStatus, setSaveStatus] = useState("saved");
1728
2504
  const [lastSavedCode, setLastSavedCode] = useState(originalCode);
1729
2505
  const [vfsPath, setVfsPath] = useState(null);
1730
2506
  const currentCodeRef = useRef(currentCode);
@@ -1760,6 +2536,14 @@ function CodePreview({ code: originalCode, compiler, services, filePath, entrypo
1760
2536
  useEffect(() => {
1761
2537
  isEditingRef.current = isEditing;
1762
2538
  }, [isEditing]);
2539
+ useEffect(() => {
2540
+ if (saveStatus === "saving") return;
2541
+ if (currentCode === lastSavedCode) {
2542
+ if (saveStatus !== "saved") setSaveStatus("saved");
2543
+ return;
2544
+ }
2545
+ if (saveStatus === "saved") setSaveStatus("unsaved");
2546
+ }, [currentCode, lastSavedCode, saveStatus]);
1763
2547
  useEffect(() => {
1764
2548
  let active = true;
1765
2549
  void (async () => {
@@ -1812,14 +2596,12 @@ function CodePreview({ code: originalCode, compiler, services, filePath, entrypo
1812
2596
  setSaveStatus("error");
1813
2597
  }
1814
2598
  }, [currentCode, getProjectId, getEntryFile]);
1815
- const { containerRef, loading, error } = useCodeCompiler(
1816
- compiler,
1817
- currentCode,
1818
- showPreview && !isEditing,
1819
- services
1820
- );
2599
+ const previewPath = filePath ?? entrypoint;
2600
+ const fileType = useMemo(() => getFileType(previewPath), [previewPath]);
2601
+ const canRenderWidget = fileType.category === "compilable";
1821
2602
  const compile = useCallback(
1822
2603
  async (code) => {
2604
+ if (!canRenderWidget) return { success: true };
1823
2605
  if (!compiler) return { success: true };
1824
2606
  const errors = [];
1825
2607
  const originalError = console.error;
@@ -1830,7 +2612,7 @@ function CodePreview({ code: originalCode, compiler, services, filePath, entrypo
1830
2612
  try {
1831
2613
  await compiler.compile(
1832
2614
  code,
1833
- createManifest(services),
2615
+ createManifest2(services),
1834
2616
  { typescript: true }
1835
2617
  );
1836
2618
  return { success: true };
@@ -1848,15 +2630,64 @@ ${errors.join("\n")}` : "";
1848
2630
  console.error = originalError;
1849
2631
  }
1850
2632
  },
1851
- [compiler, services]
2633
+ [canRenderWidget, compiler, services]
1852
2634
  );
1853
2635
  const handleRevert = () => {
1854
2636
  setCurrentCode(originalCode);
1855
2637
  setEditCount(0);
1856
2638
  };
1857
2639
  const hasChanges = currentCode !== originalCode;
2640
+ const previewBody = useMemo(() => {
2641
+ if (canRenderWidget) {
2642
+ return /* @__PURE__ */ jsx(
2643
+ WidgetPreview,
2644
+ {
2645
+ code: currentCode,
2646
+ compiler,
2647
+ services,
2648
+ enabled: showPreview && !isEditing
2649
+ }
2650
+ );
2651
+ }
2652
+ if (fileType.category === "media") {
2653
+ return /* @__PURE__ */ jsx(
2654
+ MediaPreview,
2655
+ {
2656
+ content: currentCode,
2657
+ mimeType: fileType.mimeType,
2658
+ fileName: previewPath
2659
+ }
2660
+ );
2661
+ }
2662
+ if (fileType.language === "markdown") {
2663
+ return /* @__PURE__ */ jsx("div", { className: "p-4 prose prose-sm dark:prose-invert max-w-none", children: /* @__PURE__ */ jsx(MarkdownPreview, { value: currentCode }) });
2664
+ }
2665
+ return /* @__PURE__ */ jsx(
2666
+ CodeBlockView,
2667
+ {
2668
+ content: currentCode,
2669
+ language: fileType.language
2670
+ }
2671
+ );
2672
+ }, [canRenderWidget, compiler, currentCode, fileType, isEditing, previewPath, services, showPreview]);
2673
+ const handleOpenEditor = useCallback(async () => {
2674
+ if (!onOpenEditSession) {
2675
+ setIsEditing(true);
2676
+ return;
2677
+ }
2678
+ const projectId = await getProjectId();
2679
+ const entryFile = getEntryFile();
2680
+ const initialProject = createSingleFileProject(currentCode, entryFile, projectId);
2681
+ onOpenEditSession({
2682
+ projectId,
2683
+ entryFile,
2684
+ filePath,
2685
+ initialCode: currentCode,
2686
+ initialProject
2687
+ });
2688
+ }, [onOpenEditSession, getProjectId, getEntryFile, currentCode, filePath]);
1858
2689
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1859
- /* @__PURE__ */ jsxs("div", { className: "my-3 border rounded-lg", children: [
2690
+ /* @__PURE__ */ jsxs("div", { className: "border rounded-lg overflow-hidden min-w-0", children: [
1860
2691
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2 bg-muted/50 border-b rounded-t-lg", children: [
1861
2692
  /* @__PURE__ */ jsx(Code, { className: "h-4 w-4 text-muted-foreground" }),
1862
2693
  editCount > 0 && /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground flex items-center gap-1", children: [
@@ -1865,19 +2696,6 @@ ${errors.join("\n")}` : "";
1865
2696
  " edit",
1866
2697
  editCount !== 1 ? "s" : ""
1867
2698
  ] }),
1868
- /* @__PURE__ */ jsx(
1869
- "button",
1870
- {
1871
- onClick: handleSave,
1872
- disabled: saveStatus === "saving",
1873
- className: `px-2 py-1 text-xs rounded flex items-center gap-1 ${saveStatus === "saved" ? "text-green-600" : saveStatus === "error" ? "text-destructive hover:bg-muted" : "text-muted-foreground hover:bg-muted"}`,
1874
- title: saveStatus === "saved" ? "Saved to disk" : saveStatus === "saving" ? "Saving..." : saveStatus === "error" ? "Save failed - click to retry" : "Click to save",
1875
- children: saveStatus === "saving" ? /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : /* @__PURE__ */ jsxs("span", { className: "relative", children: [
1876
- /* @__PURE__ */ jsx(Cloud, { className: "h-3 w-3" }),
1877
- saveStatus === "saved" && /* @__PURE__ */ jsx(Check, { className: "h-2 w-2 absolute -bottom-0.5 -right-0.5 stroke-[3]" })
1878
- ] })
1879
- }
1880
- ),
1881
2699
  /* @__PURE__ */ jsxs("div", { className: "ml-auto flex gap-1", children: [
1882
2700
  hasChanges && /* @__PURE__ */ jsx(
1883
2701
  "button",
@@ -1891,17 +2709,26 @@ ${errors.join("\n")}` : "";
1891
2709
  /* @__PURE__ */ jsx(
1892
2710
  "button",
1893
2711
  {
1894
- onClick: () => setIsEditing(true),
2712
+ onClick: () => void handleOpenEditor(),
1895
2713
  className: "px-2 py-1 text-xs rounded flex items-center gap-1 hover:bg-muted",
1896
2714
  title: "Edit component",
1897
2715
  children: /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3" })
1898
2716
  }
1899
2717
  ),
2718
+ /* @__PURE__ */ jsx(
2719
+ SaveStatusButton,
2720
+ {
2721
+ status: saveStatus,
2722
+ onClick: handleSave,
2723
+ disabled: saveStatus === "saving",
2724
+ tone: "muted"
2725
+ }
2726
+ ),
1900
2727
  /* @__PURE__ */ jsxs(
1901
2728
  "button",
1902
2729
  {
1903
2730
  onClick: () => setShowPreview(!showPreview),
1904
- className: `px-2 py-1 text-xs rounded flex items-center gap-1 ${showPreview ? "bg-primary text-primary-foreground" : "hover:bg-primary/20 text-primary"}`,
2731
+ className: `w-[5rem] px-2 py-1 text-xs rounded flex items-center gap-1 ${showPreview ? "bg-primary text-primary-foreground" : "hover:bg-primary/20 text-primary"}`,
1905
2732
  children: [
1906
2733
  showPreview ? /* @__PURE__ */ jsx(Eye, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Code, { className: "h-3 w-3" }),
1907
2734
  showPreview ? "Preview" : "Code"
@@ -1910,16 +2737,13 @@ ${errors.join("\n")}` : "";
1910
2737
  )
1911
2738
  ] })
1912
2739
  ] }),
1913
- showPreview ? /* @__PURE__ */ jsxs("div", { className: "bg-white", children: [
1914
- error ? /* @__PURE__ */ jsxs("div", { className: "p-3 text-sm text-destructive flex items-center gap-2", children: [
1915
- /* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0" }),
1916
- /* @__PURE__ */ jsx("span", { children: error })
1917
- ] }) : loading ? /* @__PURE__ */ jsxs("div", { className: "p-3 flex items-center gap-2 text-muted-foreground", children: [
1918
- /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
1919
- /* @__PURE__ */ jsx("span", { className: "text-sm", children: "Rendering preview..." })
1920
- ] }) : !compiler ? /* @__PURE__ */ jsx("div", { className: "p-3 text-sm text-muted-foreground", children: "Compiler not initialized" }) : null,
1921
- /* @__PURE__ */ jsx("div", { ref: containerRef })
1922
- ] }) : /* @__PURE__ */ jsx("div", { className: "p-3 bg-muted/30 overflow-auto max-h-96", children: /* @__PURE__ */ jsx("pre", { className: "text-xs whitespace-pre-wrap break-words m-0", children: /* @__PURE__ */ jsx("code", { children: currentCode }) }) })
2740
+ showPreview ? /* @__PURE__ */ jsx("div", { className: "bg-white overflow-y-auto overflow-x-hidden max-h-[60vh]", children: previewBody }) : /* @__PURE__ */ jsx("div", { className: "bg-muted/30 overflow-auto max-h-[60vh]", children: /* @__PURE__ */ jsx(
2741
+ CodeBlockView,
2742
+ {
2743
+ content: currentCode,
2744
+ language: fileType.language
2745
+ }
2746
+ ) })
1923
2747
  ] }),
1924
2748
  /* @__PURE__ */ jsx(
1925
2749
  EditModal,
@@ -1937,6 +2761,7 @@ ${errors.join("\n")}` : "";
1937
2761
  const entryFile = getEntryFile();
1938
2762
  const project = createSingleFileProject(finalCode, entryFile, projectId);
1939
2763
  await saveProject(project);
2764
+ setLastSavedCode(finalCode);
1940
2765
  setSaveStatus("saved");
1941
2766
  } catch (err) {
1942
2767
  console.warn("[VFS] Failed to save project:", err);
@@ -1947,49 +2772,77 @@ ${errors.join("\n")}` : "";
1947
2772
  },
1948
2773
  originalCode: currentCode,
1949
2774
  compile,
1950
- renderPreview: (code) => /* @__PURE__ */ jsx(ModalPreview, { code, compiler, services })
2775
+ renderPreview: (code) => /* @__PURE__ */ jsx(
2776
+ WidgetPreview,
2777
+ {
2778
+ code,
2779
+ compiler,
2780
+ services
2781
+ }
2782
+ )
1951
2783
  }
1952
2784
  )
1953
2785
  ] });
1954
2786
  }
1955
- function ModalPreview({
1956
- code,
1957
- compiler,
1958
- services
2787
+ function DefaultBadge({
2788
+ children,
2789
+ className = ""
1959
2790
  }) {
1960
- const { containerRef, loading, error } = useCodeCompiler(compiler, code, true, services);
1961
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1962
- error && /* @__PURE__ */ jsxs("div", { className: "text-sm text-destructive flex items-center gap-2", children: [
1963
- /* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0" }),
1964
- /* @__PURE__ */ jsx("span", { children: error })
1965
- ] }),
1966
- loading && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-muted-foreground", children: [
1967
- /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
1968
- /* @__PURE__ */ jsx("span", { className: "text-sm", children: "Rendering preview..." })
1969
- ] }),
1970
- !compiler && !loading && !error && /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "Compiler not initialized" }),
1971
- /* @__PURE__ */ jsx("div", { ref: containerRef })
1972
- ] });
1973
- }
1974
- function DefaultBadge({ children, className = "" }) {
1975
- return /* @__PURE__ */ jsx("span", { className: `inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${className}`, children });
2791
+ return /* @__PURE__ */ jsx(
2792
+ "span",
2793
+ {
2794
+ className: `inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${className}`,
2795
+ children
2796
+ }
2797
+ );
1976
2798
  }
1977
- function DefaultDialog({ children, open, onOpenChange }) {
2799
+ function DefaultDialog({
2800
+ children,
2801
+ open,
2802
+ onOpenChange
2803
+ }) {
1978
2804
  if (!open) return null;
1979
- return /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 bg-black/50", onClick: () => onOpenChange?.(false), children: /* @__PURE__ */ jsx("div", { className: "fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 bg-background p-6 shadow-lg rounded-lg", onClick: (e) => e.stopPropagation(), children }) });
2805
+ return /* @__PURE__ */ jsx(
2806
+ "div",
2807
+ {
2808
+ className: "fixed inset-0 z-50 bg-black/50",
2809
+ onClick: () => onOpenChange?.(false),
2810
+ children: /* @__PURE__ */ jsx(
2811
+ "div",
2812
+ {
2813
+ className: "fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 bg-background p-6 shadow-lg rounded-lg",
2814
+ onClick: (e) => e.stopPropagation(),
2815
+ children
2816
+ }
2817
+ )
2818
+ }
2819
+ );
1980
2820
  }
1981
2821
  function ServicesInspector({
1982
2822
  namespaces,
1983
2823
  services = [],
1984
2824
  BadgeComponent = DefaultBadge,
1985
- DialogComponent = DefaultDialog
2825
+ DialogComponent = DefaultDialog,
2826
+ DialogHeaderComponent = ({ children }) => /* @__PURE__ */ jsx("div", { className: "flex justify-between items-center mb-4", children }),
2827
+ DialogContentComponent = ({ children, className = "" }) => /* @__PURE__ */ jsx("div", { className, children }),
2828
+ DialogCloseComponent = ({ onClose }) => /* @__PURE__ */ jsx(
2829
+ "button",
2830
+ {
2831
+ onClick: () => onClose?.(),
2832
+ className: "text-muted-foreground hover:text-foreground",
2833
+ children: "\xD7"
2834
+ }
2835
+ )
1986
2836
  }) {
1987
2837
  const [open, setOpen] = useState(false);
1988
2838
  if (namespaces.length === 0) return null;
1989
- const groupedServices = services.reduce((acc, svc) => {
1990
- (acc[svc.namespace] ??= []).push(svc);
1991
- return acc;
1992
- }, {});
2839
+ const groupedServices = services.reduce(
2840
+ (acc, svc) => {
2841
+ (acc[svc.namespace] ??= []).push(svc);
2842
+ return acc;
2843
+ },
2844
+ {}
2845
+ );
1993
2846
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1994
2847
  /* @__PURE__ */ jsxs(
1995
2848
  "button",
@@ -2007,11 +2860,11 @@ function ServicesInspector({
2007
2860
  }
2008
2861
  ),
2009
2862
  /* @__PURE__ */ jsxs(DialogComponent, { open, onOpenChange: setOpen, children: [
2010
- /* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center mb-4", children: [
2011
- /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Available Services" }),
2012
- /* @__PURE__ */ jsx("button", { onClick: () => setOpen(false), className: "text-muted-foreground hover:text-foreground", children: "\xD7" })
2863
+ /* @__PURE__ */ jsxs(DialogHeaderComponent, { children: [
2864
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Services" }),
2865
+ /* @__PURE__ */ jsx(DialogCloseComponent, { onClose: () => setOpen(false) })
2013
2866
  ] }),
2014
- /* @__PURE__ */ jsx("div", { className: "space-y-3 max-h-96 overflow-auto", children: namespaces.map((ns) => /* @__PURE__ */ jsxs("details", { open: namespaces.length === 1, children: [
2867
+ /* @__PURE__ */ jsx(DialogContentComponent, { className: "space-y-3 max-h-96 overflow-auto", children: namespaces.map((ns) => /* @__PURE__ */ jsxs("details", { open: namespaces.length === 1, children: [
2015
2868
  /* @__PURE__ */ jsxs("summary", { className: "flex items-center gap-2 w-full p-2 rounded bg-muted/50 hover:bg-muted transition-colors cursor-pointer", children: [
2016
2869
  /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 text-muted-foreground" }),
2017
2870
  /* @__PURE__ */ jsx("span", { className: "font-medium text-sm", children: ns }),
@@ -2023,11 +2876,11 @@ function ServicesInspector({
2023
2876
  ] }),
2024
2877
  /* @__PURE__ */ jsx("div", { className: "ml-6 mt-2 space-y-2", children: groupedServices[ns]?.map((svc) => /* @__PURE__ */ jsxs("details", { children: [
2025
2878
  /* @__PURE__ */ jsxs("summary", { className: "flex items-center gap-2 w-full text-left text-sm hover:text-foreground text-muted-foreground transition-colors cursor-pointer", children: [
2026
- /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3" }),
2879
+ svc.parameters && /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3" }),
2027
2880
  /* @__PURE__ */ jsx("code", { className: "font-mono text-xs", children: svc.procedure }),
2028
2881
  /* @__PURE__ */ jsx("span", { className: "truncate text-xs opacity-70", children: svc.description })
2029
2882
  ] }),
2030
- /* @__PURE__ */ jsx("div", { className: "ml-5 mt-1 p-2 rounded border bg-muted/30 overflow-auto max-h-48", children: /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono whitespace-pre-wrap break-words m-0", children: JSON.stringify(svc.parameters.jsonSchema, null, 2) }) })
2883
+ svc.parameters && /* @__PURE__ */ jsx("div", { className: "ml-5 mt-1 p-2 rounded border bg-muted/30 overflow-auto max-h-48", children: /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono whitespace-pre-wrap break-words m-0", children: JSON.stringify(svc.parameters, null, 2) }) })
2031
2884
  ] }, svc.name)) ?? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "No tool details available" }) })
2032
2885
  ] }, ns)) })
2033
2886
  ] })
@@ -2159,4 +3012,4 @@ function cn(...inputs) {
2159
3012
  return twMerge(clsx(inputs));
2160
3013
  }
2161
3014
 
2162
- export { CodeBlockExtension, CodeBlockView, CodePreview, EditHistory, EditModal, FileTree, MarkdownEditor, MediaPreview, SaveConfirmDialog, ServicesInspector, applyDiffs, cn, extractCodeBlocks, extractProject, extractSummary, extractTextWithoutDiffs, findDiffMarkers, findFirstCodeBlock, getActiveContent, getCodeBlockLanguages, getFileType, getFiles, getLanguageFromExt, getMimeType, getVFSConfig, getVFSStore, hasCodeBlock, hasDiffBlocks, isCompilable, isImageFile, isMediaFile, isTextFile, isVFSAvailable, isVideoFile, listProjects, loadProject, parseCodeBlockAttributes, parseCodeBlocks, parseDiffs, parseEditResponse, sanitizeDiffMarkers, saveFile, saveProject, sendEditRequest, useEditSession };
3015
+ export { CodeBlockExtension, CodeBlockView, CodePreview, EditHistory, EditModal, FileTree, MarkdownEditor, MarkdownPreview, MediaPreview, SaveConfirmDialog, ServicesInspector, WidgetPreview, applyDiffs, cn, extractCodeBlocks, extractProject, extractSummary, extractTextWithoutDiffs, findDiffMarkers, findFirstCodeBlock, getActiveContent, getCodeBlockLanguages, getFileType, getFiles, getLanguageFromExt, getMimeType, getVFSConfig, getVFSStore, hasCodeBlock, hasDiffBlocks, isCompilable, isImageFile, isMarkdownFile, isMediaFile, isPreviewable, isTextFile, isVFSAvailable, isVideoFile, listProjects, loadProject, parseCodeBlockAttributes, parseCodeBlocks, parseDiffs, parseEditResponse, sanitizeDiffMarkers, saveFile, saveProject, sendEditRequest, useEditSession };