@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useCallback, useMemo, useRef, type ReactNode } from 'react';
|
|
1
|
+
import { useState, useCallback, useMemo, useRef, useEffect, type ReactNode } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
Code,
|
|
4
4
|
Eye,
|
|
@@ -10,9 +10,10 @@ import {
|
|
|
10
10
|
Send,
|
|
11
11
|
FolderTree,
|
|
12
12
|
FileCode,
|
|
13
|
-
Save,
|
|
14
13
|
} from 'lucide-react';
|
|
15
14
|
import { MarkdownEditor } from '../MarkdownEditor';
|
|
15
|
+
import { MarkdownPreview } from '../MarkdownPreview';
|
|
16
|
+
import { SaveStatusButton, type SaveStatus } from '../SaveStatusButton';
|
|
16
17
|
import { EditHistory } from './EditHistory';
|
|
17
18
|
import { FileTree } from './FileTree';
|
|
18
19
|
import { SaveConfirmDialog } from './SaveConfirmDialog';
|
|
@@ -20,7 +21,7 @@ import { CodeBlockView } from './CodeBlockView';
|
|
|
20
21
|
import { MediaPreview } from './MediaPreview';
|
|
21
22
|
import { useEditSession, type UseEditSessionOptions } from './useEditSession';
|
|
22
23
|
import { getActiveContent, getFiles } from './types';
|
|
23
|
-
import { getFileType, isCompilable, getMimeType } from './fileTypes';
|
|
24
|
+
import { getFileType, isCompilable, isMarkdownFile, getMimeType } from './fileTypes';
|
|
24
25
|
import { Bobbin, serializeChangesToYAML, type Change } from '@aprovan/bobbin';
|
|
25
26
|
import type { VirtualProject } from '@aprovan/patchwork-compiler';
|
|
26
27
|
|
|
@@ -36,6 +37,7 @@ function hashCode(str: string): number {
|
|
|
36
37
|
|
|
37
38
|
export interface EditModalProps extends UseEditSessionOptions {
|
|
38
39
|
isOpen: boolean;
|
|
40
|
+
initialTreePath?: string;
|
|
39
41
|
initialState?: Partial<{
|
|
40
42
|
showTree: boolean;
|
|
41
43
|
showPreview: boolean;
|
|
@@ -61,6 +63,7 @@ export function EditModal({
|
|
|
61
63
|
renderError,
|
|
62
64
|
previewError,
|
|
63
65
|
previewLoading,
|
|
66
|
+
initialTreePath,
|
|
64
67
|
initialState = {},
|
|
65
68
|
hideFileTree = false,
|
|
66
69
|
...sessionOptions
|
|
@@ -72,21 +75,36 @@ export function EditModal({
|
|
|
72
75
|
const [editInput, setEditInput] = useState('');
|
|
73
76
|
const [bobbinChanges, setBobbinChanges] = useState<Change[]>([]);
|
|
74
77
|
const [previewContainer, setPreviewContainer] = useState<HTMLDivElement | null>(null);
|
|
78
|
+
const [pillContainer, setPillContainer] = useState<HTMLDivElement | null>(null);
|
|
75
79
|
const [showConfirm, setShowConfirm] = useState(false);
|
|
76
80
|
const [isSaving, setIsSaving] = useState(false);
|
|
81
|
+
const [saveStatus, setSaveStatus] = useState<SaveStatus>('saved');
|
|
82
|
+
const [lastSavedSnapshot, setLastSavedSnapshot] = useState('');
|
|
77
83
|
const [saveError, setSaveError] = useState<string | null>(null);
|
|
78
84
|
const [pendingClose, setPendingClose] = useState<{ code: string; count: number } | null>(null);
|
|
85
|
+
const [treePath, setTreePath] = useState(initialTreePath ?? '');
|
|
86
|
+
const wasOpenRef = useRef(false);
|
|
79
87
|
const currentCodeRef = useRef<string>('');
|
|
80
88
|
|
|
81
89
|
const session = useEditSession(sessionOptions);
|
|
82
90
|
const code = getActiveContent(session);
|
|
91
|
+
const effectiveTreePath = treePath || session.activeFile;
|
|
83
92
|
currentCodeRef.current = code;
|
|
84
93
|
const files = useMemo(() => getFiles(session.project), [session.project]);
|
|
94
|
+
const projectSnapshot = useMemo(
|
|
95
|
+
() =>
|
|
96
|
+
Array.from(session.project.files.entries())
|
|
97
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
98
|
+
.map(([path, file]) => `${path}\u0000${file.content}`)
|
|
99
|
+
.join('\u0001'),
|
|
100
|
+
[session.project]
|
|
101
|
+
);
|
|
85
102
|
const hasChanges = code !== (session.originalProject.files.get(session.activeFile)?.content ?? '');
|
|
86
103
|
|
|
87
104
|
const fileType = useMemo(() => getFileType(session.activeFile), [session.activeFile]);
|
|
88
105
|
const isCompilableFile = isCompilable(session.activeFile);
|
|
89
|
-
const
|
|
106
|
+
const isMarkdown = isMarkdownFile(session.activeFile);
|
|
107
|
+
const showPreviewToggle = isCompilableFile || isMarkdown;
|
|
90
108
|
|
|
91
109
|
const handleBobbinChanges = useCallback((changes: Change[]) => {
|
|
92
110
|
setBobbinChanges(changes);
|
|
@@ -110,6 +128,28 @@ export function EditModal({
|
|
|
110
128
|
|
|
111
129
|
const hasSaveHandler = onSave || onSaveProject;
|
|
112
130
|
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (isOpen && !wasOpenRef.current) {
|
|
133
|
+
setLastSavedSnapshot(projectSnapshot);
|
|
134
|
+
setSaveStatus('saved');
|
|
135
|
+
setSaveError(null);
|
|
136
|
+
}
|
|
137
|
+
wasOpenRef.current = isOpen;
|
|
138
|
+
}, [isOpen, projectSnapshot]);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (!hasSaveHandler) return;
|
|
142
|
+
if (projectSnapshot === lastSavedSnapshot) {
|
|
143
|
+
if (saveStatus !== 'saving' && saveStatus !== 'saved') {
|
|
144
|
+
setSaveStatus('saved');
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (saveStatus === 'saved' || saveStatus === 'error') {
|
|
149
|
+
setSaveStatus('unsaved');
|
|
150
|
+
}
|
|
151
|
+
}, [projectSnapshot, lastSavedSnapshot, saveStatus, hasSaveHandler]);
|
|
152
|
+
|
|
113
153
|
const handleClose = useCallback(() => {
|
|
114
154
|
const editCount = session.history.length;
|
|
115
155
|
const finalCode = code;
|
|
@@ -128,6 +168,7 @@ export function EditModal({
|
|
|
128
168
|
const handleSaveAndClose = useCallback(async () => {
|
|
129
169
|
if (!pendingClose || !hasSaveHandler) return;
|
|
130
170
|
setIsSaving(true);
|
|
171
|
+
setSaveStatus('saving');
|
|
131
172
|
setSaveError(null);
|
|
132
173
|
try {
|
|
133
174
|
if (onSaveProject) {
|
|
@@ -135,6 +176,8 @@ export function EditModal({
|
|
|
135
176
|
} else if (onSave) {
|
|
136
177
|
await onSave(pendingClose.code);
|
|
137
178
|
}
|
|
179
|
+
setLastSavedSnapshot(projectSnapshot);
|
|
180
|
+
setSaveStatus('saved');
|
|
138
181
|
setShowConfirm(false);
|
|
139
182
|
setEditInput('');
|
|
140
183
|
session.clearError();
|
|
@@ -142,10 +185,11 @@ export function EditModal({
|
|
|
142
185
|
setPendingClose(null);
|
|
143
186
|
} catch (e) {
|
|
144
187
|
setSaveError(e instanceof Error ? e.message : 'Save failed');
|
|
188
|
+
setSaveStatus('error');
|
|
145
189
|
} finally {
|
|
146
190
|
setIsSaving(false);
|
|
147
191
|
}
|
|
148
|
-
}, [pendingClose, onSave, onSaveProject, session, onClose]);
|
|
192
|
+
}, [pendingClose, onSave, onSaveProject, session, onClose, projectSnapshot, hasSaveHandler]);
|
|
149
193
|
|
|
150
194
|
const handleDiscard = useCallback(() => {
|
|
151
195
|
if (!pendingClose) return;
|
|
@@ -165,6 +209,7 @@ export function EditModal({
|
|
|
165
209
|
const handleDirectSave = useCallback(async () => {
|
|
166
210
|
if (!hasSaveHandler) return;
|
|
167
211
|
setIsSaving(true);
|
|
212
|
+
setSaveStatus('saving');
|
|
168
213
|
setSaveError(null);
|
|
169
214
|
try {
|
|
170
215
|
if (onSaveProject) {
|
|
@@ -172,12 +217,15 @@ export function EditModal({
|
|
|
172
217
|
} else if (onSave && currentCodeRef.current) {
|
|
173
218
|
await onSave(currentCodeRef.current);
|
|
174
219
|
}
|
|
220
|
+
setLastSavedSnapshot(projectSnapshot);
|
|
221
|
+
setSaveStatus('saved');
|
|
175
222
|
} catch (e) {
|
|
176
223
|
setSaveError(e instanceof Error ? e.message : 'Save failed');
|
|
224
|
+
setSaveStatus('error');
|
|
177
225
|
} finally {
|
|
178
226
|
setIsSaving(false);
|
|
179
227
|
}
|
|
180
|
-
}, [onSave, onSaveProject, session.project]);
|
|
228
|
+
}, [onSave, onSaveProject, session.project, hasSaveHandler, projectSnapshot]);
|
|
181
229
|
|
|
182
230
|
if (!isOpen) return null;
|
|
183
231
|
|
|
@@ -212,30 +260,23 @@ export function EditModal({
|
|
|
212
260
|
{showTree ? <FileCode className="h-3 w-3" /> : <FolderTree className="h-3 w-3" />}
|
|
213
261
|
</button>
|
|
214
262
|
)}
|
|
263
|
+
{hasSaveHandler && (
|
|
264
|
+
<SaveStatusButton
|
|
265
|
+
status={saveStatus}
|
|
266
|
+
onClick={handleDirectSave}
|
|
267
|
+
disabled={isSaving}
|
|
268
|
+
tone="primary"
|
|
269
|
+
/>
|
|
270
|
+
)}
|
|
215
271
|
{showPreviewToggle && (
|
|
216
272
|
<button
|
|
217
273
|
onClick={() => setShowPreview(!showPreview)}
|
|
218
|
-
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'}`}
|
|
274
|
+
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'}`}
|
|
219
275
|
>
|
|
220
276
|
{showPreview ? <Eye className="h-3 w-3" /> : <Code className="h-3 w-3" />}
|
|
221
277
|
{showPreview ? 'Preview' : 'Code'}
|
|
222
278
|
</button>
|
|
223
279
|
)}
|
|
224
|
-
{hasSaveHandler && (
|
|
225
|
-
<button
|
|
226
|
-
onClick={handleDirectSave}
|
|
227
|
-
disabled={isSaving}
|
|
228
|
-
className="px-2 py-1 text-xs rounded flex items-center gap-1 hover:bg-primary/20 text-primary disabled:opacity-50"
|
|
229
|
-
title="Save changes"
|
|
230
|
-
>
|
|
231
|
-
{isSaving ? (
|
|
232
|
-
<Loader2 className="h-3 w-3 animate-spin" />
|
|
233
|
-
) : (
|
|
234
|
-
<Save className="h-3 w-3" />
|
|
235
|
-
)}
|
|
236
|
-
Save
|
|
237
|
-
</button>
|
|
238
|
-
)}
|
|
239
280
|
<button
|
|
240
281
|
onClick={handleClose}
|
|
241
282
|
className="px-2 py-1 text-xs rounded flex items-center gap-1 bg-primary text-primary-foreground hover:bg-primary/90"
|
|
@@ -252,11 +293,16 @@ export function EditModal({
|
|
|
252
293
|
<FileTree
|
|
253
294
|
files={files}
|
|
254
295
|
activeFile={session.activeFile}
|
|
255
|
-
|
|
296
|
+
activePath={effectiveTreePath}
|
|
297
|
+
onSelectFile={(path) => {
|
|
298
|
+
setTreePath(path);
|
|
299
|
+
session.setActiveFile(path);
|
|
300
|
+
}}
|
|
301
|
+
onSelectDirectory={(path) => setTreePath(path)}
|
|
256
302
|
onReplaceFile={session.replaceFile}
|
|
257
303
|
/>
|
|
258
304
|
)}
|
|
259
|
-
<div className="flex-1 overflow-auto">
|
|
305
|
+
<div className="flex-1 overflow-auto" ref={setPillContainer}>
|
|
260
306
|
{fileType.category === 'compilable' && showPreview ? (
|
|
261
307
|
<div className="bg-white h-full relative" ref={setPreviewContainer}>
|
|
262
308
|
{previewError && renderError ? (
|
|
@@ -278,7 +324,7 @@ export function EditModal({
|
|
|
278
324
|
)}
|
|
279
325
|
{!renderLoading && !renderError && !previewLoading && <Bobbin
|
|
280
326
|
container={previewContainer}
|
|
281
|
-
pillContainer={
|
|
327
|
+
pillContainer={pillContainer}
|
|
282
328
|
defaultActive={false}
|
|
283
329
|
showInspector
|
|
284
330
|
onChanges={handleBobbinChanges}
|
|
@@ -292,6 +338,14 @@ export function EditModal({
|
|
|
292
338
|
editable
|
|
293
339
|
onChange={session.updateActiveFile}
|
|
294
340
|
/>
|
|
341
|
+
) : isMarkdown && showPreview ? (
|
|
342
|
+
<div className="p-4 prose prose-sm dark:prose-invert max-w-none h-full overflow-auto">
|
|
343
|
+
<MarkdownPreview
|
|
344
|
+
value={code}
|
|
345
|
+
editable
|
|
346
|
+
onChange={session.updateActiveFile}
|
|
347
|
+
/>
|
|
348
|
+
</div>
|
|
295
349
|
) : fileType.category === 'text' ? (
|
|
296
350
|
<CodeBlockView
|
|
297
351
|
content={code}
|
|
@@ -305,10 +359,14 @@ export function EditModal({
|
|
|
305
359
|
mimeType={getMimeType(session.activeFile)}
|
|
306
360
|
fileName={session.activeFile.split('/').pop() ?? session.activeFile}
|
|
307
361
|
/>
|
|
362
|
+
// Default to code view for unknown types
|
|
308
363
|
) : (
|
|
309
|
-
<
|
|
310
|
-
|
|
311
|
-
|
|
364
|
+
<CodeBlockView
|
|
365
|
+
content={code}
|
|
366
|
+
language={fileType.language}
|
|
367
|
+
editable
|
|
368
|
+
onChange={session.updateActiveFile}
|
|
369
|
+
/>
|
|
312
370
|
)}
|
|
313
371
|
</div>
|
|
314
372
|
</div>
|