@run0/jiki-ui 0.1.0

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/dist/index.mjs ADDED
@@ -0,0 +1,1720 @@
1
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
+ import { useRef, useEffect, useState, useCallback, useMemo } from "react";
3
+ const EDITOR_THEMES = {
4
+ emerald: {
5
+ saveButtonActive: "bg-emerald-500/20 text-emerald-400 hover:bg-emerald-500/30 border border-emerald-500/30",
6
+ caret: "caret-emerald-400",
7
+ selection: "selection:bg-emerald-500/20"
8
+ },
9
+ violet: {
10
+ saveButtonActive: "bg-violet-500/20 text-violet-400 hover:bg-violet-500/30 border border-violet-500/30",
11
+ caret: "caret-violet-400",
12
+ selection: "selection:bg-violet-500/20"
13
+ },
14
+ orange: {
15
+ saveButtonActive: "bg-orange-500/20 text-orange-400 hover:bg-orange-500/30 border border-orange-500/30",
16
+ caret: "caret-orange-400",
17
+ selection: "selection:bg-orange-500/20"
18
+ },
19
+ blue: {
20
+ saveButtonActive: "bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 border border-blue-500/30",
21
+ caret: "caret-blue-400",
22
+ selection: "selection:bg-blue-500/20"
23
+ },
24
+ pink: {
25
+ saveButtonActive: "bg-pink-500/20 text-pink-400 hover:bg-pink-500/30 border border-pink-500/30",
26
+ caret: "caret-pink-400",
27
+ selection: "selection:bg-pink-500/20"
28
+ },
29
+ green: {
30
+ saveButtonActive: "bg-green-500/20 text-green-400 hover:bg-green-500/30 border border-green-500/30",
31
+ caret: "caret-green-400",
32
+ selection: "selection:bg-green-500/20"
33
+ },
34
+ amber: {
35
+ saveButtonActive: "bg-amber-500/20 text-amber-400 hover:bg-amber-500/30 border border-amber-500/30",
36
+ caret: "caret-amber-400",
37
+ selection: "selection:bg-amber-500/20"
38
+ }
39
+ };
40
+ const TERMINAL_THEMES = {
41
+ emerald: {
42
+ commandStyle: "text-emerald-400 font-semibold",
43
+ promptColor: "text-emerald-500",
44
+ spinnerBorder: "border-t-emerald-400",
45
+ inputCaret: "caret-emerald-400"
46
+ },
47
+ violet: {
48
+ commandStyle: "text-violet-400 font-semibold",
49
+ promptColor: "text-violet-500",
50
+ spinnerBorder: "border-t-violet-400",
51
+ inputCaret: "caret-violet-400"
52
+ },
53
+ orange: {
54
+ commandStyle: "text-orange-400 font-semibold",
55
+ promptColor: "text-orange-500",
56
+ spinnerBorder: "border-t-orange-400",
57
+ inputCaret: "caret-orange-400"
58
+ },
59
+ blue: {
60
+ commandStyle: "text-blue-400 font-semibold",
61
+ promptColor: "text-blue-500",
62
+ spinnerBorder: "border-t-blue-400",
63
+ inputCaret: "caret-blue-400"
64
+ },
65
+ pink: {
66
+ commandStyle: "text-pink-400 font-semibold",
67
+ promptColor: "text-pink-500",
68
+ spinnerBorder: "border-t-pink-400",
69
+ inputCaret: "caret-pink-400"
70
+ },
71
+ green: {
72
+ commandStyle: "text-green-400 font-semibold",
73
+ promptColor: "text-green-500",
74
+ spinnerBorder: "border-t-green-400",
75
+ inputCaret: "caret-green-400"
76
+ },
77
+ amber: {
78
+ commandStyle: "text-amber-400 font-semibold",
79
+ promptColor: "text-amber-500",
80
+ spinnerBorder: "border-t-amber-400",
81
+ inputCaret: "caret-amber-400"
82
+ }
83
+ };
84
+ const FILE_EXPLORER_THEMES = {
85
+ emerald: { selected: "bg-emerald-500/15 text-emerald-300" },
86
+ violet: { selected: "bg-violet-500/15 text-violet-300" },
87
+ orange: { selected: "bg-orange-500/15 text-orange-300" },
88
+ blue: { selected: "bg-blue-500/15 text-blue-300" },
89
+ pink: { selected: "bg-pink-500/15 text-pink-300" },
90
+ green: { selected: "bg-green-500/15 text-green-300" },
91
+ amber: { selected: "bg-amber-500/15 text-amber-300" }
92
+ };
93
+ function getFileExplorerTheme(color) {
94
+ return FILE_EXPLORER_THEMES[color];
95
+ }
96
+ function getEditorTheme(color) {
97
+ return EDITOR_THEMES[color];
98
+ }
99
+ function getTerminalTheme(color) {
100
+ return TERMINAL_THEMES[color];
101
+ }
102
+ function getLineStyles(color, overrides) {
103
+ return {
104
+ command: TERMINAL_THEMES[color].commandStyle,
105
+ stdout: "text-zinc-300",
106
+ stderr: "text-red-400",
107
+ info: "text-blue-400 italic",
108
+ success: "text-emerald-400",
109
+ ...overrides
110
+ };
111
+ }
112
+ const LANGUAGE_MAP = {
113
+ js: "JavaScript",
114
+ jsx: "JSX",
115
+ mjs: "JavaScript",
116
+ cjs: "JavaScript",
117
+ ts: "TypeScript",
118
+ tsx: "TSX",
119
+ mts: "TypeScript",
120
+ cts: "TypeScript",
121
+ json: "JSON",
122
+ md: "Markdown",
123
+ html: "HTML",
124
+ css: "CSS",
125
+ vue: "Vue SFC",
126
+ svelte: "Svelte",
127
+ astro: "Astro",
128
+ yaml: "YAML",
129
+ yml: "YAML",
130
+ toml: "TOML",
131
+ sh: "Shell"
132
+ };
133
+ function getLanguageLabel(filename) {
134
+ const ext = filename.split(".").pop()?.toLowerCase();
135
+ return LANGUAGE_MAP[ext || ""] || "Plain Text";
136
+ }
137
+ const SHIKI_LANG_MAP = {
138
+ js: "javascript",
139
+ mjs: "javascript",
140
+ cjs: "javascript",
141
+ ts: "typescript",
142
+ mts: "typescript",
143
+ cts: "typescript",
144
+ jsx: "jsx",
145
+ tsx: "tsx",
146
+ json: "json",
147
+ html: "html",
148
+ css: "css",
149
+ md: "markdown",
150
+ vue: "vue",
151
+ svelte: "svelte",
152
+ astro: "astro",
153
+ yaml: "yaml",
154
+ yml: "yaml",
155
+ toml: "toml",
156
+ sh: "shellscript"
157
+ };
158
+ function getShikiLang(filename) {
159
+ if (!filename) return "plaintext";
160
+ const ext = filename.split(".").pop()?.toLowerCase();
161
+ return SHIKI_LANG_MAP[ext || ""] || "plaintext";
162
+ }
163
+ let highlighterInstance = null;
164
+ let highlighterPromise = null;
165
+ async function getOrCreateHighlighter() {
166
+ if (highlighterInstance) return highlighterInstance;
167
+ if (highlighterPromise) return highlighterPromise;
168
+ highlighterPromise = import("shiki").then(
169
+ ({ createHighlighter }) => createHighlighter({ themes: ["github-dark"], langs: [] })
170
+ ).then((h) => {
171
+ highlighterInstance = h;
172
+ return h;
173
+ });
174
+ return highlighterPromise;
175
+ }
176
+ function escapeHtml(text) {
177
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
178
+ }
179
+ function buildFallbackHtml(code) {
180
+ return `<pre style="margin:0;background:transparent"><code style="color:#e4e4e7">${escapeHtml(code)}</code></pre>`;
181
+ }
182
+ async function doHighlight(code, filename, el) {
183
+ if (!el || !highlighterInstance) return;
184
+ const lang = getShikiLang(filename);
185
+ if (lang !== "plaintext" && !highlighterInstance.getLoadedLanguages().includes(lang)) {
186
+ try {
187
+ await highlighterInstance.loadLanguage(
188
+ lang
189
+ );
190
+ } catch {
191
+ el.innerHTML = buildFallbackHtml(code);
192
+ return;
193
+ }
194
+ }
195
+ try {
196
+ el.innerHTML = highlighterInstance.codeToHtml(code, {
197
+ lang,
198
+ theme: "github-dark"
199
+ });
200
+ } catch {
201
+ el.innerHTML = buildFallbackHtml(code);
202
+ }
203
+ }
204
+ function useShikiHighlighter(code, filename, targetRef) {
205
+ const readyRef = useRef(false);
206
+ const debounceRef = useRef(
207
+ void 0
208
+ );
209
+ useEffect(() => {
210
+ let cancelled = false;
211
+ getOrCreateHighlighter().then(() => {
212
+ if (cancelled) return;
213
+ readyRef.current = true;
214
+ doHighlight(code, filename, targetRef.current);
215
+ });
216
+ return () => {
217
+ cancelled = true;
218
+ };
219
+ }, []);
220
+ useEffect(() => {
221
+ if (!readyRef.current) {
222
+ if (targetRef.current) {
223
+ targetRef.current.innerHTML = buildFallbackHtml(code);
224
+ }
225
+ return;
226
+ }
227
+ clearTimeout(debounceRef.current);
228
+ debounceRef.current = setTimeout(() => {
229
+ doHighlight(code, filename, targetRef.current);
230
+ }, 30);
231
+ return () => clearTimeout(debounceRef.current);
232
+ }, [code, filename]);
233
+ }
234
+ function CodeEditor({
235
+ filename,
236
+ content,
237
+ onSave,
238
+ accentColor = "emerald"
239
+ }) {
240
+ const [localContent, setLocalContent] = useState(content);
241
+ const [isDirty, setIsDirty] = useState(false);
242
+ const textareaRef = useRef(null);
243
+ const lineCountRef = useRef(null);
244
+ const highlightRef = useRef(null);
245
+ const theme = getEditorTheme(accentColor);
246
+ useShikiHighlighter(localContent, filename, highlightRef);
247
+ useEffect(() => {
248
+ setLocalContent(content);
249
+ setIsDirty(false);
250
+ }, [content, filename]);
251
+ const handleChange = useCallback(
252
+ (e) => {
253
+ setLocalContent(e.target.value);
254
+ setIsDirty(e.target.value !== content);
255
+ },
256
+ [content]
257
+ );
258
+ const handleSave = useCallback(() => {
259
+ if (filename && isDirty) {
260
+ onSave(filename, localContent);
261
+ setIsDirty(false);
262
+ }
263
+ }, [filename, isDirty, localContent, onSave]);
264
+ const handleKeyDown = useCallback(
265
+ (e) => {
266
+ if ((e.metaKey || e.ctrlKey) && e.key === "s") {
267
+ e.preventDefault();
268
+ handleSave();
269
+ }
270
+ if (e.key === "Tab") {
271
+ e.preventDefault();
272
+ const ta = textareaRef.current;
273
+ if (!ta) return;
274
+ const start = ta.selectionStart;
275
+ const end = ta.selectionEnd;
276
+ const val = ta.value;
277
+ const newVal = val.substring(0, start) + " " + val.substring(end);
278
+ setLocalContent(newVal);
279
+ setIsDirty(newVal !== content);
280
+ requestAnimationFrame(() => {
281
+ ta.selectionStart = ta.selectionEnd = start + 2;
282
+ });
283
+ }
284
+ },
285
+ [handleSave, content]
286
+ );
287
+ const handleScroll = useCallback(() => {
288
+ if (textareaRef.current) {
289
+ if (lineCountRef.current) {
290
+ lineCountRef.current.scrollTop = textareaRef.current.scrollTop;
291
+ }
292
+ if (highlightRef.current) {
293
+ highlightRef.current.scrollTop = textareaRef.current.scrollTop;
294
+ highlightRef.current.scrollLeft = textareaRef.current.scrollLeft;
295
+ }
296
+ }
297
+ }, []);
298
+ if (!filename) {
299
+ return /* @__PURE__ */ jsx("div", { className: "h-full flex items-center justify-center text-zinc-600 text-sm", children: "Select a file to edit" });
300
+ }
301
+ const lineCount = localContent.split("\n").length;
302
+ const lineNumbers = useMemo(
303
+ () => Array.from({ length: lineCount }, (_, i) => /* @__PURE__ */ jsx(
304
+ "div",
305
+ {
306
+ className: "px-3 text-right text-zinc-600 text-[12px] leading-5",
307
+ style: { minWidth: "3rem" },
308
+ children: i + 1
309
+ },
310
+ i
311
+ )),
312
+ [lineCount]
313
+ );
314
+ return /* @__PURE__ */ jsxs("div", { className: "h-full flex flex-col", children: [
315
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-3 py-1.5 bg-zinc-900/50 border-b border-zinc-800", children: [
316
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
317
+ /* @__PURE__ */ jsx("span", { className: "text-[13px] font-mono text-zinc-300", children: filename }),
318
+ isDirty && /* @__PURE__ */ jsx(
319
+ "span",
320
+ {
321
+ className: "inline-block h-2 w-2 rounded-full bg-amber-400",
322
+ title: "Unsaved changes"
323
+ }
324
+ )
325
+ ] }),
326
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
327
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] text-zinc-600", children: getLanguageLabel(filename) }),
328
+ /* @__PURE__ */ jsx(
329
+ "button",
330
+ {
331
+ onClick: handleSave,
332
+ disabled: !isDirty,
333
+ className: `
334
+ text-[11px] px-2.5 py-1 rounded font-medium transition-colors
335
+ ${isDirty ? theme.saveButtonActive : "bg-zinc-800 text-zinc-600 border border-zinc-700 cursor-not-allowed"}
336
+ `,
337
+ children: "Save"
338
+ }
339
+ )
340
+ ] })
341
+ ] }),
342
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 flex overflow-hidden relative font-mono text-[13px] leading-5", children: [
343
+ /* @__PURE__ */ jsx(
344
+ "div",
345
+ {
346
+ ref: lineCountRef,
347
+ className: "flex-shrink-0 overflow-hidden bg-zinc-900/30 select-none",
348
+ children: lineNumbers
349
+ }
350
+ ),
351
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 relative min-w-0", children: [
352
+ /* @__PURE__ */ jsx(
353
+ "div",
354
+ {
355
+ ref: highlightRef,
356
+ className: "absolute inset-0 overflow-hidden p-2 pointer-events-none [&_pre]:!bg-transparent [&_pre]:!m-0 [&_pre]:!p-0 [&_pre]:!font-[inherit] [&_pre]:![font-size:inherit] [&_pre]:![line-height:inherit] [&_pre]:![letter-spacing:inherit] [&_pre]:![white-space:pre] [&_pre]:![word-wrap:normal] [&_code]:!font-[inherit] [&_code]:![font-size:inherit] [&_code]:![line-height:inherit] [&_.shiki]:!overflow-visible",
357
+ style: { tabSize: 2 }
358
+ }
359
+ ),
360
+ /* @__PURE__ */ jsx(
361
+ "textarea",
362
+ {
363
+ ref: textareaRef,
364
+ value: localContent,
365
+ onChange: handleChange,
366
+ onKeyDown: handleKeyDown,
367
+ onScroll: handleScroll,
368
+ wrap: "off",
369
+ spellCheck: false,
370
+ className: `
371
+ absolute inset-0 w-full h-full resize-none bg-transparent p-2
372
+ text-transparent outline-none overflow-auto whitespace-pre
373
+ ${theme.caret} ${theme.selection}
374
+ `,
375
+ style: { tabSize: 2, overflowWrap: "normal", wordBreak: "normal" }
376
+ }
377
+ )
378
+ ] })
379
+ ] }),
380
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-3 py-1 bg-zinc-900/30 border-t border-zinc-800 text-[11px] text-zinc-600", children: [
381
+ /* @__PURE__ */ jsxs("span", { children: [
382
+ lineCount,
383
+ " lines"
384
+ ] }),
385
+ /* @__PURE__ */ jsx("span", { children: "Ctrl+S / Cmd+S to save" })
386
+ ] })
387
+ ] });
388
+ }
389
+ function Terminal({
390
+ lines,
391
+ onCommand,
392
+ onClear,
393
+ accentColor = "emerald",
394
+ variant = "default",
395
+ title = "Terminal",
396
+ lineStyles: lineStyleOverrides
397
+ }) {
398
+ const [input, setInput] = useState("");
399
+ const [history, setHistory] = useState([]);
400
+ const [historyIdx, setHistoryIdx] = useState(-1);
401
+ const [isRunning, setIsRunning] = useState(false);
402
+ const scrollRef = useRef(null);
403
+ const inputRef = useRef(null);
404
+ const termTheme = getTerminalTheme(accentColor);
405
+ const styles = getLineStyles(accentColor, lineStyleOverrides);
406
+ const isCompact = variant === "compact";
407
+ const hasInput = !!onCommand;
408
+ useEffect(() => {
409
+ const el = scrollRef.current;
410
+ if (el) el.scrollTop = el.scrollHeight;
411
+ }, [lines]);
412
+ const executeCommand = useCallback(
413
+ async (cmd) => {
414
+ if (cmd === "clear") {
415
+ onClear();
416
+ return;
417
+ }
418
+ if (!onCommand) return;
419
+ setIsRunning(true);
420
+ await onCommand(cmd);
421
+ setIsRunning(false);
422
+ inputRef.current?.focus();
423
+ },
424
+ [onCommand, onClear]
425
+ );
426
+ const handleKeyDown = useCallback(
427
+ (e) => {
428
+ if (e.key === "Enter") {
429
+ e.preventDefault();
430
+ const cmd = e.currentTarget.value.trim();
431
+ if (!cmd || isRunning) return;
432
+ setInput("");
433
+ setHistory((prev) => [...prev, cmd]);
434
+ setHistoryIdx(-1);
435
+ executeCommand(cmd);
436
+ } else if (e.key === "ArrowUp") {
437
+ e.preventDefault();
438
+ if (history.length === 0) return;
439
+ const newIdx = historyIdx === -1 ? history.length - 1 : Math.max(0, historyIdx - 1);
440
+ setHistoryIdx(newIdx);
441
+ setInput(history[newIdx]);
442
+ } else if (e.key === "ArrowDown") {
443
+ e.preventDefault();
444
+ if (historyIdx === -1) return;
445
+ const newIdx = historyIdx + 1;
446
+ if (newIdx >= history.length) {
447
+ setHistoryIdx(-1);
448
+ setInput("");
449
+ } else {
450
+ setHistoryIdx(newIdx);
451
+ setInput(history[newIdx]);
452
+ }
453
+ }
454
+ },
455
+ [history, historyIdx, isRunning, executeCommand]
456
+ );
457
+ const headerPy = isCompact ? "py-1" : "py-1.5";
458
+ const dotSize = isCompact ? "h-2 w-2" : "h-2.5 w-2.5";
459
+ const dotOpacity = isCompact ? "bg-red-500/60" : "bg-red-500/70";
460
+ const dotYellow = isCompact ? "bg-yellow-500/60" : "bg-yellow-500/70";
461
+ const dotGreen = isCompact ? "bg-green-500/60" : "bg-green-500/70";
462
+ const titleSize = isCompact ? "text-[10px]" : "text-[11px]";
463
+ const titleMl = isCompact ? "ml-1" : "";
464
+ const clearSize = isCompact ? "text-[10px]" : "text-[11px]";
465
+ const outputClass = isCompact ? "flex-1 overflow-y-auto px-3 py-1.5 font-mono text-[11px] leading-4" : "flex-1 overflow-y-auto p-3 font-mono text-[13px] leading-5";
466
+ const inputPy = isCompact ? "py-1.5" : "py-2";
467
+ const inputGap = isCompact ? "gap-1.5" : "gap-2";
468
+ const promptSize = isCompact ? "text-[11px]" : "text-sm";
469
+ const inputFontSize = isCompact ? "text-[11px]" : "text-[13px]";
470
+ const spinnerSize = isCompact ? "h-2.5 w-2.5" : "h-3 w-3";
471
+ return /* @__PURE__ */ jsxs("div", { className: "h-full flex flex-col bg-zinc-950", children: [
472
+ /* @__PURE__ */ jsxs(
473
+ "div",
474
+ {
475
+ className: `flex items-center justify-between px-3 ${headerPy} bg-zinc-900/50 border-b border-zinc-800`,
476
+ children: [
477
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
478
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
479
+ /* @__PURE__ */ jsx("div", { className: `${dotSize} rounded-full ${dotOpacity}` }),
480
+ /* @__PURE__ */ jsx("div", { className: `${dotSize} rounded-full ${dotYellow}` }),
481
+ /* @__PURE__ */ jsx("div", { className: `${dotSize} rounded-full ${dotGreen}` })
482
+ ] }),
483
+ /* @__PURE__ */ jsx(
484
+ "span",
485
+ {
486
+ className: `${titleSize} font-semibold uppercase tracking-wider text-zinc-500 ${titleMl}`,
487
+ children: title
488
+ }
489
+ )
490
+ ] }),
491
+ /* @__PURE__ */ jsx(
492
+ "button",
493
+ {
494
+ onClick: onClear,
495
+ className: `${clearSize} text-zinc-600 hover:text-zinc-400 transition-colors`,
496
+ children: "Clear"
497
+ }
498
+ )
499
+ ]
500
+ }
501
+ ),
502
+ /* @__PURE__ */ jsx(
503
+ "div",
504
+ {
505
+ ref: scrollRef,
506
+ "data-testid": "terminal-output",
507
+ className: outputClass,
508
+ children: lines.map((line) => /* @__PURE__ */ jsx("div", { className: styles[line.type], children: line.text.split("\n").map((segment, i) => /* @__PURE__ */ jsx("div", { children: segment || " " }, i)) }, line.id))
509
+ }
510
+ ),
511
+ hasInput && /* @__PURE__ */ jsx("div", { className: "flex-shrink-0 border-t border-zinc-800", children: /* @__PURE__ */ jsxs("div", { className: `flex items-center px-3 ${inputPy} ${inputGap}`, children: [
512
+ /* @__PURE__ */ jsx(
513
+ "span",
514
+ {
515
+ className: `${termTheme.promptColor} font-mono ${promptSize} font-bold select-none`,
516
+ children: "$"
517
+ }
518
+ ),
519
+ /* @__PURE__ */ jsx(
520
+ "input",
521
+ {
522
+ ref: inputRef,
523
+ type: "text",
524
+ value: input,
525
+ onChange: (e) => setInput(e.target.value),
526
+ onKeyDown: handleKeyDown,
527
+ disabled: isRunning,
528
+ placeholder: isRunning ? "Running..." : "Type a command...",
529
+ className: `
530
+ flex-1 bg-transparent text-zinc-200 ${inputFontSize} font-mono
531
+ outline-none placeholder:text-zinc-700
532
+ disabled:opacity-50
533
+ ${termTheme.inputCaret}
534
+ `
535
+ }
536
+ ),
537
+ isRunning && /* @__PURE__ */ jsx(
538
+ "div",
539
+ {
540
+ className: `${spinnerSize} animate-spin rounded-full border border-zinc-600 ${termTheme.spinnerBorder}`
541
+ }
542
+ )
543
+ ] }) })
544
+ ] });
545
+ }
546
+ function BrowserWindow({
547
+ htmlSrc,
548
+ url,
549
+ canGoBack,
550
+ canGoForward,
551
+ onBack,
552
+ onForward,
553
+ onRefresh,
554
+ onNavigate,
555
+ port = 3e3,
556
+ title = "App Preview",
557
+ previewUrl,
558
+ onInspectElement
559
+ }) {
560
+ const iframeRef = useRef(null);
561
+ const [docTitle, setDocTitle] = useState(null);
562
+ const [isFocused, setIsFocused] = useState(false);
563
+ const [inspecting, setInspecting] = useState(false);
564
+ const inspectCleanupRef = useRef(null);
565
+ const displayUrl = previewUrl ? `${previewUrl}${url === "/" ? "" : url}` : `http://localhost:${port}${url === "/" ? "" : url}`;
566
+ const lastSrcdocRef = useRef("");
567
+ useEffect(() => {
568
+ const iframe = iframeRef.current;
569
+ if (!iframe) return;
570
+ if (previewUrl) {
571
+ iframe.src = `${previewUrl}${url === "/" ? "/" : url}`;
572
+ } else if (htmlSrc !== lastSrcdocRef.current) {
573
+ lastSrcdocRef.current = htmlSrc;
574
+ iframe.srcdoc = htmlSrc;
575
+ }
576
+ }, [htmlSrc, previewUrl]);
577
+ useEffect(() => {
578
+ const iframe = iframeRef.current;
579
+ if (!iframe || previewUrl) return;
580
+ try {
581
+ iframe.contentWindow?.postMessage({ type: "navigate", path: url }, "*");
582
+ } catch {
583
+ }
584
+ }, [url, previewUrl]);
585
+ useEffect(() => {
586
+ const iframe = iframeRef.current;
587
+ if (!iframe) return;
588
+ let observer;
589
+ const observe = () => {
590
+ try {
591
+ const doc = iframe.contentDocument;
592
+ if (!doc) return;
593
+ const readTitle = () => {
594
+ const t = doc.title;
595
+ if (t) setDocTitle(t);
596
+ };
597
+ readTitle();
598
+ const head = doc.head ?? doc.documentElement;
599
+ if (!head) return;
600
+ observer = new MutationObserver(readTitle);
601
+ observer.observe(head, {
602
+ childList: true,
603
+ subtree: true,
604
+ characterData: true
605
+ });
606
+ } catch {
607
+ }
608
+ };
609
+ iframe.addEventListener("load", observe);
610
+ return () => {
611
+ iframe.removeEventListener("load", observe);
612
+ observer?.disconnect();
613
+ };
614
+ }, [htmlSrc, previewUrl]);
615
+ useEffect(() => {
616
+ if (inspecting) {
617
+ inspectCleanupRef.current?.();
618
+ inspectCleanupRef.current = null;
619
+ const timer = setTimeout(() => {
620
+ if (inspecting) injectInspectMode();
621
+ }, 100);
622
+ return () => clearTimeout(timer);
623
+ }
624
+ }, [htmlSrc]);
625
+ const injectInspectMode = useCallback(() => {
626
+ const iframe = iframeRef.current;
627
+ if (!iframe) return;
628
+ try {
629
+ const doc = iframe.contentDocument;
630
+ if (!doc) return;
631
+ let currentHighlight = null;
632
+ const OUTLINE = "2px solid #3b82f6";
633
+ const OUTLINE_OFFSET = "-2px";
634
+ const onMouseOver = (e) => {
635
+ const target = e.target;
636
+ if (target === doc.body || target === doc.documentElement) return;
637
+ if (currentHighlight) {
638
+ currentHighlight.style.outline = "";
639
+ currentHighlight.style.outlineOffset = "";
640
+ }
641
+ target.style.outline = OUTLINE;
642
+ target.style.outlineOffset = OUTLINE_OFFSET;
643
+ currentHighlight = target;
644
+ };
645
+ const onMouseOut = (e) => {
646
+ const target = e.target;
647
+ target.style.outline = "";
648
+ target.style.outlineOffset = "";
649
+ if (currentHighlight === target) currentHighlight = null;
650
+ };
651
+ const onClick = (e) => {
652
+ e.preventDefault();
653
+ e.stopPropagation();
654
+ const target = e.target;
655
+ target.style.outline = "";
656
+ target.style.outlineOffset = "";
657
+ const text = (target.textContent || "").trim().slice(0, 200);
658
+ const html = target.outerHTML.slice(0, 500);
659
+ onInspectElement?.({
660
+ tagName: target.tagName.toLowerCase(),
661
+ className: target.className || "",
662
+ textContent: text,
663
+ outerHTML: html
664
+ });
665
+ cleanup();
666
+ setInspecting(false);
667
+ };
668
+ doc.addEventListener("mouseover", onMouseOver, true);
669
+ doc.addEventListener("mouseout", onMouseOut, true);
670
+ doc.addEventListener("click", onClick, true);
671
+ doc.body.style.cursor = "crosshair";
672
+ const cleanup = () => {
673
+ doc.removeEventListener("mouseover", onMouseOver, true);
674
+ doc.removeEventListener("mouseout", onMouseOut, true);
675
+ doc.removeEventListener("click", onClick, true);
676
+ doc.body.style.cursor = "";
677
+ if (currentHighlight) {
678
+ currentHighlight.style.outline = "";
679
+ currentHighlight.style.outlineOffset = "";
680
+ currentHighlight = null;
681
+ }
682
+ };
683
+ inspectCleanupRef.current = cleanup;
684
+ } catch {
685
+ }
686
+ }, [onInspectElement]);
687
+ const toggleInspect = useCallback(() => {
688
+ if (inspecting) {
689
+ inspectCleanupRef.current?.();
690
+ inspectCleanupRef.current = null;
691
+ setInspecting(false);
692
+ } else {
693
+ setInspecting(true);
694
+ requestAnimationFrame(() => injectInspectMode());
695
+ }
696
+ }, [inspecting, injectInspectMode]);
697
+ const handleAddressKeyDown = useCallback(
698
+ (e) => {
699
+ if (e.key === "Enter") {
700
+ const val = e.currentTarget.value.trim();
701
+ try {
702
+ const parsed = new URL(
703
+ val.startsWith("http") ? val : `http://localhost:${port}${val}`
704
+ );
705
+ onNavigate(parsed.pathname || "/");
706
+ } catch {
707
+ onNavigate(val.startsWith("/") ? val : `/${val}`);
708
+ }
709
+ }
710
+ },
711
+ [onNavigate, port]
712
+ );
713
+ const tabLabel = docTitle || title;
714
+ return /* @__PURE__ */ jsxs("div", { className: "h-full flex flex-col rounded-xl overflow-hidden border border-zinc-800 bg-zinc-950 shadow-[0_0_0_1px_rgba(255,255,255,0.03),0_8px_40px_-12px_rgba(0,0,0,0.6)]", children: [
715
+ /* @__PURE__ */ jsxs("div", { className: "flex-shrink-0 flex items-center gap-2 h-10 px-3 bg-zinc-900/80 border-b border-zinc-800/80 select-none", children: [
716
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-[6px] mr-1", children: [
717
+ /* @__PURE__ */ jsx("span", { className: "block h-[10px] w-[10px] rounded-full bg-[#ff5f57] ring-1 ring-black/10" }),
718
+ /* @__PURE__ */ jsx("span", { className: "block h-[10px] w-[10px] rounded-full bg-[#febc2e] ring-1 ring-black/10" }),
719
+ /* @__PURE__ */ jsx("span", { className: "block h-[10px] w-[10px] rounded-full bg-[#28c840] ring-1 ring-black/10" })
720
+ ] }),
721
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center", children: [
722
+ /* @__PURE__ */ jsx(
723
+ "button",
724
+ {
725
+ onClick: onBack,
726
+ disabled: !canGoBack,
727
+ className: "p-1 rounded-md text-zinc-500 hover:text-zinc-300 hover:bg-zinc-800 disabled:text-zinc-700 disabled:hover:bg-transparent transition-colors",
728
+ title: "Back",
729
+ children: /* @__PURE__ */ jsx(
730
+ "svg",
731
+ {
732
+ className: "w-3.5 h-3.5",
733
+ viewBox: "0 0 16 16",
734
+ fill: "currentColor",
735
+ children: /* @__PURE__ */ jsx("path", { d: "M10.354 3.354a.5.5 0 0 0-.708-.708l-5 5a.5.5 0 0 0 0 .708l5 5a.5.5 0 0 0 .708-.708L5.707 8l4.647-4.646z" })
736
+ }
737
+ )
738
+ }
739
+ ),
740
+ /* @__PURE__ */ jsx(
741
+ "button",
742
+ {
743
+ onClick: onForward,
744
+ disabled: !canGoForward,
745
+ className: "p-1 rounded-md text-zinc-500 hover:text-zinc-300 hover:bg-zinc-800 disabled:text-zinc-700 disabled:hover:bg-transparent transition-colors",
746
+ title: "Forward",
747
+ children: /* @__PURE__ */ jsx(
748
+ "svg",
749
+ {
750
+ className: "w-3.5 h-3.5",
751
+ viewBox: "0 0 16 16",
752
+ fill: "currentColor",
753
+ children: /* @__PURE__ */ jsx("path", { d: "M5.646 3.354a.5.5 0 0 1 .708-.708l5 5a.5.5 0 0 1 0 .708l-5 5a.5.5 0 0 1-.708-.708L10.293 8 5.646 3.354z" })
754
+ }
755
+ )
756
+ }
757
+ ),
758
+ /* @__PURE__ */ jsx(
759
+ "button",
760
+ {
761
+ onClick: onRefresh,
762
+ className: "p-1 rounded-md text-zinc-500 hover:text-zinc-300 hover:bg-zinc-800 transition-colors",
763
+ title: "Refresh",
764
+ children: /* @__PURE__ */ jsxs(
765
+ "svg",
766
+ {
767
+ className: "w-3.5 h-3.5",
768
+ viewBox: "0 0 16 16",
769
+ fill: "none",
770
+ stroke: "currentColor",
771
+ strokeWidth: 1.5,
772
+ strokeLinecap: "round",
773
+ strokeLinejoin: "round",
774
+ children: [
775
+ /* @__PURE__ */ jsx("path", { d: "M2.5 8a5.5 5.5 0 0 1 9.22-4.05M13.5 8a5.5 5.5 0 0 1-9.22 4.05" }),
776
+ /* @__PURE__ */ jsx("path", { d: "M13.5 2.5v3h-3M2.5 13.5v-3h3" })
777
+ ]
778
+ }
779
+ )
780
+ }
781
+ ),
782
+ onInspectElement && /* @__PURE__ */ jsx(
783
+ "button",
784
+ {
785
+ onClick: toggleInspect,
786
+ className: `p-1 rounded-md transition-colors ${inspecting ? "text-blue-400 bg-blue-500/15" : "text-zinc-500 hover:text-zinc-300 hover:bg-zinc-800"}`,
787
+ title: inspecting ? "Cancel inspect" : "Inspect element",
788
+ children: /* @__PURE__ */ jsxs(
789
+ "svg",
790
+ {
791
+ className: "w-3.5 h-3.5",
792
+ viewBox: "0 0 24 24",
793
+ fill: "none",
794
+ stroke: "currentColor",
795
+ strokeWidth: 2,
796
+ strokeLinecap: "round",
797
+ strokeLinejoin: "round",
798
+ children: [
799
+ /* @__PURE__ */ jsx("path", { d: "M3 12l3 0" }),
800
+ /* @__PURE__ */ jsx("path", { d: "M12 3l0 3" }),
801
+ /* @__PURE__ */ jsx("path", { d: "M7.8 7.8l-2.2 -2.2" }),
802
+ /* @__PURE__ */ jsx("path", { d: "M16.2 7.8l2.2 -2.2" }),
803
+ /* @__PURE__ */ jsx("path", { d: "M7.8 16.2l-2.2 2.2" }),
804
+ /* @__PURE__ */ jsx("path", { d: "M12 12l9 3l-4 2l-2 4l-3 -9" })
805
+ ]
806
+ }
807
+ )
808
+ }
809
+ )
810
+ ] }),
811
+ /* @__PURE__ */ jsxs(
812
+ "div",
813
+ {
814
+ className: `flex-1 flex items-center gap-2 h-[26px] rounded-md px-2.5 transition-colors ${isFocused ? "bg-zinc-950 ring-1 ring-zinc-600" : "bg-zinc-800/60 hover:bg-zinc-800"}`,
815
+ children: [
816
+ /* @__PURE__ */ jsx(
817
+ "svg",
818
+ {
819
+ className: "w-3 h-3 flex-shrink-0 text-zinc-500",
820
+ viewBox: "0 0 16 16",
821
+ fill: "currentColor",
822
+ children: /* @__PURE__ */ jsx(
823
+ "path",
824
+ {
825
+ fillRule: "evenodd",
826
+ d: "M8 1a4.5 4.5 0 0 0-4.5 4.5V7H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1h-.5V5.5A4.5 4.5 0 0 0 8 1zm2.5 6V5.5a2.5 2.5 0 0 0-5 0V7h5z"
827
+ }
828
+ )
829
+ }
830
+ ),
831
+ /* @__PURE__ */ jsx(
832
+ "input",
833
+ {
834
+ type: "text",
835
+ defaultValue: displayUrl,
836
+ onKeyDown: handleAddressKeyDown,
837
+ onFocus: () => setIsFocused(true),
838
+ onBlur: () => setIsFocused(false),
839
+ className: "flex-1 bg-transparent text-[11px] text-zinc-400 outline-none font-mono leading-none placeholder:text-zinc-600",
840
+ spellCheck: false
841
+ },
842
+ displayUrl
843
+ ),
844
+ tabLabel && /* @__PURE__ */ jsx("span", { className: "hidden sm:block text-[10px] text-zinc-600 truncate max-w-[120px] leading-none", children: tabLabel })
845
+ ]
846
+ }
847
+ )
848
+ ] }),
849
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 bg-white relative", children: [
850
+ /* @__PURE__ */ jsx(
851
+ "iframe",
852
+ {
853
+ ref: iframeRef,
854
+ title: docTitle || title,
855
+ sandbox: "allow-scripts allow-forms allow-popups allow-same-origin",
856
+ className: "absolute inset-0 w-full h-full border-0"
857
+ }
858
+ ),
859
+ inspecting && /* @__PURE__ */ jsx("div", { className: "absolute top-2 left-1/2 -translate-x-1/2 z-10 px-3 py-1 rounded-full bg-blue-500/90 text-white text-[10px] font-mono shadow-lg pointer-events-none", children: "Click an element to inspect" })
860
+ ] })
861
+ ] });
862
+ }
863
+ const FILE_COLOR_MAP = {
864
+ js: "text-yellow-400",
865
+ mjs: "text-yellow-400",
866
+ cjs: "text-yellow-400",
867
+ jsx: "text-cyan-400",
868
+ ts: "text-blue-400",
869
+ tsx: "text-cyan-400",
870
+ json: "text-amber-300",
871
+ html: "text-orange-400",
872
+ css: "text-pink-400",
873
+ md: "text-blue-400",
874
+ vue: "text-emerald-400",
875
+ svelte: "text-orange-400",
876
+ astro: "text-orange-400",
877
+ yaml: "text-blue-400",
878
+ yml: "text-blue-400",
879
+ toml: "text-blue-400",
880
+ sh: "text-green-400",
881
+ svg: "text-emerald-400"
882
+ };
883
+ function FileIcon({
884
+ isDir,
885
+ name,
886
+ compact
887
+ }) {
888
+ const iconSize = compact ? "text-[11px] w-3.5" : "text-xs w-4";
889
+ if (isDir) {
890
+ return /* @__PURE__ */ jsx(
891
+ "span",
892
+ {
893
+ className: `text-amber-400 ${iconSize} inline-block text-center mr-1.5`,
894
+ children: "📁"
895
+ }
896
+ );
897
+ }
898
+ const ext = name.split(".").pop()?.toLowerCase();
899
+ const color = FILE_COLOR_MAP[ext || ""] || "text-zinc-400";
900
+ return /* @__PURE__ */ jsx("span", { className: `${color} ${iconSize} inline-block text-center mr-1.5`, children: "📄" });
901
+ }
902
+ function TreeNode({
903
+ entry,
904
+ depth,
905
+ selectedFile,
906
+ onSelect,
907
+ theme,
908
+ compact
909
+ }) {
910
+ const [expanded, setExpanded] = useState(true);
911
+ const isSelected = entry.path === selectedFile;
912
+ const py = compact ? "py-[3px]" : "py-0.5";
913
+ const px = compact ? "px-1.5" : "px-2";
914
+ const fontSize = compact ? "text-[12px]" : "text-[13px]";
915
+ const arrowSize = compact ? "text-[9px] w-2.5" : "text-[10px] w-3";
916
+ const indent = compact ? depth * 10 + 6 : depth * 12 + 8;
917
+ return /* @__PURE__ */ jsxs("div", { children: [
918
+ /* @__PURE__ */ jsxs(
919
+ "button",
920
+ {
921
+ onClick: () => entry.isDir ? setExpanded(!expanded) : onSelect(entry.path),
922
+ className: `
923
+ w-full text-left flex items-center ${py} ${px} rounded ${fontSize} font-mono
924
+ transition-colors duration-75
925
+ ${isSelected ? theme.selected : "text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200"}
926
+ `,
927
+ style: { paddingLeft: `${indent}px` },
928
+ children: [
929
+ entry.isDir && /* @__PURE__ */ jsx("span", { className: `${arrowSize} mr-1 text-zinc-500 inline-block`, children: expanded ? "▾" : "▸" }),
930
+ !entry.isDir && /* @__PURE__ */ jsx("span", { className: `${arrowSize} inline-block` }),
931
+ /* @__PURE__ */ jsx(FileIcon, { isDir: entry.isDir, name: entry.name, compact }),
932
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: entry.name })
933
+ ]
934
+ }
935
+ ),
936
+ entry.isDir && expanded && entry.children?.map((child) => /* @__PURE__ */ jsx(
937
+ TreeNode,
938
+ {
939
+ entry: child,
940
+ depth: depth + 1,
941
+ selectedFile,
942
+ onSelect,
943
+ theme,
944
+ compact
945
+ },
946
+ child.path
947
+ ))
948
+ ] });
949
+ }
950
+ function FileExplorer({
951
+ files,
952
+ selectedFile,
953
+ onSelect,
954
+ accentColor = "emerald",
955
+ variant = "default"
956
+ }) {
957
+ const theme = getFileExplorerTheme(accentColor);
958
+ const compact = variant === "compact";
959
+ if (files.length === 0) {
960
+ return /* @__PURE__ */ jsx(
961
+ "div",
962
+ {
963
+ className: `p-2 ${compact ? "text-[11px]" : "text-xs"} text-zinc-600 italic`,
964
+ children: "No files"
965
+ }
966
+ );
967
+ }
968
+ return /* @__PURE__ */ jsx("div", { className: compact ? "py-0.5" : "py-1", children: files.map((entry) => /* @__PURE__ */ jsx(
969
+ TreeNode,
970
+ {
971
+ entry,
972
+ depth: 0,
973
+ selectedFile,
974
+ onSelect,
975
+ theme,
976
+ compact
977
+ },
978
+ entry.path
979
+ )) });
980
+ }
981
+ const ACCENT_LABELS = {
982
+ emerald: "text-emerald-500/70",
983
+ violet: "text-violet-500/70",
984
+ orange: "text-orange-500/70",
985
+ blue: "text-blue-500/70",
986
+ pink: "text-pink-500/70",
987
+ green: "text-green-500/70",
988
+ amber: "text-amber-500/70"
989
+ };
990
+ const ACCENT_CHEVRONS = {
991
+ emerald: "text-emerald-400",
992
+ violet: "text-violet-400",
993
+ orange: "text-orange-400",
994
+ blue: "text-blue-400",
995
+ pink: "text-pink-400",
996
+ green: "text-green-400",
997
+ amber: "text-amber-400"
998
+ };
999
+ const ACCENT_HOVER_BG = {
1000
+ emerald: "hover:bg-emerald-500/5",
1001
+ violet: "hover:bg-violet-500/5",
1002
+ orange: "hover:bg-orange-500/5",
1003
+ blue: "hover:bg-blue-500/5",
1004
+ pink: "hover:bg-pink-500/5",
1005
+ green: "hover:bg-green-500/5",
1006
+ amber: "hover:bg-amber-500/5"
1007
+ };
1008
+ function PanelToggle({
1009
+ collapsed,
1010
+ onClick,
1011
+ label,
1012
+ side = "left",
1013
+ accentColor = "emerald"
1014
+ }) {
1015
+ const chevronRight = /* @__PURE__ */ jsxs(Fragment, { children: [
1016
+ /* @__PURE__ */ jsx("path", { stroke: "none", d: "M0 0h24v24H0z", fill: "none" }),
1017
+ /* @__PURE__ */ jsx("path", { d: "M9 6l6 6l-6 6" })
1018
+ ] });
1019
+ const chevronLeft = /* @__PURE__ */ jsxs(Fragment, { children: [
1020
+ /* @__PURE__ */ jsx("path", { stroke: "none", d: "M0 0h24v24H0z" }),
1021
+ /* @__PURE__ */ jsx("path", { d: "m15 6-6 6 6 6" })
1022
+ ] });
1023
+ if (collapsed) {
1024
+ return /* @__PURE__ */ jsxs(
1025
+ "button",
1026
+ {
1027
+ onClick,
1028
+ className: "flex-shrink-0 w-7 flex flex-col items-center gap-2 pt-3 pb-3\n border-r border-zinc-800 bg-zinc-900/40\n hover:bg-zinc-800/60 transition-colors cursor-pointer",
1029
+ title: `Show ${label}`,
1030
+ children: [
1031
+ /* @__PURE__ */ jsx(
1032
+ "svg",
1033
+ {
1034
+ className: `w-3 h-3 ${ACCENT_CHEVRONS[accentColor]}`,
1035
+ viewBox: "0 0 24 24",
1036
+ stroke: "currentColor",
1037
+ fill: "none",
1038
+ children: side === "left" ? chevronRight : chevronLeft
1039
+ }
1040
+ ),
1041
+ /* @__PURE__ */ jsx(
1042
+ "span",
1043
+ {
1044
+ className: `text-[10px] font-mono tracking-wider uppercase select-none ${ACCENT_LABELS[accentColor]}`,
1045
+ style: { writingMode: "vertical-rl", transform: "rotate(180deg)" },
1046
+ children: label
1047
+ }
1048
+ )
1049
+ ]
1050
+ }
1051
+ );
1052
+ }
1053
+ return /* @__PURE__ */ jsx(
1054
+ "button",
1055
+ {
1056
+ onClick,
1057
+ className: `flex-shrink-0 w-[9px] flex items-center justify-center
1058
+ border-r border-zinc-800 bg-transparent
1059
+ ${ACCENT_HOVER_BG[accentColor]} transition-colors cursor-pointer group`,
1060
+ title: `Hide ${label}`,
1061
+ children: /* @__PURE__ */ jsx(
1062
+ "svg",
1063
+ {
1064
+ className: `w-2.5 h-2.5 text-zinc-600 opacity-0 group-hover:opacity-100
1065
+ transition-opacity ${ACCENT_CHEVRONS[accentColor]}`,
1066
+ viewBox: "0 0 16 16",
1067
+ fill: "currentColor",
1068
+ children: side === "left" ? chevronLeft : chevronRight
1069
+ }
1070
+ )
1071
+ }
1072
+ );
1073
+ }
1074
+ const ACTIVE_BG = {
1075
+ emerald: "bg-emerald-500/15",
1076
+ violet: "bg-violet-500/15",
1077
+ orange: "bg-orange-500/15",
1078
+ blue: "bg-blue-500/15",
1079
+ pink: "bg-pink-500/15",
1080
+ green: "bg-green-500/15",
1081
+ amber: "bg-amber-500/15"
1082
+ };
1083
+ const ACTIVE_TEXT = {
1084
+ emerald: "text-emerald-300",
1085
+ violet: "text-violet-300",
1086
+ orange: "text-orange-300",
1087
+ blue: "text-blue-300",
1088
+ pink: "text-pink-300",
1089
+ green: "text-green-300",
1090
+ amber: "text-amber-300"
1091
+ };
1092
+ function MobileTabBar({
1093
+ tabs,
1094
+ activeTab,
1095
+ onTabChange,
1096
+ accentColor = "emerald"
1097
+ }) {
1098
+ const containerRef = useRef(null);
1099
+ const [indicator, setIndicator] = useState({ left: 0, width: 0 });
1100
+ useEffect(() => {
1101
+ if (!containerRef.current) return;
1102
+ const activeIndex = tabs.findIndex((t) => t.id === activeTab);
1103
+ if (activeIndex < 0) return;
1104
+ const buttons = containerRef.current.querySelectorAll(
1105
+ "[data-tab-id]"
1106
+ );
1107
+ const btn = buttons[activeIndex];
1108
+ if (!btn) return;
1109
+ setIndicator({
1110
+ left: btn.offsetLeft,
1111
+ width: btn.offsetWidth
1112
+ });
1113
+ }, [activeTab, tabs]);
1114
+ return /* @__PURE__ */ jsx("div", { className: "flex-shrink-0 px-3 py-1.5 border-b border-zinc-800 bg-zinc-950", children: /* @__PURE__ */ jsxs(
1115
+ "div",
1116
+ {
1117
+ ref: containerRef,
1118
+ className: "relative flex rounded-lg bg-zinc-900 border border-zinc-800 p-0.5",
1119
+ children: [
1120
+ /* @__PURE__ */ jsx(
1121
+ "div",
1122
+ {
1123
+ className: `absolute top-0.5 bottom-0.5 rounded-md ${ACTIVE_BG[accentColor]} transition-all duration-200 ease-out`,
1124
+ style: { left: indicator.left, width: indicator.width }
1125
+ }
1126
+ ),
1127
+ tabs.map((tab) => {
1128
+ const isActive = tab.id === activeTab;
1129
+ return /* @__PURE__ */ jsxs(
1130
+ "button",
1131
+ {
1132
+ "data-tab-id": tab.id,
1133
+ onClick: () => onTabChange(tab.id),
1134
+ className: `relative z-10 flex-1 flex items-center justify-center gap-1.5
1135
+ rounded-md py-1.5 px-3 font-mono text-[11px] tracking-wide
1136
+ transition-colors duration-150
1137
+ ${isActive ? ACTIVE_TEXT[accentColor] : "text-zinc-500 hover:text-zinc-400"}`,
1138
+ children: [
1139
+ tab.icon,
1140
+ tab.label
1141
+ ]
1142
+ },
1143
+ tab.id
1144
+ );
1145
+ })
1146
+ ]
1147
+ }
1148
+ ) });
1149
+ }
1150
+ function ChatIcon({ className = "w-3.5 h-3.5" }) {
1151
+ return /* @__PURE__ */ jsx("svg", { className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" }) });
1152
+ }
1153
+ function PreviewIcon({ className = "w-3.5 h-3.5" }) {
1154
+ return /* @__PURE__ */ jsxs("svg", { className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
1155
+ /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" }),
1156
+ /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" })
1157
+ ] });
1158
+ }
1159
+ function CodeIcon({ className = "w-3.5 h-3.5" }) {
1160
+ return /* @__PURE__ */ jsx("svg", { className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" }) });
1161
+ }
1162
+ function TerminalIcon({ className = "w-3.5 h-3.5" }) {
1163
+ return /* @__PURE__ */ jsx("svg", { className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" }) });
1164
+ }
1165
+ function FilesIcon({ className = "w-3.5 h-3.5" }) {
1166
+ return /* @__PURE__ */ jsx("svg", { className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" }) });
1167
+ }
1168
+ function useMediaQuery(query) {
1169
+ const [matches, setMatches] = useState(() => {
1170
+ if (typeof window === "undefined") return false;
1171
+ return window.matchMedia(query).matches;
1172
+ });
1173
+ useEffect(() => {
1174
+ const mql = window.matchMedia(query);
1175
+ const handler = (e) => setMatches(e.matches);
1176
+ mql.addEventListener("change", handler);
1177
+ return () => mql.removeEventListener("change", handler);
1178
+ }, [query]);
1179
+ return matches;
1180
+ }
1181
+ const CHAT_THEMES = {
1182
+ emerald: {
1183
+ userBubble: "bg-emerald-500/15 border border-emerald-500/20",
1184
+ assistantBubble: "bg-zinc-800/50 border border-zinc-700/50",
1185
+ sendButton: "bg-emerald-500 hover:bg-emerald-600 text-white",
1186
+ sendButtonDisabled: "bg-zinc-700 text-zinc-500",
1187
+ streamingDot: "bg-emerald-400",
1188
+ inputCaret: "caret-emerald-400",
1189
+ modelBadge: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20"
1190
+ },
1191
+ violet: {
1192
+ userBubble: "bg-violet-500/15 border border-violet-500/20",
1193
+ assistantBubble: "bg-zinc-800/50 border border-zinc-700/50",
1194
+ sendButton: "bg-violet-500 hover:bg-violet-600 text-white",
1195
+ sendButtonDisabled: "bg-zinc-700 text-zinc-500",
1196
+ streamingDot: "bg-violet-400",
1197
+ inputCaret: "caret-violet-400",
1198
+ modelBadge: "bg-violet-500/10 text-violet-400 border-violet-500/20"
1199
+ },
1200
+ orange: {
1201
+ userBubble: "bg-orange-500/15 border border-orange-500/20",
1202
+ assistantBubble: "bg-zinc-800/50 border border-zinc-700/50",
1203
+ sendButton: "bg-orange-500 hover:bg-orange-600 text-white",
1204
+ sendButtonDisabled: "bg-zinc-700 text-zinc-500",
1205
+ streamingDot: "bg-orange-400",
1206
+ inputCaret: "caret-orange-400",
1207
+ modelBadge: "bg-orange-500/10 text-orange-400 border-orange-500/20"
1208
+ },
1209
+ blue: {
1210
+ userBubble: "bg-blue-500/15 border border-blue-500/20",
1211
+ assistantBubble: "bg-zinc-800/50 border border-zinc-700/50",
1212
+ sendButton: "bg-blue-500 hover:bg-blue-600 text-white",
1213
+ sendButtonDisabled: "bg-zinc-700 text-zinc-500",
1214
+ streamingDot: "bg-blue-400",
1215
+ inputCaret: "caret-blue-400",
1216
+ modelBadge: "bg-blue-500/10 text-blue-400 border-blue-500/20"
1217
+ },
1218
+ pink: {
1219
+ userBubble: "bg-pink-500/15 border border-pink-500/20",
1220
+ assistantBubble: "bg-zinc-800/50 border border-zinc-700/50",
1221
+ sendButton: "bg-pink-500 hover:bg-pink-600 text-white",
1222
+ sendButtonDisabled: "bg-zinc-700 text-zinc-500",
1223
+ streamingDot: "bg-pink-400",
1224
+ inputCaret: "caret-pink-400",
1225
+ modelBadge: "bg-pink-500/10 text-pink-400 border-pink-500/20"
1226
+ },
1227
+ green: {
1228
+ userBubble: "bg-green-500/15 border border-green-500/20",
1229
+ assistantBubble: "bg-zinc-800/50 border border-zinc-700/50",
1230
+ sendButton: "bg-green-500 hover:bg-green-600 text-white",
1231
+ sendButtonDisabled: "bg-zinc-700 text-zinc-500",
1232
+ streamingDot: "bg-green-400",
1233
+ inputCaret: "caret-green-400",
1234
+ modelBadge: "bg-green-500/10 text-green-400 border-green-500/20"
1235
+ },
1236
+ amber: {
1237
+ userBubble: "bg-amber-500/15 border border-amber-500/20",
1238
+ assistantBubble: "bg-zinc-800/50 border border-zinc-700/50",
1239
+ sendButton: "bg-amber-500 hover:bg-amber-600 text-white",
1240
+ sendButtonDisabled: "bg-zinc-700 text-zinc-500",
1241
+ streamingDot: "bg-amber-400",
1242
+ inputCaret: "caret-amber-400",
1243
+ modelBadge: "bg-amber-500/10 text-amber-400 border-amber-500/20"
1244
+ }
1245
+ };
1246
+ function getChatTheme(color) {
1247
+ return CHAT_THEMES[color];
1248
+ }
1249
+ function CodeBlock({ lang, content }) {
1250
+ const [collapsed, setCollapsed] = useState(false);
1251
+ const lineCount = content.split("\n").length;
1252
+ return /* @__PURE__ */ jsxs("div", { className: "my-2 rounded-lg bg-zinc-900 border border-zinc-700/50 overflow-hidden", children: [
1253
+ /* @__PURE__ */ jsxs(
1254
+ "div",
1255
+ {
1256
+ className: "flex items-center justify-between px-3 py-1.5 bg-zinc-800/50 cursor-pointer select-none",
1257
+ onClick: () => setCollapsed((c) => !c),
1258
+ children: [
1259
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1260
+ lang && /* @__PURE__ */ jsx("span", { className: "text-[10px] text-zinc-500 font-mono uppercase tracking-wider", children: lang }),
1261
+ /* @__PURE__ */ jsxs("span", { className: "text-[10px] text-zinc-600 font-mono", children: [
1262
+ lineCount,
1263
+ " lines"
1264
+ ] })
1265
+ ] }),
1266
+ /* @__PURE__ */ jsx(
1267
+ "svg",
1268
+ {
1269
+ className: `w-3 h-3 text-zinc-500 transition-transform ${collapsed ? "" : "rotate-180"}`,
1270
+ viewBox: "0 0 16 16",
1271
+ fill: "currentColor",
1272
+ children: /* @__PURE__ */ jsx("path", { d: "M4.646 5.646a.5.5 0 0 1 .708 0L8 8.293l2.646-2.647a.5.5 0 0 1 .708.708l-3 3a.5.5 0 0 1-.708 0l-3-3a.5.5 0 0 1 0-.708z" })
1273
+ }
1274
+ )
1275
+ ]
1276
+ }
1277
+ ),
1278
+ !collapsed && /* @__PURE__ */ jsx("pre", { className: "p-3 overflow-x-auto", children: /* @__PURE__ */ jsx("code", { className: "text-[12px] font-mono text-zinc-300 leading-relaxed whitespace-pre", children: content }) })
1279
+ ] });
1280
+ }
1281
+ function renderContent(text) {
1282
+ const parts = [];
1283
+ const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
1284
+ let lastIndex = 0;
1285
+ let match;
1286
+ while ((match = codeBlockRegex.exec(text)) !== null) {
1287
+ if (match.index > lastIndex) {
1288
+ parts.push({ type: "text", content: text.slice(lastIndex, match.index) });
1289
+ }
1290
+ parts.push({
1291
+ type: "code",
1292
+ content: match[2],
1293
+ lang: match[1] || void 0
1294
+ });
1295
+ lastIndex = match.index + match[0].length;
1296
+ }
1297
+ if (lastIndex < text.length) {
1298
+ parts.push({ type: "text", content: text.slice(lastIndex) });
1299
+ }
1300
+ if (parts.length === 0) {
1301
+ parts.push({ type: "text", content: text });
1302
+ }
1303
+ return /* @__PURE__ */ jsx(Fragment, { children: parts.map((part, i) => {
1304
+ if (part.type === "code") {
1305
+ return /* @__PURE__ */ jsx(CodeBlock, { lang: part.lang, content: part.content }, i);
1306
+ }
1307
+ return /* @__PURE__ */ jsx("span", { className: "whitespace-pre-wrap", children: part.content }, i);
1308
+ }) });
1309
+ }
1310
+ function renderUserContent(text) {
1311
+ const parts = text.split(/(@\/[^\s]+)/g);
1312
+ return /* @__PURE__ */ jsx(Fragment, { children: parts.map(
1313
+ (part, i) => part.startsWith("@/") ? /* @__PURE__ */ jsxs(
1314
+ "span",
1315
+ {
1316
+ className: "inline-flex items-center gap-0.5 px-1 py-0.5 rounded bg-blue-500/20 text-blue-300 font-mono text-[11px]",
1317
+ children: [
1318
+ /* @__PURE__ */ jsx(
1319
+ "svg",
1320
+ {
1321
+ className: "w-2.5 h-2.5 flex-shrink-0",
1322
+ viewBox: "0 0 16 16",
1323
+ fill: "currentColor",
1324
+ children: /* @__PURE__ */ jsx("path", { d: "M3.75 1.5A2.25 2.25 0 0 0 1.5 3.75v8.5A2.25 2.25 0 0 0 3.75 14.5h8.5a2.25 2.25 0 0 0 2.25-2.25V6.621a2.25 2.25 0 0 0-.659-1.591L10.47 1.659A2.25 2.25 0 0 0 8.879 1.5H3.75z" })
1325
+ }
1326
+ ),
1327
+ part.slice(1)
1328
+ ]
1329
+ },
1330
+ i
1331
+ ) : /* @__PURE__ */ jsx("span", { children: part }, i)
1332
+ ) });
1333
+ }
1334
+ function MessageBubble({
1335
+ msg,
1336
+ theme
1337
+ }) {
1338
+ const [collapsed, setCollapsed] = useState(false);
1339
+ const hasCode = msg.role === "assistant" && /```/.test(msg.content);
1340
+ const isLong = msg.content.length > 500;
1341
+ const canCollapse = (hasCode || isLong) && !msg.isStreaming;
1342
+ if (msg.role === "user") {
1343
+ return /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(
1344
+ "div",
1345
+ {
1346
+ className: `max-w-[85%] rounded-xl px-3.5 py-2.5 text-[13px] leading-relaxed ${theme.userBubble} text-zinc-200`,
1347
+ children: renderUserContent(msg.content)
1348
+ }
1349
+ ) });
1350
+ }
1351
+ return /* @__PURE__ */ jsx("div", { className: "text-[13px] leading-relaxed text-zinc-300", children: collapsed ? /* @__PURE__ */ jsxs(
1352
+ "button",
1353
+ {
1354
+ onClick: () => setCollapsed(false),
1355
+ className: "flex items-center gap-1.5 text-left",
1356
+ children: [
1357
+ /* @__PURE__ */ jsx(
1358
+ "svg",
1359
+ {
1360
+ className: "w-2.5 h-2.5 text-zinc-600 flex-shrink-0",
1361
+ viewBox: "0 0 16 16",
1362
+ fill: "currentColor",
1363
+ children: /* @__PURE__ */ jsx("path", { d: "M6.646 3.646a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L9.293 7 6.646 4.354a.5.5 0 0 1 0-.708z" })
1364
+ }
1365
+ ),
1366
+ /* @__PURE__ */ jsxs("span", { className: "text-[11px] text-zinc-600", children: [
1367
+ msg.content.split("\n")[0].replace(/```\w*/, "").trim().slice(0, 60) || "Response",
1368
+ "..."
1369
+ ] })
1370
+ ]
1371
+ }
1372
+ ) : /* @__PURE__ */ jsxs("div", { children: [
1373
+ renderContent(msg.content),
1374
+ msg.isStreaming && /* @__PURE__ */ jsx("span", { className: "inline-flex items-center gap-1 ml-1", children: /* @__PURE__ */ jsx(
1375
+ "span",
1376
+ {
1377
+ className: `inline-block h-1.5 w-1.5 rounded-full ${theme.streamingDot} animate-pulse`
1378
+ }
1379
+ ) }),
1380
+ canCollapse && /* @__PURE__ */ jsxs(
1381
+ "button",
1382
+ {
1383
+ onClick: () => setCollapsed(true),
1384
+ className: "mt-1 flex items-center gap-1 text-zinc-600 hover:text-zinc-400 transition-colors",
1385
+ children: [
1386
+ /* @__PURE__ */ jsx(
1387
+ "svg",
1388
+ {
1389
+ className: "w-2.5 h-2.5",
1390
+ viewBox: "0 0 16 16",
1391
+ fill: "currentColor",
1392
+ children: /* @__PURE__ */ jsx("path", { d: "M4.646 5.646a.5.5 0 0 1 .708 0L8 8.293l2.646-2.647a.5.5 0 0 1 .708.708l-3 3a.5.5 0 0 1-.708 0l-3-3a.5.5 0 0 1 0-.708z" })
1393
+ }
1394
+ ),
1395
+ /* @__PURE__ */ jsx("span", { className: "text-[10px]", children: "Collapse" })
1396
+ ]
1397
+ }
1398
+ )
1399
+ ] }) });
1400
+ }
1401
+ function AIChatPanel({
1402
+ messages,
1403
+ onSendMessage,
1404
+ isStreaming = false,
1405
+ accentColor = "blue",
1406
+ title = "Chat",
1407
+ modelLabel,
1408
+ onLoadMore,
1409
+ hasMoreMessages = false,
1410
+ filePaths = [],
1411
+ inspectedElement,
1412
+ onClearInspectedElement
1413
+ }) {
1414
+ const [input, setInput] = useState("");
1415
+ const scrollRef = useRef(null);
1416
+ const sentinelRef = useRef(null);
1417
+ const textareaRef = useRef(null);
1418
+ const isAtBottomRef = useRef(true);
1419
+ const [mentionQuery, setMentionQuery] = useState(null);
1420
+ const [mentionStart, setMentionStart] = useState(0);
1421
+ const [mentionIdx, setMentionIdx] = useState(0);
1422
+ const theme = getChatTheme(accentColor);
1423
+ const mentionFiltered = useMemo(() => {
1424
+ if (mentionQuery === null) return [];
1425
+ const q = mentionQuery.toLowerCase();
1426
+ return filePaths.filter((p) => p.toLowerCase().includes(q)).slice(0, 8);
1427
+ }, [mentionQuery, filePaths]);
1428
+ useEffect(() => {
1429
+ setMentionIdx(0);
1430
+ }, [mentionFiltered.length]);
1431
+ useEffect(() => {
1432
+ const el = scrollRef.current;
1433
+ if (el && isAtBottomRef.current) {
1434
+ el.scrollTop = el.scrollHeight;
1435
+ }
1436
+ }, [messages]);
1437
+ useEffect(() => {
1438
+ const el = scrollRef.current;
1439
+ if (!el) return;
1440
+ const handleScroll = () => {
1441
+ const threshold = 40;
1442
+ isAtBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
1443
+ };
1444
+ el.addEventListener("scroll", handleScroll, { passive: true });
1445
+ return () => el.removeEventListener("scroll", handleScroll);
1446
+ }, []);
1447
+ useEffect(() => {
1448
+ const sentinel = sentinelRef.current;
1449
+ if (!sentinel || !onLoadMore || !hasMoreMessages) return;
1450
+ const observer = new IntersectionObserver(
1451
+ (entries) => {
1452
+ if (entries[0].isIntersecting) {
1453
+ onLoadMore();
1454
+ }
1455
+ },
1456
+ { root: scrollRef.current, threshold: 0 }
1457
+ );
1458
+ observer.observe(sentinel);
1459
+ return () => observer.disconnect();
1460
+ }, [onLoadMore, hasMoreMessages]);
1461
+ useEffect(() => {
1462
+ const ta = textareaRef.current;
1463
+ if (!ta) return;
1464
+ ta.style.height = "auto";
1465
+ ta.style.height = `${Math.min(ta.scrollHeight, 200)}px`;
1466
+ }, [input]);
1467
+ const handleInputChange = useCallback(
1468
+ (e) => {
1469
+ const val = e.target.value;
1470
+ setInput(val);
1471
+ const cursorPos = e.target.selectionStart;
1472
+ const textBefore = val.slice(0, cursorPos);
1473
+ const atMatch = textBefore.match(/(?:^|\s)@([^\s]*)$/);
1474
+ if (atMatch) {
1475
+ setMentionQuery(atMatch[1]);
1476
+ setMentionStart(cursorPos - atMatch[1].length - 1);
1477
+ } else {
1478
+ setMentionQuery(null);
1479
+ }
1480
+ },
1481
+ []
1482
+ );
1483
+ const handleMentionSelect = useCallback(
1484
+ (path) => {
1485
+ const before = input.slice(0, mentionStart);
1486
+ const after = input.slice(mentionStart + 1 + (mentionQuery?.length ?? 0));
1487
+ const newInput = `${before}@${path}${after ? after : " "}`;
1488
+ setInput(newInput);
1489
+ setMentionQuery(null);
1490
+ requestAnimationFrame(() => {
1491
+ const ta = textareaRef.current;
1492
+ if (ta) {
1493
+ ta.focus();
1494
+ const pos = before.length + 1 + path.length + 1;
1495
+ ta.selectionStart = pos;
1496
+ ta.selectionEnd = pos;
1497
+ }
1498
+ });
1499
+ },
1500
+ [input, mentionStart, mentionQuery]
1501
+ );
1502
+ const handleSend = useCallback(() => {
1503
+ const trimmed = input.trim();
1504
+ if (!trimmed || isStreaming) return;
1505
+ let messageContent = trimmed;
1506
+ if (inspectedElement) {
1507
+ const ctx = `[Inspected element: <${inspectedElement.tagName} class="${inspectedElement.className}"> "${inspectedElement.textContent.slice(0, 100)}"]
1508
+
1509
+ `;
1510
+ messageContent = ctx + trimmed;
1511
+ onClearInspectedElement?.();
1512
+ }
1513
+ onSendMessage(messageContent);
1514
+ setInput("");
1515
+ setMentionQuery(null);
1516
+ isAtBottomRef.current = true;
1517
+ }, [
1518
+ input,
1519
+ isStreaming,
1520
+ onSendMessage,
1521
+ inspectedElement,
1522
+ onClearInspectedElement
1523
+ ]);
1524
+ const handleKeyDown = useCallback(
1525
+ (e) => {
1526
+ if (mentionQuery !== null && mentionFiltered.length > 0) {
1527
+ if (e.key === "ArrowDown") {
1528
+ e.preventDefault();
1529
+ setMentionIdx((i) => (i + 1) % mentionFiltered.length);
1530
+ return;
1531
+ }
1532
+ if (e.key === "ArrowUp") {
1533
+ e.preventDefault();
1534
+ setMentionIdx(
1535
+ (i) => (i - 1 + mentionFiltered.length) % mentionFiltered.length
1536
+ );
1537
+ return;
1538
+ }
1539
+ if (e.key === "Enter" || e.key === "Tab") {
1540
+ e.preventDefault();
1541
+ handleMentionSelect(mentionFiltered[mentionIdx]);
1542
+ return;
1543
+ }
1544
+ if (e.key === "Escape") {
1545
+ e.preventDefault();
1546
+ setMentionQuery(null);
1547
+ return;
1548
+ }
1549
+ }
1550
+ if (e.key === "Enter" && !e.shiftKey) {
1551
+ e.preventDefault();
1552
+ handleSend();
1553
+ }
1554
+ },
1555
+ [
1556
+ handleSend,
1557
+ mentionQuery,
1558
+ mentionFiltered,
1559
+ mentionIdx,
1560
+ handleMentionSelect
1561
+ ]
1562
+ );
1563
+ return /* @__PURE__ */ jsxs("div", { className: "h-full flex flex-col bg-zinc-950", children: [
1564
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-3 py-1.5 bg-zinc-900/50 border-b border-zinc-800", children: [
1565
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
1566
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
1567
+ /* @__PURE__ */ jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-red-500/70" }),
1568
+ /* @__PURE__ */ jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-yellow-500/70" }),
1569
+ /* @__PURE__ */ jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-green-500/70" })
1570
+ ] }),
1571
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wider text-zinc-500", children: title })
1572
+ ] }),
1573
+ modelLabel && /* @__PURE__ */ jsx(
1574
+ "span",
1575
+ {
1576
+ className: `text-[10px] font-mono px-2 py-0.5 rounded-full border ${theme.modelBadge}`,
1577
+ children: modelLabel
1578
+ }
1579
+ )
1580
+ ] }),
1581
+ /* @__PURE__ */ jsxs(
1582
+ "div",
1583
+ {
1584
+ ref: scrollRef,
1585
+ className: "flex-1 overflow-y-auto px-3 py-3 space-y-3",
1586
+ children: [
1587
+ hasMoreMessages && /* @__PURE__ */ jsx("div", { ref: sentinelRef, className: "flex justify-center py-2", children: /* @__PURE__ */ jsx("div", { className: "h-4 w-4 animate-spin rounded-full border border-zinc-600 border-t-zinc-400" }) }),
1588
+ messages.map((msg) => /* @__PURE__ */ jsx(MessageBubble, { msg, theme }, msg.id))
1589
+ ]
1590
+ }
1591
+ ),
1592
+ /* @__PURE__ */ jsxs("div", { className: "flex-shrink-0 border-t border-zinc-800 p-3", children: [
1593
+ inspectedElement && /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center gap-2 px-2.5 py-1.5 rounded-lg bg-blue-500/10 border border-blue-500/20", children: [
1594
+ /* @__PURE__ */ jsxs(
1595
+ "svg",
1596
+ {
1597
+ className: "w-3.5 h-3.5 text-blue-400 flex-shrink-0",
1598
+ viewBox: "0 0 24 24",
1599
+ fill: "none",
1600
+ stroke: "currentColor",
1601
+ strokeWidth: 2,
1602
+ strokeLinecap: "round",
1603
+ strokeLinejoin: "round",
1604
+ children: [
1605
+ /* @__PURE__ */ jsx("path", { d: "M3 12l3 0" }),
1606
+ /* @__PURE__ */ jsx("path", { d: "M12 3l0 3" }),
1607
+ /* @__PURE__ */ jsx("path", { d: "M7.8 7.8l-2.2 -2.2" }),
1608
+ /* @__PURE__ */ jsx("path", { d: "M16.2 7.8l2.2 -2.2" }),
1609
+ /* @__PURE__ */ jsx("path", { d: "M7.8 16.2l-2.2 2.2" }),
1610
+ /* @__PURE__ */ jsx("path", { d: "M12 12l9 3l-4 2l-2 4l-3 -9" })
1611
+ ]
1612
+ }
1613
+ ),
1614
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
1615
+ /* @__PURE__ */ jsxs("span", { className: "text-[11px] font-mono text-blue-300", children: [
1616
+ "<",
1617
+ inspectedElement.tagName,
1618
+ ">"
1619
+ ] }),
1620
+ inspectedElement.textContent && /* @__PURE__ */ jsxs("span", { className: "text-[11px] text-zinc-500 ml-1.5 truncate", children: [
1621
+ '"',
1622
+ inspectedElement.textContent.slice(0, 40),
1623
+ inspectedElement.textContent.length > 40 ? "..." : "",
1624
+ '"'
1625
+ ] })
1626
+ ] }),
1627
+ /* @__PURE__ */ jsx(
1628
+ "button",
1629
+ {
1630
+ onClick: onClearInspectedElement,
1631
+ className: "text-zinc-500 hover:text-zinc-300 transition-colors flex-shrink-0",
1632
+ children: /* @__PURE__ */ jsx("svg", { className: "w-3 h-3", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" }) })
1633
+ }
1634
+ )
1635
+ ] }),
1636
+ /* @__PURE__ */ jsxs("div", { className: "relative flex items-end gap-2", children: [
1637
+ mentionQuery !== null && mentionFiltered.length > 0 && /* @__PURE__ */ jsx("div", { className: "absolute bottom-full left-0 right-0 mb-1 bg-zinc-900 border border-zinc-700 rounded-lg shadow-xl overflow-hidden z-10", children: mentionFiltered.map((path, i) => /* @__PURE__ */ jsxs(
1638
+ "button",
1639
+ {
1640
+ onMouseDown: (e) => {
1641
+ e.preventDefault();
1642
+ handleMentionSelect(path);
1643
+ },
1644
+ onMouseEnter: () => setMentionIdx(i),
1645
+ className: `w-full text-left px-3 py-1.5 text-[12px] font-mono flex items-center gap-2 transition-colors ${i === mentionIdx ? "bg-blue-500/15 text-blue-300" : "text-zinc-400 hover:bg-zinc-800"}`,
1646
+ children: [
1647
+ /* @__PURE__ */ jsx(
1648
+ "svg",
1649
+ {
1650
+ className: "w-3 h-3 flex-shrink-0 text-zinc-500",
1651
+ viewBox: "0 0 16 16",
1652
+ fill: "currentColor",
1653
+ children: /* @__PURE__ */ jsx("path", { d: "M3.75 1.5A2.25 2.25 0 0 0 1.5 3.75v8.5A2.25 2.25 0 0 0 3.75 14.5h8.5a2.25 2.25 0 0 0 2.25-2.25V6.621a2.25 2.25 0 0 0-.659-1.591L10.47 1.659A2.25 2.25 0 0 0 8.879 1.5H3.75z" })
1654
+ }
1655
+ ),
1656
+ path
1657
+ ]
1658
+ },
1659
+ path
1660
+ )) }),
1661
+ /* @__PURE__ */ jsx(
1662
+ "textarea",
1663
+ {
1664
+ ref: textareaRef,
1665
+ value: input,
1666
+ onChange: handleInputChange,
1667
+ onKeyDown: handleKeyDown,
1668
+ disabled: isStreaming,
1669
+ placeholder: isStreaming ? "Generating..." : "Describe a component... (@ to reference files)",
1670
+ rows: 1,
1671
+ className: `
1672
+ flex-1 resize-none overflow-hidden bg-zinc-900 border border-zinc-700/50 rounded-lg px-3 py-2
1673
+ text-[13px] text-zinc-200 font-mono outline-none
1674
+ placeholder:text-zinc-600 disabled:opacity-50
1675
+ focus:border-zinc-600 transition-colors
1676
+ min-h-[60px]
1677
+ ${theme.inputCaret}
1678
+ `
1679
+ }
1680
+ ),
1681
+ /* @__PURE__ */ jsx(
1682
+ "button",
1683
+ {
1684
+ onClick: handleSend,
1685
+ disabled: isStreaming || !input.trim(),
1686
+ className: `
1687
+ flex-shrink-0 h-9 w-9 rounded-lg flex items-center justify-center
1688
+ transition-colors disabled:cursor-not-allowed
1689
+ ${isStreaming || !input.trim() ? theme.sendButtonDisabled : theme.sendButton}
1690
+ `,
1691
+ children: isStreaming ? /* @__PURE__ */ jsx("div", { className: "h-4 w-4 animate-spin rounded-full border border-zinc-500 border-t-zinc-300" }) : /* @__PURE__ */ jsx("svg", { className: "w-4 h-4", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M1.724 1.053a.5.5 0 0 0-.714.545l1.403 4.85a.5.5 0 0 0 .397.354l5.69.953c.268.045.268.545 0 .59l-5.69.953a.5.5 0 0 0-.397.354l-1.403 4.85a.5.5 0 0 0 .714.545l13-6.5a.5.5 0 0 0 0-.894l-13-6.5z" }) })
1692
+ }
1693
+ )
1694
+ ] }),
1695
+ /* @__PURE__ */ jsx("p", { className: "text-[10px] text-zinc-600 mt-1.5 px-1", children: "Enter to send · Shift+Enter for newline · @ to reference files" })
1696
+ ] })
1697
+ ] });
1698
+ }
1699
+ export {
1700
+ AIChatPanel,
1701
+ BrowserWindow,
1702
+ ChatIcon,
1703
+ CodeEditor,
1704
+ CodeIcon,
1705
+ FileExplorer,
1706
+ FilesIcon,
1707
+ MobileTabBar,
1708
+ PanelToggle,
1709
+ PreviewIcon,
1710
+ Terminal,
1711
+ TerminalIcon,
1712
+ getChatTheme,
1713
+ getEditorTheme,
1714
+ getFileExplorerTheme,
1715
+ getLanguageLabel,
1716
+ getLineStyles,
1717
+ getTerminalTheme,
1718
+ useMediaQuery
1719
+ };
1720
+ //# sourceMappingURL=index.mjs.map