@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/.turbo/turbo-build.log +3 -3
- 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 +1053 -200
- package/package.json +4 -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/CodeBlockView.tsx +135 -19
- package/src/components/edit/EditModal.tsx +86 -28
- package/src/components/edit/FileTree.tsx +524 -29
- package/src/components/edit/MediaPreview.tsx +23 -5
- 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';
|
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
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 = [
|
|
857
|
-
|
|
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
|
|
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({
|
|
986
|
-
|
|
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
|
-
|
|
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 ===
|
|
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: () =>
|
|
1027
|
-
|
|
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__ */
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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-
|
|
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
|
|
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
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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
|
-
|
|
1681
|
+
activePath: selectedPath,
|
|
1100
1682
|
onSelect: onSelectFile,
|
|
1101
|
-
|
|
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
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
{
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
) :
|
|
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(
|
|
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
|
|
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
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
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("
|
|
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
|
|
1816
|
-
|
|
1817
|
-
|
|
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
|
-
|
|
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: "
|
|
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: () =>
|
|
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__ */
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
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(
|
|
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
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
services
|
|
2787
|
+
function DefaultBadge({
|
|
2788
|
+
children,
|
|
2789
|
+
className = ""
|
|
1959
2790
|
}) {
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
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({
|
|
2799
|
+
function DefaultDialog({
|
|
2800
|
+
children,
|
|
2801
|
+
open,
|
|
2802
|
+
onOpenChange
|
|
2803
|
+
}) {
|
|
1978
2804
|
if (!open) return null;
|
|
1979
|
-
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
|
+
);
|
|
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(
|
|
1990
|
-
(acc
|
|
1991
|
-
|
|
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(
|
|
2011
|
-
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "
|
|
2012
|
-
/* @__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) })
|
|
2013
2866
|
] }),
|
|
2014
|
-
/* @__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: [
|
|
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
|
|
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 };
|