@aprovan/patchwork-editor 0.1.1-dev.6bd527d → 0.1.2-dev.ba8f277
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/fileTypes.d.ts +2 -0
- 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 +1030 -197
- 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 +101 -37
- 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 +6 -1
- package/src/components/edit/fileTypes.ts +8 -0
- 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,6 +860,156 @@ 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"];
|
|
@@ -938,6 +1098,12 @@ function isMediaFile(path) {
|
|
|
938
1098
|
function isTextFile(path) {
|
|
939
1099
|
return TEXT_EXTENSIONS.includes(getExtension(path));
|
|
940
1100
|
}
|
|
1101
|
+
function isMarkdownFile(path) {
|
|
1102
|
+
return getExtension(path) === ".md";
|
|
1103
|
+
}
|
|
1104
|
+
function isPreviewable(path) {
|
|
1105
|
+
return isCompilable(path) || isMarkdownFile(path);
|
|
1106
|
+
}
|
|
941
1107
|
function getLanguageFromExt(path) {
|
|
942
1108
|
const ext = getExtension(path);
|
|
943
1109
|
return EXTENSION_TO_LANGUAGE[ext] ?? null;
|
|
@@ -954,6 +1120,17 @@ function isVideoFile(path) {
|
|
|
954
1120
|
const ext = getExtension(path);
|
|
955
1121
|
return [".mp4", ".mov", ".webm"].includes(ext);
|
|
956
1122
|
}
|
|
1123
|
+
function sortNodes(nodes) {
|
|
1124
|
+
nodes.sort((a, b) => {
|
|
1125
|
+
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
1126
|
+
return a.name.localeCompare(b.name);
|
|
1127
|
+
});
|
|
1128
|
+
for (const node of nodes) {
|
|
1129
|
+
if (node.children.length > 0) {
|
|
1130
|
+
sortNodes(node.children);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
957
1134
|
function buildTree(files) {
|
|
958
1135
|
const root = { name: "", path: "", isDir: true, children: [] };
|
|
959
1136
|
for (const file of files) {
|
|
@@ -976,14 +1153,26 @@ function buildTree(files) {
|
|
|
976
1153
|
current = child;
|
|
977
1154
|
}
|
|
978
1155
|
}
|
|
979
|
-
root.children
|
|
980
|
-
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
981
|
-
return a.name.localeCompare(b.name);
|
|
982
|
-
});
|
|
1156
|
+
sortNodes(root.children);
|
|
983
1157
|
return root;
|
|
984
1158
|
}
|
|
985
|
-
function TreeNodeComponent({
|
|
986
|
-
|
|
1159
|
+
function TreeNodeComponent({
|
|
1160
|
+
node,
|
|
1161
|
+
activePath,
|
|
1162
|
+
onSelect,
|
|
1163
|
+
onSelectDirectory,
|
|
1164
|
+
onReplaceFile,
|
|
1165
|
+
onOpenInEditor,
|
|
1166
|
+
openInEditorMode = "files",
|
|
1167
|
+
openInEditorIcon,
|
|
1168
|
+
openInEditorTitle = "Open in editor",
|
|
1169
|
+
pinnedPaths,
|
|
1170
|
+
onTogglePin,
|
|
1171
|
+
pageSize = 10,
|
|
1172
|
+
depth = 0
|
|
1173
|
+
}) {
|
|
1174
|
+
const [expanded, setExpanded] = useState(false);
|
|
1175
|
+
const [visibleCount, setVisibleCount] = useState(pageSize);
|
|
987
1176
|
const [isHovered, setIsHovered] = useState(false);
|
|
988
1177
|
const fileInputRef = useRef(null);
|
|
989
1178
|
const handleUploadClick = useCallback((e) => {
|
|
@@ -1002,48 +1191,119 @@ function TreeNodeComponent({ node, activeFile, onSelect, onReplaceFile, depth =
|
|
|
1002
1191
|
reader.readAsDataURL(file);
|
|
1003
1192
|
e.target.value = "";
|
|
1004
1193
|
}, [node.path, onReplaceFile]);
|
|
1194
|
+
const isPinned = pinnedPaths?.has(node.path) ?? false;
|
|
1195
|
+
const showPin = onTogglePin && isHovered;
|
|
1196
|
+
const handleTogglePin = useCallback((e) => {
|
|
1197
|
+
e.stopPropagation();
|
|
1198
|
+
onTogglePin?.(node.path, node.isDir);
|
|
1199
|
+
}, [node.path, node.isDir, onTogglePin]);
|
|
1005
1200
|
if (!node.name) {
|
|
1006
1201
|
return /* @__PURE__ */ jsx(Fragment, { children: node.children.map((child) => /* @__PURE__ */ jsx(
|
|
1007
1202
|
TreeNodeComponent,
|
|
1008
1203
|
{
|
|
1009
1204
|
node: child,
|
|
1010
|
-
|
|
1205
|
+
activePath,
|
|
1011
1206
|
onSelect,
|
|
1207
|
+
onSelectDirectory,
|
|
1012
1208
|
onReplaceFile,
|
|
1209
|
+
onOpenInEditor,
|
|
1210
|
+
openInEditorMode,
|
|
1211
|
+
openInEditorIcon,
|
|
1212
|
+
openInEditorTitle,
|
|
1213
|
+
pinnedPaths,
|
|
1214
|
+
onTogglePin,
|
|
1215
|
+
pageSize,
|
|
1013
1216
|
depth
|
|
1014
1217
|
},
|
|
1015
1218
|
child.path
|
|
1016
1219
|
)) });
|
|
1017
1220
|
}
|
|
1018
|
-
const isActive = node.path ===
|
|
1221
|
+
const isActive = node.path === activePath;
|
|
1019
1222
|
const isMedia = !node.isDir && isMediaFile(node.path);
|
|
1020
1223
|
const showUpload = isMedia && isHovered && onReplaceFile;
|
|
1224
|
+
const showOpenInEditor = !!onOpenInEditor && isHovered && (openInEditorMode === "all" || (openInEditorMode === "directories" ? node.isDir : !node.isDir));
|
|
1225
|
+
const handleOpenInEditor = useCallback(
|
|
1226
|
+
(e) => {
|
|
1227
|
+
e.stopPropagation();
|
|
1228
|
+
onOpenInEditor?.(node.path, node.isDir);
|
|
1229
|
+
},
|
|
1230
|
+
[node.path, node.isDir, onOpenInEditor]
|
|
1231
|
+
);
|
|
1021
1232
|
if (node.isDir) {
|
|
1022
1233
|
return /* @__PURE__ */ jsxs("div", { children: [
|
|
1023
1234
|
/* @__PURE__ */ jsxs(
|
|
1024
1235
|
"button",
|
|
1025
1236
|
{
|
|
1026
|
-
onClick: () =>
|
|
1027
|
-
|
|
1237
|
+
onClick: () => {
|
|
1238
|
+
onSelectDirectory?.(node.path);
|
|
1239
|
+
setExpanded(!expanded);
|
|
1240
|
+
},
|
|
1241
|
+
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" : ""}`,
|
|
1242
|
+
onMouseEnter: () => setIsHovered(true),
|
|
1243
|
+
onMouseLeave: () => setIsHovered(false),
|
|
1028
1244
|
style: { paddingLeft: `${depth * 12 + 8}px` },
|
|
1029
1245
|
children: [
|
|
1030
1246
|
expanded ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 shrink-0" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 shrink-0" }),
|
|
1031
1247
|
/* @__PURE__ */ jsx(Folder, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
|
|
1032
|
-
/* @__PURE__ */ jsx("span", { className: "truncate", children: node.name })
|
|
1248
|
+
/* @__PURE__ */ jsx("span", { className: "truncate flex-1 flex pl-2", children: node.name }),
|
|
1249
|
+
(showPin || isPinned) && /* @__PURE__ */ jsx(
|
|
1250
|
+
"span",
|
|
1251
|
+
{
|
|
1252
|
+
onClick: handleTogglePin,
|
|
1253
|
+
className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
|
|
1254
|
+
title: isPinned ? "Unpin" : "Pin",
|
|
1255
|
+
children: isPinned ? /* @__PURE__ */ jsx(PinOff, { className: "h-3 w-3 text-primary" }) : /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3 text-muted-foreground" })
|
|
1256
|
+
}
|
|
1257
|
+
),
|
|
1258
|
+
showOpenInEditor && /* @__PURE__ */ jsx(
|
|
1259
|
+
"span",
|
|
1260
|
+
{
|
|
1261
|
+
onClick: handleOpenInEditor,
|
|
1262
|
+
className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
|
|
1263
|
+
title: openInEditorTitle,
|
|
1264
|
+
children: openInEditorIcon ?? /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 text-primary" })
|
|
1265
|
+
}
|
|
1266
|
+
)
|
|
1033
1267
|
]
|
|
1034
1268
|
}
|
|
1035
1269
|
),
|
|
1036
|
-
expanded && /* @__PURE__ */
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1270
|
+
expanded && /* @__PURE__ */ jsxs("div", { children: [
|
|
1271
|
+
node.children.slice(0, visibleCount).map((child) => /* @__PURE__ */ jsx(
|
|
1272
|
+
TreeNodeComponent,
|
|
1273
|
+
{
|
|
1274
|
+
node: child,
|
|
1275
|
+
activePath,
|
|
1276
|
+
onSelect,
|
|
1277
|
+
onSelectDirectory,
|
|
1278
|
+
onReplaceFile,
|
|
1279
|
+
onOpenInEditor,
|
|
1280
|
+
openInEditorMode,
|
|
1281
|
+
openInEditorIcon,
|
|
1282
|
+
openInEditorTitle,
|
|
1283
|
+
pinnedPaths,
|
|
1284
|
+
onTogglePin,
|
|
1285
|
+
pageSize,
|
|
1286
|
+
depth: depth + 1
|
|
1287
|
+
},
|
|
1288
|
+
child.path
|
|
1289
|
+
)),
|
|
1290
|
+
node.children.length > visibleCount && /* @__PURE__ */ jsxs(
|
|
1291
|
+
"button",
|
|
1292
|
+
{
|
|
1293
|
+
onClick: () => setVisibleCount((prev) => prev + pageSize),
|
|
1294
|
+
className: "flex items-center gap-1 w-full px-2 py-1 text-left text-xs hover:bg-muted/50 rounded text-muted-foreground",
|
|
1295
|
+
style: { paddingLeft: `${(depth + 1) * 12 + 20}px` },
|
|
1296
|
+
children: [
|
|
1297
|
+
/* @__PURE__ */ jsx(ChevronsDown, { className: "h-3 w-3 shrink-0" }),
|
|
1298
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
1299
|
+
"Show ",
|
|
1300
|
+
Math.min(pageSize, node.children.length - visibleCount),
|
|
1301
|
+
" more"
|
|
1302
|
+
] })
|
|
1303
|
+
]
|
|
1304
|
+
}
|
|
1305
|
+
)
|
|
1306
|
+
] })
|
|
1047
1307
|
] });
|
|
1048
1308
|
}
|
|
1049
1309
|
return /* @__PURE__ */ jsxs(
|
|
@@ -1061,12 +1321,30 @@ function TreeNodeComponent({ node, activeFile, onSelect, onReplaceFile, depth =
|
|
|
1061
1321
|
style: { paddingLeft: `${depth * 12 + 20}px` },
|
|
1062
1322
|
children: [
|
|
1063
1323
|
/* @__PURE__ */ jsx(File, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
|
|
1064
|
-
/* @__PURE__ */ jsx("span", { className: "truncate flex-1", children: node.name }),
|
|
1324
|
+
/* @__PURE__ */ jsx("span", { className: "truncate flex-1 flex pl-2", children: node.name }),
|
|
1325
|
+
(showPin || isPinned) && /* @__PURE__ */ jsx(
|
|
1326
|
+
"span",
|
|
1327
|
+
{
|
|
1328
|
+
onClick: handleTogglePin,
|
|
1329
|
+
className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
|
|
1330
|
+
title: isPinned ? "Unpin" : "Pin",
|
|
1331
|
+
children: isPinned ? /* @__PURE__ */ jsx(PinOff, { className: "h-3 w-3 text-primary" }) : /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3 text-muted-foreground" })
|
|
1332
|
+
}
|
|
1333
|
+
),
|
|
1334
|
+
showOpenInEditor && /* @__PURE__ */ jsx(
|
|
1335
|
+
"span",
|
|
1336
|
+
{
|
|
1337
|
+
onClick: handleOpenInEditor,
|
|
1338
|
+
className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
|
|
1339
|
+
title: openInEditorTitle,
|
|
1340
|
+
children: openInEditorIcon ?? /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 text-primary" })
|
|
1341
|
+
}
|
|
1342
|
+
),
|
|
1065
1343
|
showUpload && /* @__PURE__ */ jsx(
|
|
1066
1344
|
"span",
|
|
1067
1345
|
{
|
|
1068
1346
|
onClick: handleUploadClick,
|
|
1069
|
-
className: "p-
|
|
1347
|
+
className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
|
|
1070
1348
|
title: "Replace file",
|
|
1071
1349
|
children: /* @__PURE__ */ jsx(Upload, { className: "h-3 w-3 text-primary" })
|
|
1072
1350
|
}
|
|
@@ -1088,17 +1366,309 @@ function TreeNodeComponent({ node, activeFile, onSelect, onReplaceFile, depth =
|
|
|
1088
1366
|
}
|
|
1089
1367
|
);
|
|
1090
1368
|
}
|
|
1091
|
-
function
|
|
1369
|
+
function LazyTreeNode({
|
|
1370
|
+
entry,
|
|
1371
|
+
activePath,
|
|
1372
|
+
onSelectFile,
|
|
1373
|
+
onSelectDirectory,
|
|
1374
|
+
onOpenInEditor,
|
|
1375
|
+
openInEditorMode = "files",
|
|
1376
|
+
openInEditorIcon,
|
|
1377
|
+
openInEditorTitle = "Open in editor",
|
|
1378
|
+
pinnedPaths,
|
|
1379
|
+
onTogglePin,
|
|
1380
|
+
directoryLoader,
|
|
1381
|
+
pageSize,
|
|
1382
|
+
depth = 0,
|
|
1383
|
+
reloadToken
|
|
1384
|
+
}) {
|
|
1385
|
+
const [expanded, setExpanded] = useState(false);
|
|
1386
|
+
const [loading, setLoading] = useState(false);
|
|
1387
|
+
const [loadError, setLoadError] = useState(null);
|
|
1388
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
1389
|
+
const [children, setChildren] = useState(null);
|
|
1390
|
+
const [visibleCount, setVisibleCount] = useState(pageSize);
|
|
1391
|
+
useEffect(() => {
|
|
1392
|
+
setChildren(null);
|
|
1393
|
+
setVisibleCount(pageSize);
|
|
1394
|
+
if (expanded) {
|
|
1395
|
+
setLoading(true);
|
|
1396
|
+
setLoadError(null);
|
|
1397
|
+
directoryLoader(entry.path).then((loaded) => setChildren(loaded)).catch((err) => setLoadError(err instanceof Error ? err.message : "Failed to load directory")).finally(() => setLoading(false));
|
|
1398
|
+
}
|
|
1399
|
+
}, [reloadToken]);
|
|
1400
|
+
const isActive = entry.path === activePath;
|
|
1401
|
+
const isPinned = pinnedPaths?.has(entry.path) ?? false;
|
|
1402
|
+
const showPin = onTogglePin && isHovered;
|
|
1403
|
+
const showOpenInEditor = !!onOpenInEditor && isHovered && (openInEditorMode === "all" || (openInEditorMode === "directories" ? entry.isDir : !entry.isDir));
|
|
1404
|
+
const handleOpenInEditor = useCallback(
|
|
1405
|
+
(e) => {
|
|
1406
|
+
e.stopPropagation();
|
|
1407
|
+
onOpenInEditor?.(entry.path, entry.isDir);
|
|
1408
|
+
},
|
|
1409
|
+
[entry.path, entry.isDir, onOpenInEditor]
|
|
1410
|
+
);
|
|
1411
|
+
const handleTogglePin = useCallback(
|
|
1412
|
+
(e) => {
|
|
1413
|
+
e.stopPropagation();
|
|
1414
|
+
onTogglePin?.(entry.path, entry.isDir);
|
|
1415
|
+
},
|
|
1416
|
+
[entry.path, entry.isDir, onTogglePin]
|
|
1417
|
+
);
|
|
1418
|
+
const toggleDirectory = useCallback(async () => {
|
|
1419
|
+
if (!entry.isDir) return;
|
|
1420
|
+
onSelectDirectory?.(entry.path);
|
|
1421
|
+
if (!expanded && children === null) {
|
|
1422
|
+
setLoading(true);
|
|
1423
|
+
setLoadError(null);
|
|
1424
|
+
try {
|
|
1425
|
+
const loaded = await directoryLoader(entry.path);
|
|
1426
|
+
setChildren(loaded);
|
|
1427
|
+
} catch (err) {
|
|
1428
|
+
setLoadError(err instanceof Error ? err.message : "Failed to load directory");
|
|
1429
|
+
} finally {
|
|
1430
|
+
setLoading(false);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
setExpanded((prev) => !prev);
|
|
1434
|
+
}, [entry.isDir, entry.path, onSelectDirectory, expanded, children, directoryLoader]);
|
|
1435
|
+
if (!entry.isDir) {
|
|
1436
|
+
return /* @__PURE__ */ jsx(
|
|
1437
|
+
"div",
|
|
1438
|
+
{
|
|
1439
|
+
className: "relative",
|
|
1440
|
+
onMouseEnter: () => setIsHovered(true),
|
|
1441
|
+
onMouseLeave: () => setIsHovered(false),
|
|
1442
|
+
children: /* @__PURE__ */ jsxs(
|
|
1443
|
+
"button",
|
|
1444
|
+
{
|
|
1445
|
+
onClick: () => onSelectFile(entry.path),
|
|
1446
|
+
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" : ""}`,
|
|
1447
|
+
style: { paddingLeft: `${depth * 12 + 20}px` },
|
|
1448
|
+
children: [
|
|
1449
|
+
/* @__PURE__ */ jsx(File, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
|
|
1450
|
+
/* @__PURE__ */ jsx("span", { className: "truncate flex-1 flex pl-2", children: entry.name }),
|
|
1451
|
+
(showPin || isPinned) && /* @__PURE__ */ jsx(
|
|
1452
|
+
"span",
|
|
1453
|
+
{
|
|
1454
|
+
onClick: handleTogglePin,
|
|
1455
|
+
className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
|
|
1456
|
+
title: isPinned ? "Unpin" : "Pin",
|
|
1457
|
+
children: isPinned ? /* @__PURE__ */ jsx(PinOff, { className: "h-3 w-3 text-primary" }) : /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3 text-muted-foreground" })
|
|
1458
|
+
}
|
|
1459
|
+
),
|
|
1460
|
+
showOpenInEditor && /* @__PURE__ */ jsx(
|
|
1461
|
+
"span",
|
|
1462
|
+
{
|
|
1463
|
+
onClick: handleOpenInEditor,
|
|
1464
|
+
className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
|
|
1465
|
+
title: openInEditorTitle,
|
|
1466
|
+
children: openInEditorIcon ?? /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 text-primary" })
|
|
1467
|
+
}
|
|
1468
|
+
)
|
|
1469
|
+
]
|
|
1470
|
+
}
|
|
1471
|
+
)
|
|
1472
|
+
}
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
1476
|
+
/* @__PURE__ */ jsxs(
|
|
1477
|
+
"button",
|
|
1478
|
+
{
|
|
1479
|
+
onClick: () => void toggleDirectory(),
|
|
1480
|
+
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" : ""}`,
|
|
1481
|
+
onMouseEnter: () => setIsHovered(true),
|
|
1482
|
+
onMouseLeave: () => setIsHovered(false),
|
|
1483
|
+
style: { paddingLeft: `${depth * 12 + 8}px` },
|
|
1484
|
+
children: [
|
|
1485
|
+
expanded ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 shrink-0" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 shrink-0" }),
|
|
1486
|
+
/* @__PURE__ */ jsx(Folder, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
|
|
1487
|
+
/* @__PURE__ */ jsx("span", { className: "truncate flex-1 flex pl-2", children: entry.name }),
|
|
1488
|
+
loading && /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin text-muted-foreground" }),
|
|
1489
|
+
(showPin || isPinned) && /* @__PURE__ */ jsx(
|
|
1490
|
+
"span",
|
|
1491
|
+
{
|
|
1492
|
+
onClick: handleTogglePin,
|
|
1493
|
+
className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
|
|
1494
|
+
title: isPinned ? "Unpin" : "Pin",
|
|
1495
|
+
children: isPinned ? /* @__PURE__ */ jsx(PinOff, { className: "h-3 w-3 text-primary" }) : /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3 text-muted-foreground" })
|
|
1496
|
+
}
|
|
1497
|
+
),
|
|
1498
|
+
showOpenInEditor && /* @__PURE__ */ jsx(
|
|
1499
|
+
"span",
|
|
1500
|
+
{
|
|
1501
|
+
onClick: handleOpenInEditor,
|
|
1502
|
+
className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
|
|
1503
|
+
title: openInEditorTitle,
|
|
1504
|
+
children: openInEditorIcon ?? /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 text-primary" })
|
|
1505
|
+
}
|
|
1506
|
+
)
|
|
1507
|
+
]
|
|
1508
|
+
}
|
|
1509
|
+
),
|
|
1510
|
+
expanded && /* @__PURE__ */ jsxs("div", { children: [
|
|
1511
|
+
loadError && /* @__PURE__ */ jsx(
|
|
1512
|
+
"div",
|
|
1513
|
+
{
|
|
1514
|
+
className: "px-2 py-1 text-xs text-destructive",
|
|
1515
|
+
style: { paddingLeft: `${(depth + 1) * 12 + 20}px` },
|
|
1516
|
+
children: loadError
|
|
1517
|
+
}
|
|
1518
|
+
),
|
|
1519
|
+
(children ?? []).slice(0, visibleCount).map((child) => /* @__PURE__ */ jsx(
|
|
1520
|
+
LazyTreeNode,
|
|
1521
|
+
{
|
|
1522
|
+
entry: child,
|
|
1523
|
+
activePath,
|
|
1524
|
+
onSelectFile,
|
|
1525
|
+
onSelectDirectory,
|
|
1526
|
+
onOpenInEditor,
|
|
1527
|
+
openInEditorMode,
|
|
1528
|
+
openInEditorIcon,
|
|
1529
|
+
openInEditorTitle,
|
|
1530
|
+
pinnedPaths,
|
|
1531
|
+
onTogglePin,
|
|
1532
|
+
directoryLoader,
|
|
1533
|
+
pageSize,
|
|
1534
|
+
depth: depth + 1,
|
|
1535
|
+
reloadToken
|
|
1536
|
+
},
|
|
1537
|
+
child.path
|
|
1538
|
+
)),
|
|
1539
|
+
(children?.length ?? 0) > visibleCount && /* @__PURE__ */ jsxs(
|
|
1540
|
+
"button",
|
|
1541
|
+
{
|
|
1542
|
+
onClick: () => setVisibleCount((prev) => prev + pageSize),
|
|
1543
|
+
className: "flex items-center gap-1 w-full px-2 py-1 text-left text-xs hover:bg-muted/50 rounded text-muted-foreground",
|
|
1544
|
+
style: { paddingLeft: `${(depth + 1) * 12 + 20}px` },
|
|
1545
|
+
children: [
|
|
1546
|
+
/* @__PURE__ */ jsx(ChevronsDown, { className: "h-3 w-3 shrink-0" }),
|
|
1547
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
1548
|
+
"Show ",
|
|
1549
|
+
Math.min(pageSize, (children?.length ?? 0) - visibleCount),
|
|
1550
|
+
" more"
|
|
1551
|
+
] })
|
|
1552
|
+
]
|
|
1553
|
+
}
|
|
1554
|
+
)
|
|
1555
|
+
] })
|
|
1556
|
+
] });
|
|
1557
|
+
}
|
|
1558
|
+
function FileTree({
|
|
1559
|
+
files = [],
|
|
1560
|
+
activeFile,
|
|
1561
|
+
activePath,
|
|
1562
|
+
title = "Files",
|
|
1563
|
+
onSelectFile,
|
|
1564
|
+
onSelectDirectory,
|
|
1565
|
+
onReplaceFile,
|
|
1566
|
+
onOpenInEditor,
|
|
1567
|
+
openInEditorMode,
|
|
1568
|
+
openInEditorIcon,
|
|
1569
|
+
openInEditorTitle,
|
|
1570
|
+
pinnedPaths,
|
|
1571
|
+
onTogglePin,
|
|
1572
|
+
directoryLoader,
|
|
1573
|
+
pageSize = 10,
|
|
1574
|
+
reloadToken
|
|
1575
|
+
}) {
|
|
1092
1576
|
const tree = useMemo(() => buildTree(files), [files]);
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1577
|
+
const selectedPath = activePath ?? activeFile ?? "";
|
|
1578
|
+
const [rootEntries, setRootEntries] = useState([]);
|
|
1579
|
+
const [rootLoading, setRootLoading] = useState(false);
|
|
1580
|
+
const [rootError, setRootError] = useState(null);
|
|
1581
|
+
useEffect(() => {
|
|
1582
|
+
if (!directoryLoader) return;
|
|
1583
|
+
let cancelled = false;
|
|
1584
|
+
const loadRoot = async () => {
|
|
1585
|
+
setRootLoading(true);
|
|
1586
|
+
setRootError(null);
|
|
1587
|
+
try {
|
|
1588
|
+
const entries = await directoryLoader("");
|
|
1589
|
+
if (!cancelled) {
|
|
1590
|
+
setRootEntries(entries);
|
|
1591
|
+
}
|
|
1592
|
+
} catch (err) {
|
|
1593
|
+
if (!cancelled) {
|
|
1594
|
+
setRootError(err instanceof Error ? err.message : "Failed to load files");
|
|
1595
|
+
}
|
|
1596
|
+
} finally {
|
|
1597
|
+
if (!cancelled) {
|
|
1598
|
+
setRootLoading(false);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
void loadRoot();
|
|
1603
|
+
return () => {
|
|
1604
|
+
cancelled = true;
|
|
1605
|
+
};
|
|
1606
|
+
}, [directoryLoader, reloadToken]);
|
|
1607
|
+
return /* @__PURE__ */ jsxs("div", { className: "min-w-48 border-r bg-muted/30 overflow-auto text-foreground", children: [
|
|
1608
|
+
/* @__PURE__ */ jsx("div", { className: "p-2 border-b text-xs font-medium text-muted-foreground uppercase tracking-wide", children: title }),
|
|
1609
|
+
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(
|
|
1610
|
+
"button",
|
|
1611
|
+
{
|
|
1612
|
+
onClick: () => isDir ? onSelectDirectory?.(p) : onSelectFile(p),
|
|
1613
|
+
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"}`,
|
|
1614
|
+
children: [
|
|
1615
|
+
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" }),
|
|
1616
|
+
/* @__PURE__ */ jsx("span", { className: "truncate max-w-[120px]", children: p.split("/").pop() }),
|
|
1617
|
+
onTogglePin && /* @__PURE__ */ jsx(
|
|
1618
|
+
"span",
|
|
1619
|
+
{
|
|
1620
|
+
onClick: (e) => {
|
|
1621
|
+
e.stopPropagation();
|
|
1622
|
+
onTogglePin(p, isDir);
|
|
1623
|
+
},
|
|
1624
|
+
className: "hover:text-destructive",
|
|
1625
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-2.5 w-2.5" })
|
|
1626
|
+
}
|
|
1627
|
+
)
|
|
1628
|
+
]
|
|
1629
|
+
},
|
|
1630
|
+
p
|
|
1631
|
+
)) }),
|
|
1632
|
+
/* @__PURE__ */ jsx("div", { className: "p-1", children: directoryLoader ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1633
|
+
rootLoading && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-2 py-1 text-xs text-muted-foreground", children: [
|
|
1634
|
+
/* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }),
|
|
1635
|
+
/* @__PURE__ */ jsx("span", { children: "Loading..." })
|
|
1636
|
+
] }),
|
|
1637
|
+
rootError && /* @__PURE__ */ jsx("div", { className: "px-2 py-1 text-xs text-destructive", children: rootError }),
|
|
1638
|
+
rootEntries.map((entry) => /* @__PURE__ */ jsx(
|
|
1639
|
+
LazyTreeNode,
|
|
1640
|
+
{
|
|
1641
|
+
entry,
|
|
1642
|
+
activePath: selectedPath,
|
|
1643
|
+
onSelectFile,
|
|
1644
|
+
onSelectDirectory,
|
|
1645
|
+
onOpenInEditor,
|
|
1646
|
+
openInEditorMode,
|
|
1647
|
+
openInEditorIcon,
|
|
1648
|
+
openInEditorTitle,
|
|
1649
|
+
pinnedPaths,
|
|
1650
|
+
onTogglePin,
|
|
1651
|
+
directoryLoader,
|
|
1652
|
+
pageSize,
|
|
1653
|
+
reloadToken
|
|
1654
|
+
},
|
|
1655
|
+
entry.path
|
|
1656
|
+
))
|
|
1657
|
+
] }) : /* @__PURE__ */ jsx(
|
|
1096
1658
|
TreeNodeComponent,
|
|
1097
1659
|
{
|
|
1098
1660
|
node: tree,
|
|
1099
|
-
|
|
1661
|
+
activePath: selectedPath,
|
|
1100
1662
|
onSelect: onSelectFile,
|
|
1101
|
-
|
|
1663
|
+
onSelectDirectory,
|
|
1664
|
+
onReplaceFile,
|
|
1665
|
+
onOpenInEditor,
|
|
1666
|
+
openInEditorMode,
|
|
1667
|
+
openInEditorIcon,
|
|
1668
|
+
openInEditorTitle,
|
|
1669
|
+
pinnedPaths,
|
|
1670
|
+
onTogglePin,
|
|
1671
|
+
pageSize
|
|
1102
1672
|
}
|
|
1103
1673
|
) })
|
|
1104
1674
|
] });
|
|
@@ -1152,8 +1722,68 @@ function SaveConfirmDialog({
|
|
|
1152
1722
|
] })
|
|
1153
1723
|
] }) });
|
|
1154
1724
|
}
|
|
1725
|
+
var highlighterPromise = null;
|
|
1726
|
+
var COMMON_LANGUAGES = [
|
|
1727
|
+
"typescript",
|
|
1728
|
+
"javascript",
|
|
1729
|
+
"tsx",
|
|
1730
|
+
"jsx",
|
|
1731
|
+
"json",
|
|
1732
|
+
"html",
|
|
1733
|
+
"css",
|
|
1734
|
+
"markdown",
|
|
1735
|
+
"yaml",
|
|
1736
|
+
"python",
|
|
1737
|
+
"bash",
|
|
1738
|
+
"sql"
|
|
1739
|
+
];
|
|
1740
|
+
function getHighlighter() {
|
|
1741
|
+
if (!highlighterPromise) {
|
|
1742
|
+
highlighterPromise = createHighlighter({
|
|
1743
|
+
themes: ["github-light"],
|
|
1744
|
+
langs: COMMON_LANGUAGES
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
return highlighterPromise;
|
|
1748
|
+
}
|
|
1749
|
+
function normalizeLanguage(lang) {
|
|
1750
|
+
if (!lang) return "typescript";
|
|
1751
|
+
const normalized = lang.toLowerCase();
|
|
1752
|
+
const mapping = {
|
|
1753
|
+
ts: "typescript",
|
|
1754
|
+
tsx: "tsx",
|
|
1755
|
+
js: "javascript",
|
|
1756
|
+
jsx: "jsx",
|
|
1757
|
+
json: "json",
|
|
1758
|
+
html: "html",
|
|
1759
|
+
css: "css",
|
|
1760
|
+
md: "markdown",
|
|
1761
|
+
markdown: "markdown",
|
|
1762
|
+
yml: "yaml",
|
|
1763
|
+
yaml: "yaml",
|
|
1764
|
+
py: "python",
|
|
1765
|
+
python: "python",
|
|
1766
|
+
sh: "bash",
|
|
1767
|
+
bash: "bash",
|
|
1768
|
+
sql: "sql",
|
|
1769
|
+
typescript: "typescript",
|
|
1770
|
+
javascript: "javascript"
|
|
1771
|
+
};
|
|
1772
|
+
return mapping[normalized] || "typescript";
|
|
1773
|
+
}
|
|
1155
1774
|
function CodeBlockView({ content, language, editable = false, onChange }) {
|
|
1156
1775
|
const textareaRef = useRef(null);
|
|
1776
|
+
const containerRef = useRef(null);
|
|
1777
|
+
const [highlighter, setHighlighter] = useState(null);
|
|
1778
|
+
useEffect(() => {
|
|
1779
|
+
let mounted = true;
|
|
1780
|
+
getHighlighter().then((h) => {
|
|
1781
|
+
if (mounted) setHighlighter(h);
|
|
1782
|
+
});
|
|
1783
|
+
return () => {
|
|
1784
|
+
mounted = false;
|
|
1785
|
+
};
|
|
1786
|
+
}, []);
|
|
1157
1787
|
useEffect(() => {
|
|
1158
1788
|
if (textareaRef.current) {
|
|
1159
1789
|
textareaRef.current.style.height = "auto";
|
|
@@ -1184,23 +1814,60 @@ function CodeBlockView({ content, language, editable = false, onChange }) {
|
|
|
1184
1814
|
[onChange]
|
|
1185
1815
|
);
|
|
1186
1816
|
const langLabel = language || "text";
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
{
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1817
|
+
const shikiLang = useMemo(() => normalizeLanguage(language), [language]);
|
|
1818
|
+
const highlightedHtml = useMemo(() => {
|
|
1819
|
+
if (!highlighter) return null;
|
|
1820
|
+
try {
|
|
1821
|
+
return highlighter.codeToHtml(content, {
|
|
1822
|
+
lang: shikiLang,
|
|
1823
|
+
theme: "github-light"
|
|
1824
|
+
});
|
|
1825
|
+
} catch {
|
|
1826
|
+
return null;
|
|
1827
|
+
}
|
|
1828
|
+
}, [highlighter, content, shikiLang]);
|
|
1829
|
+
return /* @__PURE__ */ jsxs("div", { className: "h-full flex flex-col bg-[#ffffff]", children: [
|
|
1830
|
+
/* @__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 }) }),
|
|
1831
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto overflow-x-hidden", children: editable ? /* @__PURE__ */ jsxs("div", { className: "relative min-h-full", children: [
|
|
1832
|
+
/* @__PURE__ */ jsx(
|
|
1833
|
+
"div",
|
|
1834
|
+
{
|
|
1835
|
+
ref: containerRef,
|
|
1836
|
+
className: "absolute top-0 left-0 right-0 pointer-events-none p-4",
|
|
1837
|
+
"aria-hidden": "true",
|
|
1838
|
+
children: highlightedHtml ? /* @__PURE__ */ jsx(
|
|
1839
|
+
"div",
|
|
1840
|
+
{
|
|
1841
|
+
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",
|
|
1842
|
+
dangerouslySetInnerHTML: { __html: highlightedHtml }
|
|
1843
|
+
}
|
|
1844
|
+
) : /* @__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
1845
|
}
|
|
1846
|
+
),
|
|
1847
|
+
/* @__PURE__ */ jsx(
|
|
1848
|
+
"textarea",
|
|
1849
|
+
{
|
|
1850
|
+
ref: textareaRef,
|
|
1851
|
+
value: content,
|
|
1852
|
+
onChange: handleChange,
|
|
1853
|
+
onKeyDown: handleKeyDown,
|
|
1854
|
+
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",
|
|
1855
|
+
spellCheck: false,
|
|
1856
|
+
style: {
|
|
1857
|
+
tabSize: 2,
|
|
1858
|
+
caretColor: "#24292f",
|
|
1859
|
+
wordBreak: "break-word",
|
|
1860
|
+
overflowWrap: "break-word"
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
)
|
|
1864
|
+
] }) : /* @__PURE__ */ jsx("div", { className: "p-4", children: highlightedHtml ? /* @__PURE__ */ jsx(
|
|
1865
|
+
"div",
|
|
1866
|
+
{
|
|
1867
|
+
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",
|
|
1868
|
+
dangerouslySetInnerHTML: { __html: highlightedHtml }
|
|
1202
1869
|
}
|
|
1203
|
-
) : /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono whitespace-pre-wrap break-words m-0 leading-relaxed", children: /* @__PURE__ */ jsx("code", { children: content }) })
|
|
1870
|
+
) : /* @__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
1871
|
] });
|
|
1205
1872
|
}
|
|
1206
1873
|
function formatFileSize(bytes) {
|
|
@@ -1208,10 +1875,16 @@ function formatFileSize(bytes) {
|
|
|
1208
1875
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1209
1876
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1210
1877
|
}
|
|
1878
|
+
function isUrl(content) {
|
|
1879
|
+
return content.startsWith("/") || content.startsWith("http://") || content.startsWith("https://") || content.startsWith("./") || content.startsWith("../");
|
|
1880
|
+
}
|
|
1211
1881
|
function getDataUrl(content, mimeType) {
|
|
1212
1882
|
if (content.startsWith("data:")) {
|
|
1213
1883
|
return content;
|
|
1214
1884
|
}
|
|
1885
|
+
if (isUrl(content)) {
|
|
1886
|
+
return content;
|
|
1887
|
+
}
|
|
1215
1888
|
return `data:${mimeType};base64,${content}`;
|
|
1216
1889
|
}
|
|
1217
1890
|
function MediaPreview({ content, mimeType, fileName }) {
|
|
@@ -1220,8 +1893,8 @@ function MediaPreview({ content, mimeType, fileName }) {
|
|
|
1220
1893
|
const dataUrl = getDataUrl(content, mimeType);
|
|
1221
1894
|
const isImage = isImageFile(fileName);
|
|
1222
1895
|
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);
|
|
1896
|
+
const isUrlContent = isUrl(content);
|
|
1897
|
+
const estimatedBytes = isUrlContent ? null : content.startsWith("data:") ? Math.floor((content.split(",")[1]?.length ?? 0) * 0.75) : Math.floor(content.length * 0.75);
|
|
1225
1898
|
useEffect(() => {
|
|
1226
1899
|
setDimensions(null);
|
|
1227
1900
|
setError(null);
|
|
@@ -1281,7 +1954,7 @@ function MediaPreview({ content, mimeType, fileName }) {
|
|
|
1281
1954
|
dimensions.height,
|
|
1282
1955
|
" px"
|
|
1283
1956
|
] }),
|
|
1284
|
-
/* @__PURE__ */ jsx("span", { children: formatFileSize(estimatedBytes) }),
|
|
1957
|
+
estimatedBytes !== null && /* @__PURE__ */ jsx("span", { children: formatFileSize(estimatedBytes) }),
|
|
1285
1958
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground/60", children: mimeType })
|
|
1286
1959
|
] })
|
|
1287
1960
|
] })
|
|
@@ -1304,6 +1977,7 @@ function EditModal({
|
|
|
1304
1977
|
renderError,
|
|
1305
1978
|
previewError,
|
|
1306
1979
|
previewLoading,
|
|
1980
|
+
initialTreePath,
|
|
1307
1981
|
initialState = {},
|
|
1308
1982
|
hideFileTree = false,
|
|
1309
1983
|
...sessionOptions
|
|
@@ -1315,19 +1989,30 @@ function EditModal({
|
|
|
1315
1989
|
const [editInput, setEditInput] = useState("");
|
|
1316
1990
|
const [bobbinChanges, setBobbinChanges] = useState([]);
|
|
1317
1991
|
const [previewContainer, setPreviewContainer] = useState(null);
|
|
1992
|
+
const [pillContainer, setPillContainer] = useState(null);
|
|
1318
1993
|
const [showConfirm, setShowConfirm] = useState(false);
|
|
1319
1994
|
const [isSaving, setIsSaving] = useState(false);
|
|
1995
|
+
const [saveStatus, setSaveStatus] = useState("saved");
|
|
1996
|
+
const [lastSavedSnapshot, setLastSavedSnapshot] = useState("");
|
|
1320
1997
|
const [saveError, setSaveError] = useState(null);
|
|
1321
1998
|
const [pendingClose, setPendingClose] = useState(null);
|
|
1999
|
+
const [treePath, setTreePath] = useState(initialTreePath ?? "");
|
|
2000
|
+
const wasOpenRef = useRef(false);
|
|
1322
2001
|
const currentCodeRef = useRef("");
|
|
1323
2002
|
const session = useEditSession(sessionOptions);
|
|
1324
2003
|
const code = getActiveContent(session);
|
|
2004
|
+
const effectiveTreePath = treePath || session.activeFile;
|
|
1325
2005
|
currentCodeRef.current = code;
|
|
1326
2006
|
const files = useMemo(() => getFiles(session.project), [session.project]);
|
|
2007
|
+
const projectSnapshot = useMemo(
|
|
2008
|
+
() => Array.from(session.project.files.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([path, file]) => `${path}\0${file.content}`).join(""),
|
|
2009
|
+
[session.project]
|
|
2010
|
+
);
|
|
1327
2011
|
const hasChanges = code !== (session.originalProject.files.get(session.activeFile)?.content ?? "");
|
|
1328
2012
|
const fileType = useMemo(() => getFileType(session.activeFile), [session.activeFile]);
|
|
1329
2013
|
const isCompilableFile = isCompilable(session.activeFile);
|
|
1330
|
-
const
|
|
2014
|
+
const isMarkdown = isMarkdownFile(session.activeFile);
|
|
2015
|
+
const showPreviewToggle = isCompilableFile || isMarkdown;
|
|
1331
2016
|
const handleBobbinChanges = useCallback((changes) => {
|
|
1332
2017
|
setBobbinChanges(changes);
|
|
1333
2018
|
}, []);
|
|
@@ -1349,6 +2034,26 @@ ${bobbinYaml}
|
|
|
1349
2034
|
setBobbinChanges([]);
|
|
1350
2035
|
};
|
|
1351
2036
|
const hasSaveHandler = onSave || onSaveProject;
|
|
2037
|
+
useEffect(() => {
|
|
2038
|
+
if (isOpen && !wasOpenRef.current) {
|
|
2039
|
+
setLastSavedSnapshot(projectSnapshot);
|
|
2040
|
+
setSaveStatus("saved");
|
|
2041
|
+
setSaveError(null);
|
|
2042
|
+
}
|
|
2043
|
+
wasOpenRef.current = isOpen;
|
|
2044
|
+
}, [isOpen, projectSnapshot]);
|
|
2045
|
+
useEffect(() => {
|
|
2046
|
+
if (!hasSaveHandler) return;
|
|
2047
|
+
if (projectSnapshot === lastSavedSnapshot) {
|
|
2048
|
+
if (saveStatus !== "saving" && saveStatus !== "saved") {
|
|
2049
|
+
setSaveStatus("saved");
|
|
2050
|
+
}
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
if (saveStatus === "saved" || saveStatus === "error") {
|
|
2054
|
+
setSaveStatus("unsaved");
|
|
2055
|
+
}
|
|
2056
|
+
}, [projectSnapshot, lastSavedSnapshot, saveStatus, hasSaveHandler]);
|
|
1352
2057
|
const handleClose = useCallback(() => {
|
|
1353
2058
|
const editCount = session.history.length;
|
|
1354
2059
|
const finalCode = code;
|
|
@@ -1365,6 +2070,7 @@ ${bobbinYaml}
|
|
|
1365
2070
|
const handleSaveAndClose = useCallback(async () => {
|
|
1366
2071
|
if (!pendingClose || !hasSaveHandler) return;
|
|
1367
2072
|
setIsSaving(true);
|
|
2073
|
+
setSaveStatus("saving");
|
|
1368
2074
|
setSaveError(null);
|
|
1369
2075
|
try {
|
|
1370
2076
|
if (onSaveProject) {
|
|
@@ -1372,6 +2078,8 @@ ${bobbinYaml}
|
|
|
1372
2078
|
} else if (onSave) {
|
|
1373
2079
|
await onSave(pendingClose.code);
|
|
1374
2080
|
}
|
|
2081
|
+
setLastSavedSnapshot(projectSnapshot);
|
|
2082
|
+
setSaveStatus("saved");
|
|
1375
2083
|
setShowConfirm(false);
|
|
1376
2084
|
setEditInput("");
|
|
1377
2085
|
session.clearError();
|
|
@@ -1379,10 +2087,11 @@ ${bobbinYaml}
|
|
|
1379
2087
|
setPendingClose(null);
|
|
1380
2088
|
} catch (e) {
|
|
1381
2089
|
setSaveError(e instanceof Error ? e.message : "Save failed");
|
|
2090
|
+
setSaveStatus("error");
|
|
1382
2091
|
} finally {
|
|
1383
2092
|
setIsSaving(false);
|
|
1384
2093
|
}
|
|
1385
|
-
}, [pendingClose, onSave, onSaveProject, session, onClose]);
|
|
2094
|
+
}, [pendingClose, onSave, onSaveProject, session, onClose, projectSnapshot, hasSaveHandler]);
|
|
1386
2095
|
const handleDiscard = useCallback(() => {
|
|
1387
2096
|
if (!pendingClose) return;
|
|
1388
2097
|
setShowConfirm(false);
|
|
@@ -1399,6 +2108,7 @@ ${bobbinYaml}
|
|
|
1399
2108
|
const handleDirectSave = useCallback(async () => {
|
|
1400
2109
|
if (!hasSaveHandler) return;
|
|
1401
2110
|
setIsSaving(true);
|
|
2111
|
+
setSaveStatus("saving");
|
|
1402
2112
|
setSaveError(null);
|
|
1403
2113
|
try {
|
|
1404
2114
|
if (onSaveProject) {
|
|
@@ -1406,12 +2116,15 @@ ${bobbinYaml}
|
|
|
1406
2116
|
} else if (onSave && currentCodeRef.current) {
|
|
1407
2117
|
await onSave(currentCodeRef.current);
|
|
1408
2118
|
}
|
|
2119
|
+
setLastSavedSnapshot(projectSnapshot);
|
|
2120
|
+
setSaveStatus("saved");
|
|
1409
2121
|
} catch (e) {
|
|
1410
2122
|
setSaveError(e instanceof Error ? e.message : "Save failed");
|
|
2123
|
+
setSaveStatus("error");
|
|
1411
2124
|
} finally {
|
|
1412
2125
|
setIsSaving(false);
|
|
1413
2126
|
}
|
|
1414
|
-
}, [onSave, onSaveProject, session.project]);
|
|
2127
|
+
}, [onSave, onSaveProject, session.project, hasSaveHandler, projectSnapshot]);
|
|
1415
2128
|
if (!isOpen) return null;
|
|
1416
2129
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1417
2130
|
/* @__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 +2153,26 @@ ${bobbinYaml}
|
|
|
1440
2153
|
children: showTree ? /* @__PURE__ */ jsx(FileCode, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(FolderTree, { className: "h-3 w-3" })
|
|
1441
2154
|
}
|
|
1442
2155
|
),
|
|
2156
|
+
hasSaveHandler && /* @__PURE__ */ jsx(
|
|
2157
|
+
SaveStatusButton,
|
|
2158
|
+
{
|
|
2159
|
+
status: saveStatus,
|
|
2160
|
+
onClick: handleDirectSave,
|
|
2161
|
+
disabled: isSaving,
|
|
2162
|
+
tone: "primary"
|
|
2163
|
+
}
|
|
2164
|
+
),
|
|
1443
2165
|
showPreviewToggle && /* @__PURE__ */ jsxs(
|
|
1444
2166
|
"button",
|
|
1445
2167
|
{
|
|
1446
2168
|
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"}`,
|
|
2169
|
+
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
2170
|
children: [
|
|
1449
2171
|
showPreview ? /* @__PURE__ */ jsx(Eye, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Code, { className: "h-3 w-3" }),
|
|
1450
2172
|
showPreview ? "Preview" : "Code"
|
|
1451
2173
|
]
|
|
1452
2174
|
}
|
|
1453
2175
|
),
|
|
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
2176
|
/* @__PURE__ */ jsxs(
|
|
1468
2177
|
"button",
|
|
1469
2178
|
{
|
|
@@ -1484,11 +2193,16 @@ ${bobbinYaml}
|
|
|
1484
2193
|
{
|
|
1485
2194
|
files,
|
|
1486
2195
|
activeFile: session.activeFile,
|
|
1487
|
-
|
|
2196
|
+
activePath: effectiveTreePath,
|
|
2197
|
+
onSelectFile: (path) => {
|
|
2198
|
+
setTreePath(path);
|
|
2199
|
+
session.setActiveFile(path);
|
|
2200
|
+
},
|
|
2201
|
+
onSelectDirectory: (path) => setTreePath(path),
|
|
1488
2202
|
onReplaceFile: session.replaceFile
|
|
1489
2203
|
}
|
|
1490
2204
|
),
|
|
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: [
|
|
2205
|
+
/* @__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
2206
|
previewError && renderError ? renderError(previewError) : previewError ? /* @__PURE__ */ jsxs("div", { className: "p-4 text-sm text-destructive flex items-center gap-2", children: [
|
|
1493
2207
|
/* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0" }),
|
|
1494
2208
|
/* @__PURE__ */ jsx("span", { children: previewError })
|
|
@@ -1500,7 +2214,7 @@ ${bobbinYaml}
|
|
|
1500
2214
|
Bobbin,
|
|
1501
2215
|
{
|
|
1502
2216
|
container: previewContainer,
|
|
1503
|
-
pillContainer
|
|
2217
|
+
pillContainer,
|
|
1504
2218
|
defaultActive: false,
|
|
1505
2219
|
showInspector: true,
|
|
1506
2220
|
onChanges: handleBobbinChanges,
|
|
@@ -1515,7 +2229,14 @@ ${bobbinYaml}
|
|
|
1515
2229
|
editable: true,
|
|
1516
2230
|
onChange: session.updateActiveFile
|
|
1517
2231
|
}
|
|
1518
|
-
) :
|
|
2232
|
+
) : isMarkdown && showPreview ? /* @__PURE__ */ jsx("div", { className: "p-4 prose prose-sm dark:prose-invert max-w-none h-full overflow-auto", children: /* @__PURE__ */ jsx(
|
|
2233
|
+
MarkdownPreview,
|
|
2234
|
+
{
|
|
2235
|
+
value: code,
|
|
2236
|
+
editable: true,
|
|
2237
|
+
onChange: session.updateActiveFile
|
|
2238
|
+
}
|
|
2239
|
+
) }) : fileType.category === "text" ? /* @__PURE__ */ jsx(
|
|
1519
2240
|
CodeBlockView,
|
|
1520
2241
|
{
|
|
1521
2242
|
content: code,
|
|
@@ -1530,7 +2251,15 @@ ${bobbinYaml}
|
|
|
1530
2251
|
mimeType: getMimeType(session.activeFile),
|
|
1531
2252
|
fileName: session.activeFile.split("/").pop() ?? session.activeFile
|
|
1532
2253
|
}
|
|
1533
|
-
) : /* @__PURE__ */ jsx(
|
|
2254
|
+
) : /* @__PURE__ */ jsx(
|
|
2255
|
+
CodeBlockView,
|
|
2256
|
+
{
|
|
2257
|
+
content: code,
|
|
2258
|
+
language: fileType.language,
|
|
2259
|
+
editable: true,
|
|
2260
|
+
onChange: session.updateActiveFile
|
|
2261
|
+
}
|
|
2262
|
+
) })
|
|
1534
2263
|
] }),
|
|
1535
2264
|
/* @__PURE__ */ jsx(
|
|
1536
2265
|
EditHistory,
|
|
@@ -1596,6 +2325,77 @@ ${bobbinYaml}
|
|
|
1596
2325
|
)
|
|
1597
2326
|
] });
|
|
1598
2327
|
}
|
|
2328
|
+
function createManifest(services) {
|
|
2329
|
+
return {
|
|
2330
|
+
name: "preview",
|
|
2331
|
+
version: "1.0.0",
|
|
2332
|
+
platform: "browser",
|
|
2333
|
+
image: "@aprovan/patchwork-image-shadcn",
|
|
2334
|
+
services
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
function WidgetPreview({
|
|
2338
|
+
code,
|
|
2339
|
+
compiler,
|
|
2340
|
+
services,
|
|
2341
|
+
enabled = true
|
|
2342
|
+
}) {
|
|
2343
|
+
const [loading, setLoading] = useState(false);
|
|
2344
|
+
const [error, setError] = useState(null);
|
|
2345
|
+
const containerRef = useRef(null);
|
|
2346
|
+
const mountedRef = useRef(null);
|
|
2347
|
+
useEffect(() => {
|
|
2348
|
+
if (!enabled || !compiler || !containerRef.current) return;
|
|
2349
|
+
let cancelled = false;
|
|
2350
|
+
const compileAndMount = async () => {
|
|
2351
|
+
setLoading(true);
|
|
2352
|
+
setError(null);
|
|
2353
|
+
try {
|
|
2354
|
+
if (mountedRef.current) {
|
|
2355
|
+
compiler.unmount(mountedRef.current);
|
|
2356
|
+
mountedRef.current = null;
|
|
2357
|
+
}
|
|
2358
|
+
const widget = await compiler.compile(code, createManifest(services), {
|
|
2359
|
+
typescript: true
|
|
2360
|
+
});
|
|
2361
|
+
if (cancelled || !containerRef.current) return;
|
|
2362
|
+
const mounted = await compiler.mount(widget, {
|
|
2363
|
+
target: containerRef.current,
|
|
2364
|
+
mode: "embedded"
|
|
2365
|
+
});
|
|
2366
|
+
mountedRef.current = mounted;
|
|
2367
|
+
} catch (err) {
|
|
2368
|
+
if (!cancelled) {
|
|
2369
|
+
setError(err instanceof Error ? err.message : "Failed to render preview");
|
|
2370
|
+
}
|
|
2371
|
+
} finally {
|
|
2372
|
+
if (!cancelled) {
|
|
2373
|
+
setLoading(false);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
};
|
|
2377
|
+
void compileAndMount();
|
|
2378
|
+
return () => {
|
|
2379
|
+
cancelled = true;
|
|
2380
|
+
if (mountedRef.current && compiler) {
|
|
2381
|
+
compiler.unmount(mountedRef.current);
|
|
2382
|
+
mountedRef.current = null;
|
|
2383
|
+
}
|
|
2384
|
+
};
|
|
2385
|
+
}, [code, compiler, enabled, services]);
|
|
2386
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2387
|
+
error && /* @__PURE__ */ jsxs("div", { className: "text-sm text-destructive flex items-center gap-2 p-3", children: [
|
|
2388
|
+
/* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0" }),
|
|
2389
|
+
/* @__PURE__ */ jsx("span", { children: error })
|
|
2390
|
+
] }),
|
|
2391
|
+
loading && /* @__PURE__ */ jsxs("div", { className: "p-3 flex items-center gap-2 text-muted-foreground", children: [
|
|
2392
|
+
/* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
|
|
2393
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm", children: "Rendering preview..." })
|
|
2394
|
+
] }),
|
|
2395
|
+
!compiler && enabled && !loading && !error && /* @__PURE__ */ jsx("div", { className: "p-3 text-sm text-muted-foreground", children: "Compiler not initialized" }),
|
|
2396
|
+
/* @__PURE__ */ jsx("div", { ref: containerRef, className: "w-full" })
|
|
2397
|
+
] });
|
|
2398
|
+
}
|
|
1599
2399
|
var VFS_BASE_URL = "/vfs";
|
|
1600
2400
|
var vfsConfigCache = null;
|
|
1601
2401
|
async function getVFSConfig() {
|
|
@@ -1659,7 +2459,7 @@ async function isVFSAvailable() {
|
|
|
1659
2459
|
return false;
|
|
1660
2460
|
}
|
|
1661
2461
|
}
|
|
1662
|
-
function
|
|
2462
|
+
function createManifest2(services) {
|
|
1663
2463
|
return {
|
|
1664
2464
|
name: "preview",
|
|
1665
2465
|
version: "1.0.0",
|
|
@@ -1668,63 +2468,19 @@ function createManifest(services) {
|
|
|
1668
2468
|
services
|
|
1669
2469
|
};
|
|
1670
2470
|
}
|
|
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" }) {
|
|
2471
|
+
function CodePreview({
|
|
2472
|
+
code: originalCode,
|
|
2473
|
+
compiler,
|
|
2474
|
+
services,
|
|
2475
|
+
filePath,
|
|
2476
|
+
entrypoint = "index.ts",
|
|
2477
|
+
onOpenEditSession
|
|
2478
|
+
}) {
|
|
1723
2479
|
const [isEditing, setIsEditing] = useState(false);
|
|
1724
2480
|
const [showPreview, setShowPreview] = useState(true);
|
|
1725
2481
|
const [currentCode, setCurrentCode] = useState(originalCode);
|
|
1726
2482
|
const [editCount, setEditCount] = useState(0);
|
|
1727
|
-
const [saveStatus, setSaveStatus] = useState("
|
|
2483
|
+
const [saveStatus, setSaveStatus] = useState("saved");
|
|
1728
2484
|
const [lastSavedCode, setLastSavedCode] = useState(originalCode);
|
|
1729
2485
|
const [vfsPath, setVfsPath] = useState(null);
|
|
1730
2486
|
const currentCodeRef = useRef(currentCode);
|
|
@@ -1760,6 +2516,14 @@ function CodePreview({ code: originalCode, compiler, services, filePath, entrypo
|
|
|
1760
2516
|
useEffect(() => {
|
|
1761
2517
|
isEditingRef.current = isEditing;
|
|
1762
2518
|
}, [isEditing]);
|
|
2519
|
+
useEffect(() => {
|
|
2520
|
+
if (saveStatus === "saving") return;
|
|
2521
|
+
if (currentCode === lastSavedCode) {
|
|
2522
|
+
if (saveStatus !== "saved") setSaveStatus("saved");
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
if (saveStatus === "saved") setSaveStatus("unsaved");
|
|
2526
|
+
}, [currentCode, lastSavedCode, saveStatus]);
|
|
1763
2527
|
useEffect(() => {
|
|
1764
2528
|
let active = true;
|
|
1765
2529
|
void (async () => {
|
|
@@ -1812,14 +2576,12 @@ function CodePreview({ code: originalCode, compiler, services, filePath, entrypo
|
|
|
1812
2576
|
setSaveStatus("error");
|
|
1813
2577
|
}
|
|
1814
2578
|
}, [currentCode, getProjectId, getEntryFile]);
|
|
1815
|
-
const
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
showPreview && !isEditing,
|
|
1819
|
-
services
|
|
1820
|
-
);
|
|
2579
|
+
const previewPath = filePath ?? entrypoint;
|
|
2580
|
+
const fileType = useMemo(() => getFileType(previewPath), [previewPath]);
|
|
2581
|
+
const canRenderWidget = fileType.category === "compilable";
|
|
1821
2582
|
const compile = useCallback(
|
|
1822
2583
|
async (code) => {
|
|
2584
|
+
if (!canRenderWidget) return { success: true };
|
|
1823
2585
|
if (!compiler) return { success: true };
|
|
1824
2586
|
const errors = [];
|
|
1825
2587
|
const originalError = console.error;
|
|
@@ -1830,7 +2592,7 @@ function CodePreview({ code: originalCode, compiler, services, filePath, entrypo
|
|
|
1830
2592
|
try {
|
|
1831
2593
|
await compiler.compile(
|
|
1832
2594
|
code,
|
|
1833
|
-
|
|
2595
|
+
createManifest2(services),
|
|
1834
2596
|
{ typescript: true }
|
|
1835
2597
|
);
|
|
1836
2598
|
return { success: true };
|
|
@@ -1848,15 +2610,64 @@ ${errors.join("\n")}` : "";
|
|
|
1848
2610
|
console.error = originalError;
|
|
1849
2611
|
}
|
|
1850
2612
|
},
|
|
1851
|
-
[compiler, services]
|
|
2613
|
+
[canRenderWidget, compiler, services]
|
|
1852
2614
|
);
|
|
1853
2615
|
const handleRevert = () => {
|
|
1854
2616
|
setCurrentCode(originalCode);
|
|
1855
2617
|
setEditCount(0);
|
|
1856
2618
|
};
|
|
1857
2619
|
const hasChanges = currentCode !== originalCode;
|
|
2620
|
+
const previewBody = useMemo(() => {
|
|
2621
|
+
if (canRenderWidget) {
|
|
2622
|
+
return /* @__PURE__ */ jsx(
|
|
2623
|
+
WidgetPreview,
|
|
2624
|
+
{
|
|
2625
|
+
code: currentCode,
|
|
2626
|
+
compiler,
|
|
2627
|
+
services,
|
|
2628
|
+
enabled: showPreview && !isEditing
|
|
2629
|
+
}
|
|
2630
|
+
);
|
|
2631
|
+
}
|
|
2632
|
+
if (fileType.category === "media") {
|
|
2633
|
+
return /* @__PURE__ */ jsx(
|
|
2634
|
+
MediaPreview,
|
|
2635
|
+
{
|
|
2636
|
+
content: currentCode,
|
|
2637
|
+
mimeType: fileType.mimeType,
|
|
2638
|
+
fileName: previewPath
|
|
2639
|
+
}
|
|
2640
|
+
);
|
|
2641
|
+
}
|
|
2642
|
+
if (fileType.language === "markdown") {
|
|
2643
|
+
return /* @__PURE__ */ jsx("div", { className: "p-4 prose prose-sm dark:prose-invert max-w-none", children: /* @__PURE__ */ jsx(MarkdownPreview, { value: currentCode }) });
|
|
2644
|
+
}
|
|
2645
|
+
return /* @__PURE__ */ jsx(
|
|
2646
|
+
CodeBlockView,
|
|
2647
|
+
{
|
|
2648
|
+
content: currentCode,
|
|
2649
|
+
language: fileType.language
|
|
2650
|
+
}
|
|
2651
|
+
);
|
|
2652
|
+
}, [canRenderWidget, compiler, currentCode, fileType, isEditing, previewPath, services, showPreview]);
|
|
2653
|
+
const handleOpenEditor = useCallback(async () => {
|
|
2654
|
+
if (!onOpenEditSession) {
|
|
2655
|
+
setIsEditing(true);
|
|
2656
|
+
return;
|
|
2657
|
+
}
|
|
2658
|
+
const projectId = await getProjectId();
|
|
2659
|
+
const entryFile = getEntryFile();
|
|
2660
|
+
const initialProject = createSingleFileProject(currentCode, entryFile, projectId);
|
|
2661
|
+
onOpenEditSession({
|
|
2662
|
+
projectId,
|
|
2663
|
+
entryFile,
|
|
2664
|
+
filePath,
|
|
2665
|
+
initialCode: currentCode,
|
|
2666
|
+
initialProject
|
|
2667
|
+
});
|
|
2668
|
+
}, [onOpenEditSession, getProjectId, getEntryFile, currentCode, filePath]);
|
|
1858
2669
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1859
|
-
/* @__PURE__ */ jsxs("div", { className: "
|
|
2670
|
+
/* @__PURE__ */ jsxs("div", { className: "border rounded-lg overflow-hidden min-w-0", children: [
|
|
1860
2671
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2 bg-muted/50 border-b rounded-t-lg", children: [
|
|
1861
2672
|
/* @__PURE__ */ jsx(Code, { className: "h-4 w-4 text-muted-foreground" }),
|
|
1862
2673
|
editCount > 0 && /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground flex items-center gap-1", children: [
|
|
@@ -1865,19 +2676,6 @@ ${errors.join("\n")}` : "";
|
|
|
1865
2676
|
" edit",
|
|
1866
2677
|
editCount !== 1 ? "s" : ""
|
|
1867
2678
|
] }),
|
|
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
2679
|
/* @__PURE__ */ jsxs("div", { className: "ml-auto flex gap-1", children: [
|
|
1882
2680
|
hasChanges && /* @__PURE__ */ jsx(
|
|
1883
2681
|
"button",
|
|
@@ -1891,17 +2689,26 @@ ${errors.join("\n")}` : "";
|
|
|
1891
2689
|
/* @__PURE__ */ jsx(
|
|
1892
2690
|
"button",
|
|
1893
2691
|
{
|
|
1894
|
-
onClick: () =>
|
|
2692
|
+
onClick: () => void handleOpenEditor(),
|
|
1895
2693
|
className: "px-2 py-1 text-xs rounded flex items-center gap-1 hover:bg-muted",
|
|
1896
2694
|
title: "Edit component",
|
|
1897
2695
|
children: /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3" })
|
|
1898
2696
|
}
|
|
1899
2697
|
),
|
|
2698
|
+
/* @__PURE__ */ jsx(
|
|
2699
|
+
SaveStatusButton,
|
|
2700
|
+
{
|
|
2701
|
+
status: saveStatus,
|
|
2702
|
+
onClick: handleSave,
|
|
2703
|
+
disabled: saveStatus === "saving",
|
|
2704
|
+
tone: "muted"
|
|
2705
|
+
}
|
|
2706
|
+
),
|
|
1900
2707
|
/* @__PURE__ */ jsxs(
|
|
1901
2708
|
"button",
|
|
1902
2709
|
{
|
|
1903
2710
|
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"}`,
|
|
2711
|
+
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
2712
|
children: [
|
|
1906
2713
|
showPreview ? /* @__PURE__ */ jsx(Eye, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Code, { className: "h-3 w-3" }),
|
|
1907
2714
|
showPreview ? "Preview" : "Code"
|
|
@@ -1910,16 +2717,13 @@ ${errors.join("\n")}` : "";
|
|
|
1910
2717
|
)
|
|
1911
2718
|
] })
|
|
1912
2719
|
] }),
|
|
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 }) }) })
|
|
2720
|
+
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(
|
|
2721
|
+
CodeBlockView,
|
|
2722
|
+
{
|
|
2723
|
+
content: currentCode,
|
|
2724
|
+
language: fileType.language
|
|
2725
|
+
}
|
|
2726
|
+
) })
|
|
1923
2727
|
] }),
|
|
1924
2728
|
/* @__PURE__ */ jsx(
|
|
1925
2729
|
EditModal,
|
|
@@ -1937,6 +2741,7 @@ ${errors.join("\n")}` : "";
|
|
|
1937
2741
|
const entryFile = getEntryFile();
|
|
1938
2742
|
const project = createSingleFileProject(finalCode, entryFile, projectId);
|
|
1939
2743
|
await saveProject(project);
|
|
2744
|
+
setLastSavedCode(finalCode);
|
|
1940
2745
|
setSaveStatus("saved");
|
|
1941
2746
|
} catch (err) {
|
|
1942
2747
|
console.warn("[VFS] Failed to save project:", err);
|
|
@@ -1947,49 +2752,77 @@ ${errors.join("\n")}` : "";
|
|
|
1947
2752
|
},
|
|
1948
2753
|
originalCode: currentCode,
|
|
1949
2754
|
compile,
|
|
1950
|
-
renderPreview: (code) => /* @__PURE__ */ jsx(
|
|
2755
|
+
renderPreview: (code) => /* @__PURE__ */ jsx(
|
|
2756
|
+
WidgetPreview,
|
|
2757
|
+
{
|
|
2758
|
+
code,
|
|
2759
|
+
compiler,
|
|
2760
|
+
services
|
|
2761
|
+
}
|
|
2762
|
+
)
|
|
1951
2763
|
}
|
|
1952
2764
|
)
|
|
1953
2765
|
] });
|
|
1954
2766
|
}
|
|
1955
|
-
function
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
services
|
|
2767
|
+
function DefaultBadge({
|
|
2768
|
+
children,
|
|
2769
|
+
className = ""
|
|
1959
2770
|
}) {
|
|
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 });
|
|
2771
|
+
return /* @__PURE__ */ jsx(
|
|
2772
|
+
"span",
|
|
2773
|
+
{
|
|
2774
|
+
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}`,
|
|
2775
|
+
children
|
|
2776
|
+
}
|
|
2777
|
+
);
|
|
1976
2778
|
}
|
|
1977
|
-
function DefaultDialog({
|
|
2779
|
+
function DefaultDialog({
|
|
2780
|
+
children,
|
|
2781
|
+
open,
|
|
2782
|
+
onOpenChange
|
|
2783
|
+
}) {
|
|
1978
2784
|
if (!open) return null;
|
|
1979
|
-
return /* @__PURE__ */ jsx(
|
|
2785
|
+
return /* @__PURE__ */ jsx(
|
|
2786
|
+
"div",
|
|
2787
|
+
{
|
|
2788
|
+
className: "fixed inset-0 z-50 bg-black/50",
|
|
2789
|
+
onClick: () => onOpenChange?.(false),
|
|
2790
|
+
children: /* @__PURE__ */ jsx(
|
|
2791
|
+
"div",
|
|
2792
|
+
{
|
|
2793
|
+
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",
|
|
2794
|
+
onClick: (e) => e.stopPropagation(),
|
|
2795
|
+
children
|
|
2796
|
+
}
|
|
2797
|
+
)
|
|
2798
|
+
}
|
|
2799
|
+
);
|
|
1980
2800
|
}
|
|
1981
2801
|
function ServicesInspector({
|
|
1982
2802
|
namespaces,
|
|
1983
2803
|
services = [],
|
|
1984
2804
|
BadgeComponent = DefaultBadge,
|
|
1985
|
-
DialogComponent = DefaultDialog
|
|
2805
|
+
DialogComponent = DefaultDialog,
|
|
2806
|
+
DialogHeaderComponent = ({ children }) => /* @__PURE__ */ jsx("div", { className: "flex justify-between items-center mb-4", children }),
|
|
2807
|
+
DialogContentComponent = ({ children, className = "" }) => /* @__PURE__ */ jsx("div", { className, children }),
|
|
2808
|
+
DialogCloseComponent = ({ onClose }) => /* @__PURE__ */ jsx(
|
|
2809
|
+
"button",
|
|
2810
|
+
{
|
|
2811
|
+
onClick: () => onClose?.(),
|
|
2812
|
+
className: "text-muted-foreground hover:text-foreground",
|
|
2813
|
+
children: "\xD7"
|
|
2814
|
+
}
|
|
2815
|
+
)
|
|
1986
2816
|
}) {
|
|
1987
2817
|
const [open, setOpen] = useState(false);
|
|
1988
2818
|
if (namespaces.length === 0) return null;
|
|
1989
|
-
const groupedServices = services.reduce(
|
|
1990
|
-
(acc
|
|
1991
|
-
|
|
1992
|
-
|
|
2819
|
+
const groupedServices = services.reduce(
|
|
2820
|
+
(acc, svc) => {
|
|
2821
|
+
(acc[svc.namespace] ??= []).push(svc);
|
|
2822
|
+
return acc;
|
|
2823
|
+
},
|
|
2824
|
+
{}
|
|
2825
|
+
);
|
|
1993
2826
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1994
2827
|
/* @__PURE__ */ jsxs(
|
|
1995
2828
|
"button",
|
|
@@ -2007,11 +2840,11 @@ function ServicesInspector({
|
|
|
2007
2840
|
}
|
|
2008
2841
|
),
|
|
2009
2842
|
/* @__PURE__ */ jsxs(DialogComponent, { open, onOpenChange: setOpen, children: [
|
|
2010
|
-
/* @__PURE__ */ jsxs(
|
|
2843
|
+
/* @__PURE__ */ jsxs(DialogHeaderComponent, { children: [
|
|
2011
2844
|
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Available Services" }),
|
|
2012
|
-
/* @__PURE__ */ jsx(
|
|
2845
|
+
/* @__PURE__ */ jsx(DialogCloseComponent, { onClose: () => setOpen(false) })
|
|
2013
2846
|
] }),
|
|
2014
|
-
/* @__PURE__ */ jsx(
|
|
2847
|
+
/* @__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
2848
|
/* @__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
2849
|
/* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 text-muted-foreground" }),
|
|
2017
2850
|
/* @__PURE__ */ jsx("span", { className: "font-medium text-sm", children: ns }),
|
|
@@ -2023,11 +2856,11 @@ function ServicesInspector({
|
|
|
2023
2856
|
] }),
|
|
2024
2857
|
/* @__PURE__ */ jsx("div", { className: "ml-6 mt-2 space-y-2", children: groupedServices[ns]?.map((svc) => /* @__PURE__ */ jsxs("details", { children: [
|
|
2025
2858
|
/* @__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" }),
|
|
2859
|
+
svc.parameters && /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3" }),
|
|
2027
2860
|
/* @__PURE__ */ jsx("code", { className: "font-mono text-xs", children: svc.procedure }),
|
|
2028
2861
|
/* @__PURE__ */ jsx("span", { className: "truncate text-xs opacity-70", children: svc.description })
|
|
2029
2862
|
] }),
|
|
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
|
|
2863
|
+
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
2864
|
] }, svc.name)) ?? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "No tool details available" }) })
|
|
2032
2865
|
] }, ns)) })
|
|
2033
2866
|
] })
|
|
@@ -2159,4 +2992,4 @@ function cn(...inputs) {
|
|
|
2159
2992
|
return twMerge(clsx(inputs));
|
|
2160
2993
|
}
|
|
2161
2994
|
|
|
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 };
|
|
2995
|
+
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 };
|