@aprovan/patchwork-editor 0.1.2-dev.03aaf5b → 0.1.2-dev.f456953
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/.turbo/turbo-build.log +2 -2
- package/dist/components/CodePreview.d.ts +10 -1
- package/dist/components/MarkdownPreview.d.ts +8 -0
- package/dist/components/SaveStatusButton.d.ts +9 -0
- package/dist/components/ServicesInspector.d.ts +3 -4
- package/dist/components/WidgetPreview.d.ts +8 -0
- package/dist/components/edit/EditModal.d.ts +2 -1
- package/dist/components/edit/FileTree.d.ts +22 -3
- package/dist/components/edit/api.d.ts +1 -1
- package/dist/components/edit/fileTypes.d.ts +3 -1
- package/dist/components/edit/useEditSession.d.ts +1 -0
- package/dist/components/index.d.ts +7 -5
- package/dist/index.d.ts +3 -3
- package/dist/index.js +927 -179
- package/package.json +3 -3
- package/src/components/CodePreview.tsx +118 -160
- package/src/components/MarkdownPreview.tsx +147 -0
- package/src/components/SaveStatusButton.tsx +55 -0
- package/src/components/ServicesInspector.tsx +102 -38
- package/src/components/WidgetPreview.tsx +102 -0
- package/src/components/edit/EditModal.tsx +83 -26
- package/src/components/edit/FileTree.tsx +523 -28
- package/src/components/edit/api.ts +14 -9
- package/src/components/edit/fileTypes.ts +82 -54
- package/src/components/edit/useEditSession.ts +13 -3
- package/src/components/index.ts +7 -5
- package/src/index.ts +10 -6
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,
|
|
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';
|
|
@@ -371,7 +371,11 @@ async function sendEditRequest(request, options = {}) {
|
|
|
371
371
|
}
|
|
372
372
|
const text = await streamResponse(response, onProgress);
|
|
373
373
|
if (!hasDiffBlocks(text)) {
|
|
374
|
-
|
|
374
|
+
return {
|
|
375
|
+
newCode: request.code,
|
|
376
|
+
summary: text.trim(),
|
|
377
|
+
progressNotes: []
|
|
378
|
+
};
|
|
375
379
|
}
|
|
376
380
|
const parsed = parseEditResponse(text);
|
|
377
381
|
const result = applyDiffs(request.code, parsed.diffs, { sanitize });
|
|
@@ -440,6 +444,7 @@ function useEditSession(options) {
|
|
|
440
444
|
const {
|
|
441
445
|
originalCode,
|
|
442
446
|
originalProject: providedProject,
|
|
447
|
+
initialActiveFile,
|
|
443
448
|
compile,
|
|
444
449
|
apiEndpoint
|
|
445
450
|
} = options;
|
|
@@ -455,7 +460,9 @@ function useEditSession(options) {
|
|
|
455
460
|
);
|
|
456
461
|
const lastSyncedProjectRef = useRef(originalProject);
|
|
457
462
|
const [project, setProject] = useState(originalProject);
|
|
458
|
-
const [activeFile, setActiveFile] = useState(
|
|
463
|
+
const [activeFile, setActiveFile] = useState(
|
|
464
|
+
initialActiveFile && originalProject.files.has(initialActiveFile) ? initialActiveFile : originalProject.entry
|
|
465
|
+
);
|
|
459
466
|
const [history, setHistory] = useState([]);
|
|
460
467
|
const [isApplying, setIsApplying] = useState(false);
|
|
461
468
|
const [error, setError] = useState(null);
|
|
@@ -465,12 +472,14 @@ function useEditSession(options) {
|
|
|
465
472
|
if (originalProject !== lastSyncedProjectRef.current) {
|
|
466
473
|
lastSyncedProjectRef.current = originalProject;
|
|
467
474
|
setProject(originalProject);
|
|
468
|
-
setActiveFile(
|
|
475
|
+
setActiveFile(
|
|
476
|
+
initialActiveFile && originalProject.files.has(initialActiveFile) ? initialActiveFile : originalProject.entry
|
|
477
|
+
);
|
|
469
478
|
setHistory([]);
|
|
470
479
|
setError(null);
|
|
471
480
|
setStreamingNotes([]);
|
|
472
481
|
}
|
|
473
|
-
}, [originalProject]);
|
|
482
|
+
}, [originalProject, initialActiveFile]);
|
|
474
483
|
const performEdit = useCallback(
|
|
475
484
|
async (currentCode2, prompt, isRetry = false) => {
|
|
476
485
|
const entries = [];
|
|
@@ -851,11 +860,181 @@ function MarkdownEditor({
|
|
|
851
860
|
}
|
|
852
861
|
);
|
|
853
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
|
+
}
|
|
854
1013
|
|
|
855
1014
|
// src/components/edit/fileTypes.ts
|
|
856
1015
|
var COMPILABLE_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"];
|
|
857
|
-
var MEDIA_EXTENSIONS = [
|
|
858
|
-
|
|
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
|
+
];
|
|
859
1038
|
var EXTENSION_TO_LANGUAGE = {
|
|
860
1039
|
".tsx": "tsx",
|
|
861
1040
|
".jsx": "jsx",
|
|
@@ -939,6 +1118,12 @@ function isMediaFile(path) {
|
|
|
939
1118
|
function isTextFile(path) {
|
|
940
1119
|
return TEXT_EXTENSIONS.includes(getExtension(path));
|
|
941
1120
|
}
|
|
1121
|
+
function isMarkdownFile(path) {
|
|
1122
|
+
return getExtension(path) === ".md";
|
|
1123
|
+
}
|
|
1124
|
+
function isPreviewable(path) {
|
|
1125
|
+
return isCompilable(path) || isMarkdownFile(path);
|
|
1126
|
+
}
|
|
942
1127
|
function getLanguageFromExt(path) {
|
|
943
1128
|
const ext = getExtension(path);
|
|
944
1129
|
return EXTENSION_TO_LANGUAGE[ext] ?? null;
|
|
@@ -955,6 +1140,17 @@ function isVideoFile(path) {
|
|
|
955
1140
|
const ext = getExtension(path);
|
|
956
1141
|
return [".mp4", ".mov", ".webm"].includes(ext);
|
|
957
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
|
+
}
|
|
958
1154
|
function buildTree(files) {
|
|
959
1155
|
const root = { name: "", path: "", isDir: true, children: [] };
|
|
960
1156
|
for (const file of files) {
|
|
@@ -977,14 +1173,26 @@ function buildTree(files) {
|
|
|
977
1173
|
current = child;
|
|
978
1174
|
}
|
|
979
1175
|
}
|
|
980
|
-
root.children
|
|
981
|
-
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
982
|
-
return a.name.localeCompare(b.name);
|
|
983
|
-
});
|
|
1176
|
+
sortNodes(root.children);
|
|
984
1177
|
return root;
|
|
985
1178
|
}
|
|
986
|
-
function TreeNodeComponent({
|
|
987
|
-
|
|
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);
|
|
988
1196
|
const [isHovered, setIsHovered] = useState(false);
|
|
989
1197
|
const fileInputRef = useRef(null);
|
|
990
1198
|
const handleUploadClick = useCallback((e) => {
|
|
@@ -1003,48 +1211,119 @@ function TreeNodeComponent({ node, activeFile, onSelect, onReplaceFile, depth =
|
|
|
1003
1211
|
reader.readAsDataURL(file);
|
|
1004
1212
|
e.target.value = "";
|
|
1005
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]);
|
|
1006
1220
|
if (!node.name) {
|
|
1007
1221
|
return /* @__PURE__ */ jsx(Fragment, { children: node.children.map((child) => /* @__PURE__ */ jsx(
|
|
1008
1222
|
TreeNodeComponent,
|
|
1009
1223
|
{
|
|
1010
1224
|
node: child,
|
|
1011
|
-
|
|
1225
|
+
activePath,
|
|
1012
1226
|
onSelect,
|
|
1227
|
+
onSelectDirectory,
|
|
1013
1228
|
onReplaceFile,
|
|
1229
|
+
onOpenInEditor,
|
|
1230
|
+
openInEditorMode,
|
|
1231
|
+
openInEditorIcon,
|
|
1232
|
+
openInEditorTitle,
|
|
1233
|
+
pinnedPaths,
|
|
1234
|
+
onTogglePin,
|
|
1235
|
+
pageSize,
|
|
1014
1236
|
depth
|
|
1015
1237
|
},
|
|
1016
1238
|
child.path
|
|
1017
1239
|
)) });
|
|
1018
1240
|
}
|
|
1019
|
-
const isActive = node.path ===
|
|
1241
|
+
const isActive = node.path === activePath;
|
|
1020
1242
|
const isMedia = !node.isDir && isMediaFile(node.path);
|
|
1021
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
|
+
);
|
|
1022
1252
|
if (node.isDir) {
|
|
1023
1253
|
return /* @__PURE__ */ jsxs("div", { children: [
|
|
1024
1254
|
/* @__PURE__ */ jsxs(
|
|
1025
1255
|
"button",
|
|
1026
1256
|
{
|
|
1027
|
-
onClick: () =>
|
|
1028
|
-
|
|
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),
|
|
1029
1264
|
style: { paddingLeft: `${depth * 12 + 8}px` },
|
|
1030
1265
|
children: [
|
|
1031
1266
|
expanded ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 shrink-0" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 shrink-0" }),
|
|
1032
1267
|
/* @__PURE__ */ jsx(Folder, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
|
|
1033
|
-
/* @__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
|
+
)
|
|
1034
1287
|
]
|
|
1035
1288
|
}
|
|
1036
1289
|
),
|
|
1037
|
-
expanded && /* @__PURE__ */
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
+
] })
|
|
1048
1327
|
] });
|
|
1049
1328
|
}
|
|
1050
1329
|
return /* @__PURE__ */ jsxs(
|
|
@@ -1062,7 +1341,25 @@ function TreeNodeComponent({ node, activeFile, onSelect, onReplaceFile, depth =
|
|
|
1062
1341
|
style: { paddingLeft: `${depth * 12 + 20}px` },
|
|
1063
1342
|
children: [
|
|
1064
1343
|
/* @__PURE__ */ jsx(File, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
|
|
1065
|
-
/* @__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
|
+
),
|
|
1066
1363
|
showUpload && /* @__PURE__ */ jsx(
|
|
1067
1364
|
"span",
|
|
1068
1365
|
{
|
|
@@ -1089,17 +1386,309 @@ function TreeNodeComponent({ node, activeFile, onSelect, onReplaceFile, depth =
|
|
|
1089
1386
|
}
|
|
1090
1387
|
);
|
|
1091
1388
|
}
|
|
1092
|
-
function
|
|
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
|
+
}) {
|
|
1093
1596
|
const tree = useMemo(() => buildTree(files), [files]);
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
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(
|
|
1097
1678
|
TreeNodeComponent,
|
|
1098
1679
|
{
|
|
1099
1680
|
node: tree,
|
|
1100
|
-
|
|
1681
|
+
activePath: selectedPath,
|
|
1101
1682
|
onSelect: onSelectFile,
|
|
1102
|
-
|
|
1683
|
+
onSelectDirectory,
|
|
1684
|
+
onReplaceFile,
|
|
1685
|
+
onOpenInEditor,
|
|
1686
|
+
openInEditorMode,
|
|
1687
|
+
openInEditorIcon,
|
|
1688
|
+
openInEditorTitle,
|
|
1689
|
+
pinnedPaths,
|
|
1690
|
+
onTogglePin,
|
|
1691
|
+
pageSize
|
|
1103
1692
|
}
|
|
1104
1693
|
) })
|
|
1105
1694
|
] });
|
|
@@ -1408,6 +1997,7 @@ function EditModal({
|
|
|
1408
1997
|
renderError,
|
|
1409
1998
|
previewError,
|
|
1410
1999
|
previewLoading,
|
|
2000
|
+
initialTreePath,
|
|
1411
2001
|
initialState = {},
|
|
1412
2002
|
hideFileTree = false,
|
|
1413
2003
|
...sessionOptions
|
|
@@ -1422,17 +2012,27 @@ function EditModal({
|
|
|
1422
2012
|
const [pillContainer, setPillContainer] = useState(null);
|
|
1423
2013
|
const [showConfirm, setShowConfirm] = useState(false);
|
|
1424
2014
|
const [isSaving, setIsSaving] = useState(false);
|
|
2015
|
+
const [saveStatus, setSaveStatus] = useState("saved");
|
|
2016
|
+
const [lastSavedSnapshot, setLastSavedSnapshot] = useState("");
|
|
1425
2017
|
const [saveError, setSaveError] = useState(null);
|
|
1426
2018
|
const [pendingClose, setPendingClose] = useState(null);
|
|
2019
|
+
const [treePath, setTreePath] = useState(initialTreePath ?? "");
|
|
2020
|
+
const wasOpenRef = useRef(false);
|
|
1427
2021
|
const currentCodeRef = useRef("");
|
|
1428
2022
|
const session = useEditSession(sessionOptions);
|
|
1429
2023
|
const code = getActiveContent(session);
|
|
2024
|
+
const effectiveTreePath = treePath || session.activeFile;
|
|
1430
2025
|
currentCodeRef.current = code;
|
|
1431
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
|
+
);
|
|
1432
2031
|
const hasChanges = code !== (session.originalProject.files.get(session.activeFile)?.content ?? "");
|
|
1433
2032
|
const fileType = useMemo(() => getFileType(session.activeFile), [session.activeFile]);
|
|
1434
2033
|
const isCompilableFile = isCompilable(session.activeFile);
|
|
1435
|
-
const
|
|
2034
|
+
const isMarkdown = isMarkdownFile(session.activeFile);
|
|
2035
|
+
const showPreviewToggle = isCompilableFile || isMarkdown;
|
|
1436
2036
|
const handleBobbinChanges = useCallback((changes) => {
|
|
1437
2037
|
setBobbinChanges(changes);
|
|
1438
2038
|
}, []);
|
|
@@ -1454,6 +2054,26 @@ ${bobbinYaml}
|
|
|
1454
2054
|
setBobbinChanges([]);
|
|
1455
2055
|
};
|
|
1456
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]);
|
|
1457
2077
|
const handleClose = useCallback(() => {
|
|
1458
2078
|
const editCount = session.history.length;
|
|
1459
2079
|
const finalCode = code;
|
|
@@ -1470,6 +2090,7 @@ ${bobbinYaml}
|
|
|
1470
2090
|
const handleSaveAndClose = useCallback(async () => {
|
|
1471
2091
|
if (!pendingClose || !hasSaveHandler) return;
|
|
1472
2092
|
setIsSaving(true);
|
|
2093
|
+
setSaveStatus("saving");
|
|
1473
2094
|
setSaveError(null);
|
|
1474
2095
|
try {
|
|
1475
2096
|
if (onSaveProject) {
|
|
@@ -1477,6 +2098,8 @@ ${bobbinYaml}
|
|
|
1477
2098
|
} else if (onSave) {
|
|
1478
2099
|
await onSave(pendingClose.code);
|
|
1479
2100
|
}
|
|
2101
|
+
setLastSavedSnapshot(projectSnapshot);
|
|
2102
|
+
setSaveStatus("saved");
|
|
1480
2103
|
setShowConfirm(false);
|
|
1481
2104
|
setEditInput("");
|
|
1482
2105
|
session.clearError();
|
|
@@ -1484,10 +2107,11 @@ ${bobbinYaml}
|
|
|
1484
2107
|
setPendingClose(null);
|
|
1485
2108
|
} catch (e) {
|
|
1486
2109
|
setSaveError(e instanceof Error ? e.message : "Save failed");
|
|
2110
|
+
setSaveStatus("error");
|
|
1487
2111
|
} finally {
|
|
1488
2112
|
setIsSaving(false);
|
|
1489
2113
|
}
|
|
1490
|
-
}, [pendingClose, onSave, onSaveProject, session, onClose]);
|
|
2114
|
+
}, [pendingClose, onSave, onSaveProject, session, onClose, projectSnapshot, hasSaveHandler]);
|
|
1491
2115
|
const handleDiscard = useCallback(() => {
|
|
1492
2116
|
if (!pendingClose) return;
|
|
1493
2117
|
setShowConfirm(false);
|
|
@@ -1504,6 +2128,7 @@ ${bobbinYaml}
|
|
|
1504
2128
|
const handleDirectSave = useCallback(async () => {
|
|
1505
2129
|
if (!hasSaveHandler) return;
|
|
1506
2130
|
setIsSaving(true);
|
|
2131
|
+
setSaveStatus("saving");
|
|
1507
2132
|
setSaveError(null);
|
|
1508
2133
|
try {
|
|
1509
2134
|
if (onSaveProject) {
|
|
@@ -1511,12 +2136,15 @@ ${bobbinYaml}
|
|
|
1511
2136
|
} else if (onSave && currentCodeRef.current) {
|
|
1512
2137
|
await onSave(currentCodeRef.current);
|
|
1513
2138
|
}
|
|
2139
|
+
setLastSavedSnapshot(projectSnapshot);
|
|
2140
|
+
setSaveStatus("saved");
|
|
1514
2141
|
} catch (e) {
|
|
1515
2142
|
setSaveError(e instanceof Error ? e.message : "Save failed");
|
|
2143
|
+
setSaveStatus("error");
|
|
1516
2144
|
} finally {
|
|
1517
2145
|
setIsSaving(false);
|
|
1518
2146
|
}
|
|
1519
|
-
}, [onSave, onSaveProject, session.project]);
|
|
2147
|
+
}, [onSave, onSaveProject, session.project, hasSaveHandler, projectSnapshot]);
|
|
1520
2148
|
if (!isOpen) return null;
|
|
1521
2149
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1522
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: [
|
|
@@ -1545,30 +2173,26 @@ ${bobbinYaml}
|
|
|
1545
2173
|
children: showTree ? /* @__PURE__ */ jsx(FileCode, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(FolderTree, { className: "h-3 w-3" })
|
|
1546
2174
|
}
|
|
1547
2175
|
),
|
|
2176
|
+
hasSaveHandler && /* @__PURE__ */ jsx(
|
|
2177
|
+
SaveStatusButton,
|
|
2178
|
+
{
|
|
2179
|
+
status: saveStatus,
|
|
2180
|
+
onClick: handleDirectSave,
|
|
2181
|
+
disabled: isSaving,
|
|
2182
|
+
tone: "primary"
|
|
2183
|
+
}
|
|
2184
|
+
),
|
|
1548
2185
|
showPreviewToggle && /* @__PURE__ */ jsxs(
|
|
1549
2186
|
"button",
|
|
1550
2187
|
{
|
|
1551
2188
|
onClick: () => setShowPreview(!showPreview),
|
|
1552
|
-
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"}`,
|
|
1553
2190
|
children: [
|
|
1554
2191
|
showPreview ? /* @__PURE__ */ jsx(Eye, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Code, { className: "h-3 w-3" }),
|
|
1555
2192
|
showPreview ? "Preview" : "Code"
|
|
1556
2193
|
]
|
|
1557
2194
|
}
|
|
1558
2195
|
),
|
|
1559
|
-
hasSaveHandler && /* @__PURE__ */ jsxs(
|
|
1560
|
-
"button",
|
|
1561
|
-
{
|
|
1562
|
-
onClick: handleDirectSave,
|
|
1563
|
-
disabled: isSaving,
|
|
1564
|
-
className: "px-2 py-1 text-xs rounded flex items-center gap-1 hover:bg-primary/20 text-primary disabled:opacity-50",
|
|
1565
|
-
title: "Save changes",
|
|
1566
|
-
children: [
|
|
1567
|
-
isSaving ? /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : /* @__PURE__ */ jsx(Save, { className: "h-3 w-3" }),
|
|
1568
|
-
"Save"
|
|
1569
|
-
]
|
|
1570
|
-
}
|
|
1571
|
-
),
|
|
1572
2196
|
/* @__PURE__ */ jsxs(
|
|
1573
2197
|
"button",
|
|
1574
2198
|
{
|
|
@@ -1589,7 +2213,12 @@ ${bobbinYaml}
|
|
|
1589
2213
|
{
|
|
1590
2214
|
files,
|
|
1591
2215
|
activeFile: session.activeFile,
|
|
1592
|
-
|
|
2216
|
+
activePath: effectiveTreePath,
|
|
2217
|
+
onSelectFile: (path) => {
|
|
2218
|
+
setTreePath(path);
|
|
2219
|
+
session.setActiveFile(path);
|
|
2220
|
+
},
|
|
2221
|
+
onSelectDirectory: (path) => setTreePath(path),
|
|
1593
2222
|
onReplaceFile: session.replaceFile
|
|
1594
2223
|
}
|
|
1595
2224
|
),
|
|
@@ -1620,7 +2249,14 @@ ${bobbinYaml}
|
|
|
1620
2249
|
editable: true,
|
|
1621
2250
|
onChange: session.updateActiveFile
|
|
1622
2251
|
}
|
|
1623
|
-
) :
|
|
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(
|
|
1624
2260
|
CodeBlockView,
|
|
1625
2261
|
{
|
|
1626
2262
|
content: code,
|
|
@@ -1635,7 +2271,15 @@ ${bobbinYaml}
|
|
|
1635
2271
|
mimeType: getMimeType(session.activeFile),
|
|
1636
2272
|
fileName: session.activeFile.split("/").pop() ?? session.activeFile
|
|
1637
2273
|
}
|
|
1638
|
-
) : /* @__PURE__ */ jsx(
|
|
2274
|
+
) : /* @__PURE__ */ jsx(
|
|
2275
|
+
CodeBlockView,
|
|
2276
|
+
{
|
|
2277
|
+
content: code,
|
|
2278
|
+
language: fileType.language,
|
|
2279
|
+
editable: true,
|
|
2280
|
+
onChange: session.updateActiveFile
|
|
2281
|
+
}
|
|
2282
|
+
) })
|
|
1639
2283
|
] }),
|
|
1640
2284
|
/* @__PURE__ */ jsx(
|
|
1641
2285
|
EditHistory,
|
|
@@ -1701,6 +2345,77 @@ ${bobbinYaml}
|
|
|
1701
2345
|
)
|
|
1702
2346
|
] });
|
|
1703
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
|
+
}
|
|
1704
2419
|
var VFS_BASE_URL = "/vfs";
|
|
1705
2420
|
var vfsConfigCache = null;
|
|
1706
2421
|
async function getVFSConfig() {
|
|
@@ -1764,7 +2479,7 @@ async function isVFSAvailable() {
|
|
|
1764
2479
|
return false;
|
|
1765
2480
|
}
|
|
1766
2481
|
}
|
|
1767
|
-
function
|
|
2482
|
+
function createManifest2(services) {
|
|
1768
2483
|
return {
|
|
1769
2484
|
name: "preview",
|
|
1770
2485
|
version: "1.0.0",
|
|
@@ -1773,63 +2488,19 @@ function createManifest(services) {
|
|
|
1773
2488
|
services
|
|
1774
2489
|
};
|
|
1775
2490
|
}
|
|
1776
|
-
function
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
async function compileAndMount() {
|
|
1785
|
-
if (!containerRef.current || !compiler) return;
|
|
1786
|
-
setLoading(true);
|
|
1787
|
-
setError(null);
|
|
1788
|
-
try {
|
|
1789
|
-
if (mountedRef.current) {
|
|
1790
|
-
compiler.unmount(mountedRef.current);
|
|
1791
|
-
mountedRef.current = null;
|
|
1792
|
-
}
|
|
1793
|
-
const widget = await compiler.compile(
|
|
1794
|
-
code,
|
|
1795
|
-
createManifest(services),
|
|
1796
|
-
{ typescript: true }
|
|
1797
|
-
);
|
|
1798
|
-
if (cancelled) {
|
|
1799
|
-
return;
|
|
1800
|
-
}
|
|
1801
|
-
const mounted = await compiler.mount(widget, {
|
|
1802
|
-
target: containerRef.current,
|
|
1803
|
-
mode: "embedded"
|
|
1804
|
-
});
|
|
1805
|
-
mountedRef.current = mounted;
|
|
1806
|
-
} catch (err) {
|
|
1807
|
-
if (!cancelled) {
|
|
1808
|
-
setError(err instanceof Error ? err.message : "Failed to render JSX");
|
|
1809
|
-
}
|
|
1810
|
-
} finally {
|
|
1811
|
-
if (!cancelled) {
|
|
1812
|
-
setLoading(false);
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
compileAndMount();
|
|
1817
|
-
return () => {
|
|
1818
|
-
cancelled = true;
|
|
1819
|
-
if (mountedRef.current && compiler) {
|
|
1820
|
-
compiler.unmount(mountedRef.current);
|
|
1821
|
-
mountedRef.current = null;
|
|
1822
|
-
}
|
|
1823
|
-
};
|
|
1824
|
-
}, [code, compiler, enabled, services]);
|
|
1825
|
-
return { containerRef, loading, error };
|
|
1826
|
-
}
|
|
1827
|
-
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
|
+
}) {
|
|
1828
2499
|
const [isEditing, setIsEditing] = useState(false);
|
|
1829
2500
|
const [showPreview, setShowPreview] = useState(true);
|
|
1830
2501
|
const [currentCode, setCurrentCode] = useState(originalCode);
|
|
1831
2502
|
const [editCount, setEditCount] = useState(0);
|
|
1832
|
-
const [saveStatus, setSaveStatus] = useState("
|
|
2503
|
+
const [saveStatus, setSaveStatus] = useState("saved");
|
|
1833
2504
|
const [lastSavedCode, setLastSavedCode] = useState(originalCode);
|
|
1834
2505
|
const [vfsPath, setVfsPath] = useState(null);
|
|
1835
2506
|
const currentCodeRef = useRef(currentCode);
|
|
@@ -1865,6 +2536,14 @@ function CodePreview({ code: originalCode, compiler, services, filePath, entrypo
|
|
|
1865
2536
|
useEffect(() => {
|
|
1866
2537
|
isEditingRef.current = isEditing;
|
|
1867
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]);
|
|
1868
2547
|
useEffect(() => {
|
|
1869
2548
|
let active = true;
|
|
1870
2549
|
void (async () => {
|
|
@@ -1917,14 +2596,12 @@ function CodePreview({ code: originalCode, compiler, services, filePath, entrypo
|
|
|
1917
2596
|
setSaveStatus("error");
|
|
1918
2597
|
}
|
|
1919
2598
|
}, [currentCode, getProjectId, getEntryFile]);
|
|
1920
|
-
const
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
showPreview && !isEditing,
|
|
1924
|
-
services
|
|
1925
|
-
);
|
|
2599
|
+
const previewPath = filePath ?? entrypoint;
|
|
2600
|
+
const fileType = useMemo(() => getFileType(previewPath), [previewPath]);
|
|
2601
|
+
const canRenderWidget = fileType.category === "compilable";
|
|
1926
2602
|
const compile = useCallback(
|
|
1927
2603
|
async (code) => {
|
|
2604
|
+
if (!canRenderWidget) return { success: true };
|
|
1928
2605
|
if (!compiler) return { success: true };
|
|
1929
2606
|
const errors = [];
|
|
1930
2607
|
const originalError = console.error;
|
|
@@ -1935,7 +2612,7 @@ function CodePreview({ code: originalCode, compiler, services, filePath, entrypo
|
|
|
1935
2612
|
try {
|
|
1936
2613
|
await compiler.compile(
|
|
1937
2614
|
code,
|
|
1938
|
-
|
|
2615
|
+
createManifest2(services),
|
|
1939
2616
|
{ typescript: true }
|
|
1940
2617
|
);
|
|
1941
2618
|
return { success: true };
|
|
@@ -1953,15 +2630,64 @@ ${errors.join("\n")}` : "";
|
|
|
1953
2630
|
console.error = originalError;
|
|
1954
2631
|
}
|
|
1955
2632
|
},
|
|
1956
|
-
[compiler, services]
|
|
2633
|
+
[canRenderWidget, compiler, services]
|
|
1957
2634
|
);
|
|
1958
2635
|
const handleRevert = () => {
|
|
1959
2636
|
setCurrentCode(originalCode);
|
|
1960
2637
|
setEditCount(0);
|
|
1961
2638
|
};
|
|
1962
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]);
|
|
1963
2689
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1964
|
-
/* @__PURE__ */ jsxs("div", { className: "
|
|
2690
|
+
/* @__PURE__ */ jsxs("div", { className: "border rounded-lg overflow-hidden min-w-0", children: [
|
|
1965
2691
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2 bg-muted/50 border-b rounded-t-lg", children: [
|
|
1966
2692
|
/* @__PURE__ */ jsx(Code, { className: "h-4 w-4 text-muted-foreground" }),
|
|
1967
2693
|
editCount > 0 && /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground flex items-center gap-1", children: [
|
|
@@ -1970,19 +2696,6 @@ ${errors.join("\n")}` : "";
|
|
|
1970
2696
|
" edit",
|
|
1971
2697
|
editCount !== 1 ? "s" : ""
|
|
1972
2698
|
] }),
|
|
1973
|
-
/* @__PURE__ */ jsx(
|
|
1974
|
-
"button",
|
|
1975
|
-
{
|
|
1976
|
-
onClick: handleSave,
|
|
1977
|
-
disabled: saveStatus === "saving",
|
|
1978
|
-
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"}`,
|
|
1979
|
-
title: saveStatus === "saved" ? "Saved to disk" : saveStatus === "saving" ? "Saving..." : saveStatus === "error" ? "Save failed - click to retry" : "Click to save",
|
|
1980
|
-
children: saveStatus === "saving" ? /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : /* @__PURE__ */ jsxs("span", { className: "relative", children: [
|
|
1981
|
-
/* @__PURE__ */ jsx(Cloud, { className: "h-3 w-3" }),
|
|
1982
|
-
saveStatus === "saved" && /* @__PURE__ */ jsx(Check, { className: "h-2 w-2 absolute -bottom-0.5 -right-0.5 stroke-[3]" })
|
|
1983
|
-
] })
|
|
1984
|
-
}
|
|
1985
|
-
),
|
|
1986
2699
|
/* @__PURE__ */ jsxs("div", { className: "ml-auto flex gap-1", children: [
|
|
1987
2700
|
hasChanges && /* @__PURE__ */ jsx(
|
|
1988
2701
|
"button",
|
|
@@ -1996,17 +2709,26 @@ ${errors.join("\n")}` : "";
|
|
|
1996
2709
|
/* @__PURE__ */ jsx(
|
|
1997
2710
|
"button",
|
|
1998
2711
|
{
|
|
1999
|
-
onClick: () =>
|
|
2712
|
+
onClick: () => void handleOpenEditor(),
|
|
2000
2713
|
className: "px-2 py-1 text-xs rounded flex items-center gap-1 hover:bg-muted",
|
|
2001
2714
|
title: "Edit component",
|
|
2002
2715
|
children: /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3" })
|
|
2003
2716
|
}
|
|
2004
2717
|
),
|
|
2718
|
+
/* @__PURE__ */ jsx(
|
|
2719
|
+
SaveStatusButton,
|
|
2720
|
+
{
|
|
2721
|
+
status: saveStatus,
|
|
2722
|
+
onClick: handleSave,
|
|
2723
|
+
disabled: saveStatus === "saving",
|
|
2724
|
+
tone: "muted"
|
|
2725
|
+
}
|
|
2726
|
+
),
|
|
2005
2727
|
/* @__PURE__ */ jsxs(
|
|
2006
2728
|
"button",
|
|
2007
2729
|
{
|
|
2008
2730
|
onClick: () => setShowPreview(!showPreview),
|
|
2009
|
-
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"}`,
|
|
2010
2732
|
children: [
|
|
2011
2733
|
showPreview ? /* @__PURE__ */ jsx(Eye, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Code, { className: "h-3 w-3" }),
|
|
2012
2734
|
showPreview ? "Preview" : "Code"
|
|
@@ -2015,16 +2737,13 @@ ${errors.join("\n")}` : "";
|
|
|
2015
2737
|
)
|
|
2016
2738
|
] })
|
|
2017
2739
|
] }),
|
|
2018
|
-
showPreview ? /* @__PURE__ */
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
] }) : !compiler ? /* @__PURE__ */ jsx("div", { className: "p-3 text-sm text-muted-foreground", children: "Compiler not initialized" }) : null,
|
|
2026
|
-
/* @__PURE__ */ jsx("div", { ref: containerRef })
|
|
2027
|
-
] }) : /* @__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
|
+
) })
|
|
2028
2747
|
] }),
|
|
2029
2748
|
/* @__PURE__ */ jsx(
|
|
2030
2749
|
EditModal,
|
|
@@ -2042,6 +2761,7 @@ ${errors.join("\n")}` : "";
|
|
|
2042
2761
|
const entryFile = getEntryFile();
|
|
2043
2762
|
const project = createSingleFileProject(finalCode, entryFile, projectId);
|
|
2044
2763
|
await saveProject(project);
|
|
2764
|
+
setLastSavedCode(finalCode);
|
|
2045
2765
|
setSaveStatus("saved");
|
|
2046
2766
|
} catch (err) {
|
|
2047
2767
|
console.warn("[VFS] Failed to save project:", err);
|
|
@@ -2052,49 +2772,77 @@ ${errors.join("\n")}` : "";
|
|
|
2052
2772
|
},
|
|
2053
2773
|
originalCode: currentCode,
|
|
2054
2774
|
compile,
|
|
2055
|
-
renderPreview: (code) => /* @__PURE__ */ jsx(
|
|
2775
|
+
renderPreview: (code) => /* @__PURE__ */ jsx(
|
|
2776
|
+
WidgetPreview,
|
|
2777
|
+
{
|
|
2778
|
+
code,
|
|
2779
|
+
compiler,
|
|
2780
|
+
services
|
|
2781
|
+
}
|
|
2782
|
+
)
|
|
2056
2783
|
}
|
|
2057
2784
|
)
|
|
2058
2785
|
] });
|
|
2059
2786
|
}
|
|
2060
|
-
function
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
services
|
|
2787
|
+
function DefaultBadge({
|
|
2788
|
+
children,
|
|
2789
|
+
className = ""
|
|
2064
2790
|
}) {
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
/* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
|
|
2073
|
-
/* @__PURE__ */ jsx("span", { className: "text-sm", children: "Rendering preview..." })
|
|
2074
|
-
] }),
|
|
2075
|
-
!compiler && !loading && !error && /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "Compiler not initialized" }),
|
|
2076
|
-
/* @__PURE__ */ jsx("div", { ref: containerRef })
|
|
2077
|
-
] });
|
|
2078
|
-
}
|
|
2079
|
-
function DefaultBadge({ children, className = "" }) {
|
|
2080
|
-
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
|
+
);
|
|
2081
2798
|
}
|
|
2082
|
-
function DefaultDialog({
|
|
2799
|
+
function DefaultDialog({
|
|
2800
|
+
children,
|
|
2801
|
+
open,
|
|
2802
|
+
onOpenChange
|
|
2803
|
+
}) {
|
|
2083
2804
|
if (!open) return null;
|
|
2084
|
-
return /* @__PURE__ */ jsx(
|
|
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
|
+
);
|
|
2085
2820
|
}
|
|
2086
2821
|
function ServicesInspector({
|
|
2087
2822
|
namespaces,
|
|
2088
2823
|
services = [],
|
|
2089
2824
|
BadgeComponent = DefaultBadge,
|
|
2090
|
-
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
|
+
)
|
|
2091
2836
|
}) {
|
|
2092
2837
|
const [open, setOpen] = useState(false);
|
|
2093
2838
|
if (namespaces.length === 0) return null;
|
|
2094
|
-
const groupedServices = services.reduce(
|
|
2095
|
-
(acc
|
|
2096
|
-
|
|
2097
|
-
|
|
2839
|
+
const groupedServices = services.reduce(
|
|
2840
|
+
(acc, svc) => {
|
|
2841
|
+
(acc[svc.namespace] ??= []).push(svc);
|
|
2842
|
+
return acc;
|
|
2843
|
+
},
|
|
2844
|
+
{}
|
|
2845
|
+
);
|
|
2098
2846
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2099
2847
|
/* @__PURE__ */ jsxs(
|
|
2100
2848
|
"button",
|
|
@@ -2112,11 +2860,11 @@ function ServicesInspector({
|
|
|
2112
2860
|
}
|
|
2113
2861
|
),
|
|
2114
2862
|
/* @__PURE__ */ jsxs(DialogComponent, { open, onOpenChange: setOpen, children: [
|
|
2115
|
-
/* @__PURE__ */ jsxs(
|
|
2116
|
-
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "
|
|
2117
|
-
/* @__PURE__ */ jsx(
|
|
2863
|
+
/* @__PURE__ */ jsxs(DialogHeaderComponent, { children: [
|
|
2864
|
+
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Services" }),
|
|
2865
|
+
/* @__PURE__ */ jsx(DialogCloseComponent, { onClose: () => setOpen(false) })
|
|
2118
2866
|
] }),
|
|
2119
|
-
/* @__PURE__ */ jsx(
|
|
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: [
|
|
2120
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: [
|
|
2121
2869
|
/* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 text-muted-foreground" }),
|
|
2122
2870
|
/* @__PURE__ */ jsx("span", { className: "font-medium text-sm", children: ns }),
|
|
@@ -2128,11 +2876,11 @@ function ServicesInspector({
|
|
|
2128
2876
|
] }),
|
|
2129
2877
|
/* @__PURE__ */ jsx("div", { className: "ml-6 mt-2 space-y-2", children: groupedServices[ns]?.map((svc) => /* @__PURE__ */ jsxs("details", { children: [
|
|
2130
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: [
|
|
2131
|
-
/* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3" }),
|
|
2879
|
+
svc.parameters && /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3" }),
|
|
2132
2880
|
/* @__PURE__ */ jsx("code", { className: "font-mono text-xs", children: svc.procedure }),
|
|
2133
2881
|
/* @__PURE__ */ jsx("span", { className: "truncate text-xs opacity-70", children: svc.description })
|
|
2134
2882
|
] }),
|
|
2135
|
-
/* @__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
|
|
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) }) })
|
|
2136
2884
|
] }, svc.name)) ?? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "No tool details available" }) })
|
|
2137
2885
|
] }, ns)) })
|
|
2138
2886
|
] })
|
|
@@ -2264,4 +3012,4 @@ function cn(...inputs) {
|
|
|
2264
3012
|
return twMerge(clsx(inputs));
|
|
2265
3013
|
}
|
|
2266
3014
|
|
|
2267
|
-
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 };
|