agent-canvas 0.1.13 → 0.1.15
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/bin/lib/commands/push.ts +26 -10
- package/bun.lock +3 -0
- package/daemon/client/AnnotationProvider.tsx +2 -2
- package/daemon/client/App.tsx +125 -47
- package/daemon/client/CompareView.tsx +161 -31
- package/daemon/client/OverviewView.tsx +326 -0
- package/daemon/client/PlanRenderer.tsx +13 -8
- package/daemon/client/RevisionSelect.tsx +16 -7
- package/daemon/client/components/Mermaid.tsx +3 -2
- package/daemon/client/generateMarkdown.ts +24 -5
- package/daemon/client/runtime.ts +9 -2
- package/daemon/client/useTextAnnotation.tsx +6 -2
- package/daemon/dist/client.css +1 -1
- package/daemon/dist/client.js +58 -58
- package/daemon/dist/components.js +23 -23
- package/daemon/dist/runtime.js +1 -1
- package/daemon/src/compiler.ts +100 -0
- package/daemon/src/handlers/api.ts +59 -24
- package/daemon/src/session.ts +140 -41
- package/daemon/src/watcher.ts +42 -30
- package/package.json +4 -3
- package/skills/canvas/SKILL.md +24 -19
- package/skills/canvas/components.md +29 -15
- package/skills/canvas/flows.md +1 -1
package/bin/lib/commands/push.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseArgs } from "util";
|
|
2
|
-
import {
|
|
3
|
-
import { resolve } from "path";
|
|
2
|
+
import { existsSync, statSync } from "fs";
|
|
3
|
+
import { resolve, basename } from "path";
|
|
4
4
|
import { BASE_URL } from "../config.ts";
|
|
5
5
|
import { ensureDaemon } from "../daemon-lifecycle.ts";
|
|
6
6
|
import { getSessionId, openBrowser } from "../helpers.ts";
|
|
@@ -16,30 +16,40 @@ export async function handlePush(args: string[]) {
|
|
|
16
16
|
allowPositionals: true,
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
const
|
|
20
|
-
if (!
|
|
21
|
-
console.error("Error: No
|
|
19
|
+
const target = positionals[0];
|
|
20
|
+
if (!target) {
|
|
21
|
+
console.error("Error: No directory specified. Usage: agent-canvas push <directory>");
|
|
22
22
|
process.exit(1);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const resolvedPath = resolve(
|
|
25
|
+
const resolvedPath = resolve(target);
|
|
26
26
|
if (!existsSync(resolvedPath)) {
|
|
27
|
-
console.error(`Error:
|
|
27
|
+
console.error(`Error: Path not found: ${resolvedPath}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (statSync(resolvedPath).isFile()) {
|
|
32
|
+
console.error(`Error: Expected a directory, got a file: ${resolvedPath}`);
|
|
33
|
+
console.error("Usage: agent-canvas push <directory>");
|
|
28
34
|
process.exit(1);
|
|
29
35
|
}
|
|
30
|
-
const jsx = readFileSync(resolvedPath, "utf-8");
|
|
31
36
|
|
|
32
37
|
const sessionId = getSessionId(values.session);
|
|
33
38
|
const projectRoot = process.env.CANVAS_PROJECT_ROOT || process.cwd();
|
|
34
39
|
const label = values.label;
|
|
35
|
-
const autoLabel = !label ? resolvedPath
|
|
40
|
+
const autoLabel = !label ? basename(resolvedPath) : undefined;
|
|
36
41
|
|
|
37
42
|
await ensureDaemon();
|
|
38
43
|
|
|
39
44
|
const response = await fetch(`${BASE_URL}/api/session/${sessionId}/plan`, {
|
|
40
45
|
method: "POST",
|
|
41
46
|
headers: { "Content-Type": "application/json" },
|
|
42
|
-
body: JSON.stringify({
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
directory: resolvedPath,
|
|
49
|
+
projectRoot,
|
|
50
|
+
label: label || autoLabel,
|
|
51
|
+
...(values.response ? { response: values.response } : {}),
|
|
52
|
+
}),
|
|
43
53
|
});
|
|
44
54
|
|
|
45
55
|
const result = await response.json() as any;
|
|
@@ -50,6 +60,11 @@ export async function handlePush(args: string[]) {
|
|
|
50
60
|
console.log(result.unconsumedFeedback);
|
|
51
61
|
} else {
|
|
52
62
|
console.error(`Compilation error:\n${result.error}`);
|
|
63
|
+
if (result.errors) {
|
|
64
|
+
for (const [file, err] of Object.entries(result.errors)) {
|
|
65
|
+
console.error(` ${file}: ${err}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
53
68
|
}
|
|
54
69
|
process.exit(1);
|
|
55
70
|
}
|
|
@@ -63,5 +78,6 @@ export async function handlePush(args: string[]) {
|
|
|
63
78
|
browserUrl: result.browserUrl,
|
|
64
79
|
revision: result.revision,
|
|
65
80
|
sessionId,
|
|
81
|
+
canvasFiles: result.canvasFiles,
|
|
66
82
|
}));
|
|
67
83
|
}
|
package/bun.lock
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"marked": "^17.0.4",
|
|
9
9
|
"preact": "^10.25.0",
|
|
10
|
+
"preact-render-to-string": "^6.6.6",
|
|
10
11
|
},
|
|
11
12
|
"devDependencies": {
|
|
12
13
|
"@tailwindcss/cli": "4",
|
|
@@ -151,6 +152,8 @@
|
|
|
151
152
|
|
|
152
153
|
"preact": ["preact@10.29.0", "", {}, "sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg=="],
|
|
153
154
|
|
|
155
|
+
"preact-render-to-string": ["preact-render-to-string@6.6.6", "", { "peerDependencies": { "preact": ">=10 || >= 11.0.0-0" } }, "sha512-EfqZJytnjJldV+YaaqhthU2oXsEf5e+6rDv957p+zxAvNfFLQOPfvBOTncscQ+akzu6Wrl7s3Pa0LjUQmWJsGQ=="],
|
|
156
|
+
|
|
154
157
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
|
155
158
|
|
|
156
159
|
"tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
|
|
@@ -82,8 +82,8 @@ export function AnnotationProvider({ sessionId, revision, isReadOnly, children }
|
|
|
82
82
|
return () => { if (persistTimerRef.current) clearTimeout(persistTimerRef.current); };
|
|
83
83
|
}, [annotations, generalNote, responses, feedbackEntries, sessionId, revision, isReadOnly]);
|
|
84
84
|
|
|
85
|
-
const addAnnotationWithId = useCallback((id: string, snippet: string, note: string, filePath?: string, context?: AnnotationContext, images?: string[]) => {
|
|
86
|
-
setAnnotations((prev) => [...prev, { id, snippet, note, createdAt: new Date().toISOString(), filePath, context, ...(images?.length ? { images } : {}) }]);
|
|
85
|
+
const addAnnotationWithId = useCallback((id: string, snippet: string, note: string, filePath?: string, context?: AnnotationContext, images?: string[], canvasFile?: string) => {
|
|
86
|
+
setAnnotations((prev) => [...prev, { id, snippet, note, createdAt: new Date().toISOString(), filePath, context, ...(images?.length ? { images } : {}), ...(canvasFile ? { canvasFile } : {}) }]);
|
|
87
87
|
}, []);
|
|
88
88
|
|
|
89
89
|
const addAnnotation = useCallback((snippet: string, note: string, filePath?: string) => {
|
package/daemon/client/App.tsx
CHANGED
|
@@ -12,22 +12,27 @@ import { FileViewer } from "./FileViewer";
|
|
|
12
12
|
import { SessionSwitcher } from "./SessionSwitcher";
|
|
13
13
|
import { exportCanvasToMarkdown } from "./exportMarkdown";
|
|
14
14
|
import { CompareView } from "./CompareView";
|
|
15
|
+
import { OverviewView, categorizeChanges, getAffectedFiles } from "./OverviewView";
|
|
15
16
|
import { RevisionSelect } from "./RevisionSelect";
|
|
16
17
|
import { generateAnnotationId, RESPONSE_ANNOTATION_PATH } from "./utils";
|
|
17
18
|
import { wrapRangeWithMark, restoreMarks, renameMarkId, unwrapMarks, updateAllMarkStates } from "./highlightRange";
|
|
18
19
|
import { extractContext } from "./annotationContext";
|
|
19
20
|
import { AnnotationCreatePopover, AnnotationEditPopover } from "./Popover";
|
|
20
21
|
|
|
21
|
-
export type ActiveView = { type: "
|
|
22
|
+
export type ActiveView = { type: "overview" } | { type: "canvas"; filename: string } | { type: "file"; path: string };
|
|
23
|
+
|
|
24
|
+
export interface CanvasFileInfo {
|
|
25
|
+
filename: string;
|
|
26
|
+
diffStats?: { added: number; removed: number };
|
|
27
|
+
}
|
|
22
28
|
|
|
23
29
|
export interface RevisionInfo {
|
|
24
30
|
revision: number;
|
|
25
31
|
label?: string;
|
|
26
|
-
|
|
32
|
+
canvasFiles: CanvasFileInfo[];
|
|
27
33
|
createdAt: string;
|
|
28
34
|
hasFeedback: boolean;
|
|
29
35
|
response?: string;
|
|
30
|
-
diffStats?: { added: number; removed: number };
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
export const ActiveViewContext = createContext<{
|
|
@@ -35,11 +40,13 @@ export const ActiveViewContext = createContext<{
|
|
|
35
40
|
setActiveView: (v: ActiveView) => void;
|
|
36
41
|
openFiles: string[];
|
|
37
42
|
closeFile: (path: string) => void;
|
|
43
|
+
canvasFiles: string[];
|
|
38
44
|
}>({
|
|
39
|
-
activeView: { type: "
|
|
45
|
+
activeView: { type: "overview" },
|
|
40
46
|
setActiveView: () => {},
|
|
41
47
|
openFiles: [],
|
|
42
48
|
closeFile: () => {},
|
|
49
|
+
canvasFiles: [],
|
|
43
50
|
});
|
|
44
51
|
|
|
45
52
|
export const RevisionContext = createContext<{
|
|
@@ -146,14 +153,11 @@ function RevisionSelector() {
|
|
|
146
153
|
/>
|
|
147
154
|
<button
|
|
148
155
|
onClick={() => {
|
|
149
|
-
const current = revisions.find((r) => r.revision === selectedRevision);
|
|
150
|
-
const sourceFile = current?.sourceFile;
|
|
151
156
|
let left: number;
|
|
152
157
|
if (selectedRevision === currentRevision) {
|
|
153
|
-
// Find previous revision with same sourceFile
|
|
154
158
|
const prev = [...revisions]
|
|
155
159
|
.reverse()
|
|
156
|
-
.find((r) => r.revision < currentRevision
|
|
160
|
+
.find((r) => r.revision < currentRevision);
|
|
157
161
|
left = prev?.revision ?? Math.max(1, currentRevision - 1);
|
|
158
162
|
} else {
|
|
159
163
|
left = selectedRevision;
|
|
@@ -176,7 +180,8 @@ function App() {
|
|
|
176
180
|
const [compareRevision, setCompareRevision] = useState<{ left: number; right: number } | null>(null);
|
|
177
181
|
const [connected, setConnected] = useState(false);
|
|
178
182
|
const [previewOpen, setPreviewOpen] = useState(false);
|
|
179
|
-
const [
|
|
183
|
+
const [canvasFiles, setCanvasFiles] = useState<string[]>([]);
|
|
184
|
+
const [activeView, setActiveViewRaw] = useState<ActiveView>({ type: "overview" });
|
|
180
185
|
const [openFiles, setOpenFiles] = useState<string[]>([]);
|
|
181
186
|
const wsRef = useRef<WebSocket | null>(null);
|
|
182
187
|
|
|
@@ -196,6 +201,10 @@ function App() {
|
|
|
196
201
|
}
|
|
197
202
|
if (activeView.type === "file") {
|
|
198
203
|
parts.push(activeView.path.split("/").pop() || activeView.path);
|
|
204
|
+
} else if (activeView.type === "overview") {
|
|
205
|
+
parts.push("Overview");
|
|
206
|
+
} else if (activeView.type === "canvas" && canvasFiles.length > 1 && activeView.filename) {
|
|
207
|
+
parts.push(activeView.filename.replace(/\.jsx$/, ""));
|
|
199
208
|
} else if (compareRevision) {
|
|
200
209
|
parts.push("Compare");
|
|
201
210
|
}
|
|
@@ -210,10 +219,8 @@ function App() {
|
|
|
210
219
|
|
|
211
220
|
const setActiveView = useCallback((v: ActiveView) => {
|
|
212
221
|
// Save current scroll position before switching
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
window.scrollY
|
|
216
|
-
);
|
|
222
|
+
const key = activeView.type === "overview" ? "overview" : activeView.type === "canvas" ? `canvas:${activeView.filename}` : `file:${activeView.path}`;
|
|
223
|
+
scrollPositions.current.set(key, window.scrollY);
|
|
217
224
|
setActiveViewRaw(v);
|
|
218
225
|
if (v.type === "file") {
|
|
219
226
|
setOpenFiles((prev) => prev.includes(v.path) ? prev : [...prev, v.path]);
|
|
@@ -225,16 +232,15 @@ function App() {
|
|
|
225
232
|
setOpenFiles((prev) => prev.filter((p) => p !== path));
|
|
226
233
|
setActiveViewRaw((prev) => {
|
|
227
234
|
if (prev.type === "file" && prev.path === path) {
|
|
228
|
-
|
|
229
|
-
return { type: "plan" };
|
|
235
|
+
return canvasFiles.length > 1 ? { type: "overview" } : { type: "canvas", filename: canvasFiles[0] || "" };
|
|
230
236
|
}
|
|
231
237
|
return prev;
|
|
232
238
|
});
|
|
233
|
-
}, []);
|
|
239
|
+
}, [canvasFiles]);
|
|
234
240
|
|
|
235
241
|
// Restore scroll synchronously after DOM update (before paint)
|
|
236
242
|
useLayoutEffect(() => {
|
|
237
|
-
const key = activeView.type === "
|
|
243
|
+
const key = activeView.type === "overview" ? "overview" : activeView.type === "canvas" ? `canvas:${activeView.filename}` : `file:${activeView.path}`;
|
|
238
244
|
window.scrollTo(0, scrollPositions.current.get(key) || 0);
|
|
239
245
|
}, [activeView]);
|
|
240
246
|
|
|
@@ -249,6 +255,20 @@ function App() {
|
|
|
249
255
|
setSelectedRevision(data.currentRevision);
|
|
250
256
|
setRevisions(data.revisions || []);
|
|
251
257
|
}
|
|
258
|
+
if (data.canvasFiles) {
|
|
259
|
+
const files = (data.canvasFiles as string[]).sort();
|
|
260
|
+
setCanvasFiles(files);
|
|
261
|
+
setActiveViewRaw((prev) => {
|
|
262
|
+
if (prev.type === "overview") {
|
|
263
|
+
// On initial load: overview if multiple files, direct if single
|
|
264
|
+
return files.length > 1 ? prev : { type: "canvas", filename: files[0] || "" };
|
|
265
|
+
}
|
|
266
|
+
if (prev.type === "canvas" && (!prev.filename || !files.includes(prev.filename))) {
|
|
267
|
+
return { type: "canvas", filename: files[0] || "" };
|
|
268
|
+
}
|
|
269
|
+
return prev;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
252
272
|
if (data.projectRoot) setProjectRoot(data.projectRoot);
|
|
253
273
|
})
|
|
254
274
|
.catch(() => {});
|
|
@@ -270,7 +290,26 @@ function App() {
|
|
|
270
290
|
setCurrentRevision(data.currentRevision);
|
|
271
291
|
setSelectedRevision(data.currentRevision);
|
|
272
292
|
setCompareRevision(null);
|
|
273
|
-
if (data.revisions)
|
|
293
|
+
if (data.revisions) {
|
|
294
|
+
const allRevisions = data.revisions as RevisionInfo[];
|
|
295
|
+
setRevisions(allRevisions);
|
|
296
|
+
const latest = allRevisions.find((r: RevisionInfo) => r.revision === data.currentRevision);
|
|
297
|
+
if (latest?.canvasFiles) {
|
|
298
|
+
const files = latest.canvasFiles.map((cf: CanvasFileInfo) => cf.filename).sort();
|
|
299
|
+
setCanvasFiles(files);
|
|
300
|
+
// Determine what changed and auto-navigate
|
|
301
|
+
const previous = allRevisions.find((r: RevisionInfo) => r.revision === data.currentRevision - 1);
|
|
302
|
+
const changes = categorizeChanges(latest, previous);
|
|
303
|
+
const affected = getAffectedFiles(changes);
|
|
304
|
+
if (affected.length === 1) {
|
|
305
|
+
setActiveViewRaw({ type: "canvas", filename: affected[0] });
|
|
306
|
+
} else if (affected.length > 1 || files.length > 1) {
|
|
307
|
+
setActiveViewRaw({ type: "overview" });
|
|
308
|
+
} else {
|
|
309
|
+
setActiveViewRaw({ type: "canvas", filename: files[0] || "" });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
274
313
|
}
|
|
275
314
|
if (data.type === "revision-updated") {
|
|
276
315
|
if (data.revisions) setRevisions(data.revisions);
|
|
@@ -299,7 +338,7 @@ function App() {
|
|
|
299
338
|
<SessionContext.Provider value={sessionId}>
|
|
300
339
|
<RevisionContext.Provider value={{ currentRevision, selectedRevision, revisions, setSelectedRevision, isReadOnly, compareRevision, setCompareRevision }}>
|
|
301
340
|
<AnnotationProvider key={`${sessionId}:${selectedRevision}`} sessionId={sessionId} revision={selectedRevision} isReadOnly={isReadOnly}>
|
|
302
|
-
<ActiveViewContext.Provider value={{ activeView, setActiveView, openFiles, closeFile }}>
|
|
341
|
+
<ActiveViewContext.Provider value={{ activeView, setActiveView, openFiles, closeFile, canvasFiles }}>
|
|
303
342
|
<ActiveViewCtx.Provider value={{ setActiveView }}>
|
|
304
343
|
<div className="min-h-screen bg-bg-base">
|
|
305
344
|
{/* Left panel — fixed to viewport */}
|
|
@@ -344,12 +383,19 @@ function App() {
|
|
|
344
383
|
</div>
|
|
345
384
|
|
|
346
385
|
<ContentTabs />
|
|
347
|
-
{/*
|
|
348
|
-
|
|
386
|
+
{/* Overview — shows all affected canvases inline */}
|
|
387
|
+
{activeView.type === "overview" && (
|
|
388
|
+
<OverviewView
|
|
389
|
+
revision={selectedRevision}
|
|
390
|
+
responseBanner={selectedRevInfo?.response ? <ResponseBanner markdown={selectedRevInfo.response} /> : undefined}
|
|
391
|
+
/>
|
|
392
|
+
)}
|
|
393
|
+
{/* Individual canvas view — only mounted when active */}
|
|
394
|
+
{activeView.type === "canvas" && canvasFiles.includes(activeView.filename) && (
|
|
349
395
|
<div className="relative max-w-[720px] mx-auto px-6 pt-12 pb-32">
|
|
350
396
|
<button
|
|
351
397
|
onClick={() => {
|
|
352
|
-
const planContent = document.querySelector(".plan-content
|
|
398
|
+
const planContent = document.querySelector(`[data-canvas-file="${activeView.filename}"] .plan-content`);
|
|
353
399
|
if (!planContent) return;
|
|
354
400
|
const md = exportCanvasToMarkdown(planContent as HTMLElement);
|
|
355
401
|
navigator.clipboard.writeText(md).then(() => {
|
|
@@ -372,12 +418,15 @@ function App() {
|
|
|
372
418
|
<polyline points="20 6 9 17 4 12"/>
|
|
373
419
|
</svg>
|
|
374
420
|
</button>
|
|
375
|
-
{
|
|
421
|
+
{/* Show response banner on individual canvas only when there's no overview (single canvas) */}
|
|
422
|
+
{canvasFiles.length <= 1 && selectedRevInfo?.response && (
|
|
376
423
|
<ResponseBanner markdown={selectedRevInfo.response} />
|
|
377
424
|
)}
|
|
378
|
-
<
|
|
425
|
+
<div data-canvas-file={activeView.filename}>
|
|
426
|
+
<PlanRenderer key={activeView.filename} revision={selectedRevision} filename={activeView.filename} />
|
|
427
|
+
</div>
|
|
379
428
|
</div>
|
|
380
|
-
|
|
429
|
+
)}
|
|
381
430
|
{openFiles.map((filePath) => (
|
|
382
431
|
<div key={filePath} style={{ display: activeView.type === "file" && activeView.path === filePath ? undefined : "none" }}>
|
|
383
432
|
<FileViewer path={filePath} />
|
|
@@ -715,37 +764,66 @@ function ResponseBanner({ markdown }: { markdown: string }) {
|
|
|
715
764
|
}
|
|
716
765
|
|
|
717
766
|
function ContentTabs() {
|
|
718
|
-
const { activeView, setActiveView, openFiles, closeFile } = React.useContext(ActiveViewContext);
|
|
767
|
+
const { activeView, setActiveView, openFiles, closeFile, canvasFiles } = React.useContext(ActiveViewContext);
|
|
719
768
|
const sessionId = React.useContext(SessionContext);
|
|
720
769
|
const { addAnnotationWithId, setActiveAnnotationId } = useAnnotations();
|
|
721
770
|
|
|
722
|
-
const showTabs = openFiles.length > 0;
|
|
771
|
+
const showTabs = canvasFiles.length > 1 || openFiles.length > 0;
|
|
723
772
|
if (!showTabs) return null;
|
|
724
773
|
|
|
725
774
|
return (
|
|
726
775
|
<div className="flex items-center border-b border-border-medium bg-bg-surface overflow-x-auto sticky top-0 z-10">
|
|
727
|
-
{/*
|
|
728
|
-
{
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
<
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
776
|
+
{/* Overview tab — only when multiple canvases */}
|
|
777
|
+
{canvasFiles.length > 1 && (
|
|
778
|
+
<>
|
|
779
|
+
<button
|
|
780
|
+
onClick={() => setActiveView({ type: "overview" })}
|
|
781
|
+
className={`flex items-center gap-1.5 px-4 py-2.5 text-[13px] font-body whitespace-nowrap transition-colors relative ${
|
|
782
|
+
activeView.type === "overview"
|
|
783
|
+
? "text-text-primary"
|
|
784
|
+
: "text-text-tertiary hover:text-text-secondary"
|
|
785
|
+
}`}
|
|
786
|
+
>
|
|
787
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" className="opacity-60">
|
|
788
|
+
<path d="M12 7L2 12l10 5 10-5-10-5z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
789
|
+
</svg>
|
|
790
|
+
Overview
|
|
791
|
+
{activeView.type === "overview" && (
|
|
792
|
+
<span className="absolute bottom-0 left-3 right-3 h-[2px] bg-text-primary rounded-full" />
|
|
793
|
+
)}
|
|
794
|
+
</button>
|
|
795
|
+
<span className="w-px h-4 bg-border-medium flex-shrink-0" />
|
|
796
|
+
</>
|
|
745
797
|
)}
|
|
746
798
|
|
|
747
|
-
{
|
|
748
|
-
{
|
|
799
|
+
{/* Canvas tabs */}
|
|
800
|
+
{canvasFiles.map((filename, i) => {
|
|
801
|
+
const label = filename.replace(/\.jsx$/, "");
|
|
802
|
+
const isActive = activeView.type === "canvas" && activeView.filename === filename;
|
|
803
|
+
return (
|
|
804
|
+
<React.Fragment key={filename}>
|
|
805
|
+
<button
|
|
806
|
+
onClick={() => setActiveView({ type: "canvas", filename })}
|
|
807
|
+
className={`flex items-center gap-1.5 px-4 py-2.5 text-[13px] font-body whitespace-nowrap transition-colors relative ${
|
|
808
|
+
isActive
|
|
809
|
+
? "text-text-primary"
|
|
810
|
+
: "text-text-tertiary hover:text-text-secondary"
|
|
811
|
+
}`}
|
|
812
|
+
>
|
|
813
|
+
{label}
|
|
814
|
+
{isActive && (
|
|
815
|
+
<span className="absolute bottom-0 left-3 right-3 h-[2px] bg-text-primary rounded-full" />
|
|
816
|
+
)}
|
|
817
|
+
</button>
|
|
818
|
+
{i < canvasFiles.length - 1 && (
|
|
819
|
+
<span className="w-px h-4 bg-border-medium flex-shrink-0" />
|
|
820
|
+
)}
|
|
821
|
+
</React.Fragment>
|
|
822
|
+
);
|
|
823
|
+
})}
|
|
824
|
+
|
|
825
|
+
{openFiles.length > 0 && <>
|
|
826
|
+
{/* Separator between canvas and file tabs */}
|
|
749
827
|
<span className="w-px h-4 bg-border-medium flex-shrink-0" />
|
|
750
828
|
|
|
751
829
|
{/* File tabs */}
|