zelune 0.0.0-stage → 1.4.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.
@@ -0,0 +1,979 @@
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import {
3
+ Code2,
4
+ Sparkles,
5
+ Type,
6
+ Palette,
7
+ Box,
8
+ Navigation,
9
+ Copy,
10
+ Check,
11
+ Sliders,
12
+ X,
13
+ PanelRightClose,
14
+ } from 'lucide-react';
15
+ import type { InspectedElement, PatchPayload, StyleValue } from '../server/astEngine';
16
+
17
+ interface InspectorPanelProps {
18
+ inspected: InspectedElement | null;
19
+ isOpen: boolean;
20
+ isLoading: boolean;
21
+ isSaving: boolean;
22
+ statusMessage: string | null;
23
+ onPatch: (payload: PatchPayload) => Promise<void>;
24
+ onClearSelection: () => void;
25
+ onCollapse: () => void;
26
+ onJumpToScreen: (routeName: string) => void;
27
+ }
28
+
29
+ const COLOR_SWATCHES = [
30
+ '#0A0B0E',
31
+ '#13161E',
32
+ '#1E2433',
33
+ '#38BDF8',
34
+ '#6366F1',
35
+ '#10B981',
36
+ '#F59E0B',
37
+ '#F43F5E',
38
+ '#F8FAFC',
39
+ ];
40
+
41
+ const FONT_WEIGHTS = ['400', '500', '600', '700', '800'];
42
+
43
+ export const InspectorPanel: React.FC<InspectorPanelProps> = ({
44
+ inspected,
45
+ isOpen,
46
+ isLoading,
47
+ isSaving,
48
+ statusMessage,
49
+ onPatch,
50
+ onClearSelection,
51
+ onCollapse,
52
+ onJumpToScreen,
53
+ }) => {
54
+ const [localText, setLocalText] = useState('');
55
+ const [localStyles, setLocalStyles] = useState<Record<string, StyleValue>>({});
56
+ const [agentInstruction, setAgentInstruction] = useState('');
57
+ const [copiedPrompt, setCopiedPrompt] = useState(false);
58
+
59
+ const dialogRef = useRef<HTMLDialogElement | null>(null);
60
+ const debounceTimerRef = useRef<number | null>(null);
61
+
62
+ useEffect(() => {
63
+ if (!inspected) {
64
+ setLocalText('');
65
+ setLocalStyles({});
66
+ return;
67
+ }
68
+ setLocalText(inspected.textContent ?? '');
69
+ setLocalStyles(inspected.styles ?? {});
70
+ }, [inspected]);
71
+
72
+ // Modern Web Guidance fallback for <dialog closedby="any"> light-dismiss in browsers without native closedBy
73
+ useEffect(() => {
74
+ const dialog = dialogRef.current;
75
+ if (!dialog) return;
76
+
77
+ if (!('closedBy' in HTMLDialogElement.prototype)) {
78
+ const handleBackdropClick = (event: MouseEvent) => {
79
+ if (event.target !== dialog) return;
80
+ const rect = dialog.getBoundingClientRect();
81
+ const isDialogContent =
82
+ rect.top <= event.clientY &&
83
+ event.clientY <= rect.top + rect.height &&
84
+ rect.left <= event.clientX &&
85
+ event.clientX <= rect.left + rect.width;
86
+ if (isDialogContent) return;
87
+ dialog.close();
88
+ };
89
+ dialog.addEventListener('click', handleBackdropClick);
90
+ return () => dialog.removeEventListener('click', handleBackdropClick);
91
+ }
92
+ }, [inspected]);
93
+
94
+ const schedulePatch = (nextPatch: Omit<PatchPayload, 'file' | 'astPath'>) => {
95
+ if (!inspected) return;
96
+ if (debounceTimerRef.current) {
97
+ window.clearTimeout(debounceTimerRef.current);
98
+ }
99
+ debounceTimerRef.current = window.setTimeout(() => {
100
+ void onPatch({
101
+ file: inspected.file,
102
+ astPath: inspected.astPath,
103
+ line: inspected.line,
104
+ ...nextPatch,
105
+ });
106
+ }, 220);
107
+ };
108
+
109
+ const handleTextChange = (val: string) => {
110
+ setLocalText(val);
111
+ schedulePatch({ text: val });
112
+ };
113
+
114
+ const handleStyleChange = (key: string, rawVal: string | number) => {
115
+ const nextStyles = { ...localStyles, [key]: rawVal };
116
+ setLocalStyles(nextStyles);
117
+ schedulePatch({ styles: { [key]: rawVal } });
118
+ };
119
+
120
+ const handleNumericStyleChange = (key: string, rawStr: string) => {
121
+ if (rawStr.trim() === '') {
122
+ const copy = { ...localStyles };
123
+ delete copy[key];
124
+ setLocalStyles(copy);
125
+ schedulePatch({ styles: { [key]: null } });
126
+ return;
127
+ }
128
+ const parsed = Number(rawStr);
129
+ if (!Number.isNaN(parsed)) {
130
+ handleStyleChange(key, parsed);
131
+ }
132
+ };
133
+
134
+ const buildAntigravityPrompt = () => {
135
+ if (!inspected) return '';
136
+ return [
137
+ `Refactor the <${inspected.tagName}> element in \`${inspected.file}\` (around line ${inspected.line}, AST index ${inspected.astPath}).`,
138
+ inspected.styleSheetKey
139
+ ? `Associated StyleSheet key: \`styles.${inspected.styleSheetKey}\`.`
140
+ : '',
141
+ '',
142
+ `Current JSX Snippet:`,
143
+ '```tsx',
144
+ inspected.codeSnippet,
145
+ '```',
146
+ '',
147
+ `Requested Change:`,
148
+ agentInstruction.trim() || 'Update this component layout and styling per DESIGN.md.',
149
+ ]
150
+ .filter(Boolean)
151
+ .join('\n');
152
+ };
153
+
154
+ const handleCopyPrompt = async () => {
155
+ const text = buildAntigravityPrompt();
156
+ await navigator.clipboard.writeText(text);
157
+ setCopiedPrompt(true);
158
+ window.setTimeout(() => setCopiedPrompt(false), 2000);
159
+ };
160
+
161
+ const targetWidth = inspected ? 340 : 320;
162
+
163
+ return (
164
+ <aside
165
+ aria-hidden={!isOpen}
166
+ className="zelune-collapsible-panel"
167
+ style={{
168
+ width: isOpen ? targetWidth : 0,
169
+ opacity: isOpen ? 1 : 0,
170
+ pointerEvents: isOpen ? 'auto' : 'none',
171
+ backgroundColor: 'var(--studio-surface-1)',
172
+ borderLeft: isOpen ? '1px solid var(--studio-border-hairline)' : '0px solid transparent',
173
+ display: 'flex',
174
+ flexDirection: 'column',
175
+ height: '100%',
176
+ zIndex: 30,
177
+ }}
178
+ >
179
+ {!inspected ? (
180
+ <div
181
+ className="zelune-scroller"
182
+ style={{
183
+ width: 320,
184
+ minWidth: 320,
185
+ height: '100%',
186
+ display: 'flex',
187
+ flexDirection: 'column',
188
+ justifyContent: 'space-between',
189
+ padding: 18,
190
+ color: 'var(--studio-text-secondary)',
191
+ overflowY: 'auto',
192
+ }}
193
+ >
194
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
195
+ <div
196
+ style={{
197
+ display: 'flex',
198
+ alignItems: 'center',
199
+ justifyContent: 'space-between',
200
+ color: '#F8FAFC',
201
+ fontWeight: 600,
202
+ fontSize: 13.5,
203
+ letterSpacing: '-0.01em',
204
+ }}
205
+ >
206
+ <span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
207
+ <Sliders size={15} color="#38BDF8" />
208
+ <span>AST Visual Inspector</span>
209
+ </span>
210
+ <button
211
+ type="button"
212
+ className="zelune-interactive"
213
+ onClick={onCollapse}
214
+ title="Collapse Inspector Panel (])"
215
+ style={{
216
+ background: 'transparent',
217
+ border: '1px solid transparent',
218
+ borderRadius: 5,
219
+ color: 'var(--studio-text-muted)',
220
+ cursor: 'pointer',
221
+ padding: 4,
222
+ display: 'inline-flex',
223
+ alignItems: 'center',
224
+ justifyContent: 'center',
225
+ }}
226
+ >
227
+ <PanelRightClose size={14} />
228
+ </button>
229
+ </div>
230
+
231
+ <div
232
+ style={{
233
+ backgroundColor: 'var(--studio-surface-2)',
234
+ border: '1px solid var(--studio-border-hairline)',
235
+ borderRadius: 10,
236
+ padding: 14,
237
+ fontSize: 12.5,
238
+ lineHeight: 1.6,
239
+ }}
240
+ >
241
+ Select any node on a <code style={{ color: '#38BDF8', fontFamily: 'var(--font-mono)' }}>393 × 852</code>{' '}
242
+ artboard to inspect and live-patch its{' '}
243
+ <strong style={{ color: '#E2E8F0' }}>StyleSheet.create</strong> rules or{' '}
244
+ <strong style={{ color: '#E2E8F0' }}>static copy</strong> directly in the{' '}
245
+ <code style={{ color: '#A5B4FC', fontFamily: 'var(--font-mono)' }}>.tsx</code> AST.
246
+ </div>
247
+
248
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 4 }}>
249
+ <div
250
+ style={{
251
+ fontSize: 10.5,
252
+ fontFamily: 'var(--font-mono)',
253
+ fontWeight: 600,
254
+ color: 'var(--studio-text-muted)',
255
+ letterSpacing: '0.06em',
256
+ }}
257
+ >
258
+ DESIGN &amp; SYNC GOVERNANCE
259
+ </div>
260
+ <div style={{ fontSize: 12, color: 'var(--studio-text-secondary)', lineHeight: 1.5 }}>
261
+ • <strong style={{ color: '#CBD5E1' }}>Tier 1 (Surgical AST)</strong>: Instant write-back to colors, typography, spacing, and copy.
262
+ </div>
263
+ <div style={{ fontSize: 12, color: 'var(--studio-text-secondary)', lineHeight: 1.5 }}>
264
+ • <strong style={{ color: '#CBD5E1' }}>Tier 2 (Antigravity Bridge)</strong>: Dispatch structural refactors with exact file &amp; AST path coordinates.
265
+ </div>
266
+ <div style={{ fontSize: 12, color: 'var(--studio-text-secondary)', lineHeight: 1.5 }}>
267
+ • <strong style={{ color: '#CBD5E1' }}>Impeccable &amp; Taste Guardrails</strong>: Governed by{' '}
268
+ <code style={{ fontFamily: 'var(--font-mono)', color: '#38BDF8' }}>PRODUCT.md</code> &amp;{' '}
269
+ <code style={{ fontFamily: 'var(--font-mono)', color: '#38BDF8' }}>DESIGN.md</code>.
270
+ </div>
271
+ </div>
272
+ </div>
273
+
274
+ <div
275
+ style={{
276
+ fontSize: 11,
277
+ fontFamily: 'var(--font-mono)',
278
+ color: 'var(--studio-text-muted)',
279
+ borderTop: '1px solid var(--studio-border-hairline)',
280
+ paddingTop: 12,
281
+ }}
282
+ >
283
+ MCP Server: <span style={{ color: '#34D399' }}>Zelune Ready (mcp/server.ts)</span>
284
+ </div>
285
+ </div>
286
+ ) : (
287
+ <div
288
+ className="zelune-scroller"
289
+ style={{
290
+ width: 340,
291
+ minWidth: 340,
292
+ height: '100%',
293
+ display: 'flex',
294
+ flexDirection: 'column',
295
+ overflowY: 'auto',
296
+ }}
297
+ >
298
+ {/* Inspector Header */}
299
+ <div
300
+ style={{
301
+ padding: '12px 16px',
302
+ borderBottom: '1px solid var(--studio-border-hairline)',
303
+ display: 'flex',
304
+ alignItems: 'center',
305
+ justifyContent: 'space-between',
306
+ backgroundColor: 'var(--studio-surface-2)',
307
+ }}
308
+ >
309
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
310
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
311
+ <span
312
+ style={{
313
+ backgroundColor: 'rgba(99, 102, 241, 0.18)',
314
+ border: '1px solid rgba(99, 102, 241, 0.4)',
315
+ color: '#C7D2FE',
316
+ fontSize: 11.5,
317
+ fontWeight: 700,
318
+ fontFamily: 'var(--font-mono)',
319
+ padding: '1px 6px',
320
+ borderRadius: 4,
321
+ }}
322
+ >
323
+ &lt;{inspected.tagName}&gt;
324
+ </span>
325
+ {inspected.styleSheetKey && (
326
+ <span
327
+ style={{
328
+ backgroundColor: 'rgba(16, 185, 129, 0.14)',
329
+ border: '1px solid rgba(16, 185, 129, 0.35)',
330
+ color: '#A7F3D0',
331
+ fontSize: 11,
332
+ fontFamily: 'var(--font-mono)',
333
+ padding: '1px 6px',
334
+ borderRadius: 4,
335
+ }}
336
+ >
337
+ styles.{inspected.styleSheetKey}
338
+ </span>
339
+ )}
340
+ </div>
341
+ <span
342
+ className="tabular-nums"
343
+ style={{
344
+ fontSize: 10.5,
345
+ fontFamily: 'var(--font-mono)',
346
+ color: 'var(--studio-text-muted)',
347
+ marginTop: 1,
348
+ }}
349
+ >
350
+ {inspected.file}:{inspected.line}:{inspected.col}
351
+ </span>
352
+ </div>
353
+
354
+ <div style={{ display: 'flex', alignItems: 'center', gap: 2 }}>
355
+ <button
356
+ type="button"
357
+ className="zelune-interactive"
358
+ onClick={onCollapse}
359
+ title="Collapse Inspector Panel (])"
360
+ style={{
361
+ background: 'transparent',
362
+ border: '1px solid transparent',
363
+ borderRadius: 5,
364
+ color: 'var(--studio-text-muted)',
365
+ cursor: 'pointer',
366
+ padding: 4,
367
+ display: 'inline-flex',
368
+ alignItems: 'center',
369
+ }}
370
+ >
371
+ <PanelRightClose size={14} />
372
+ </button>
373
+ <button
374
+ type="button"
375
+ className="zelune-interactive"
376
+ onClick={onClearSelection}
377
+ title="Deselect element (Esc)"
378
+ style={{
379
+ background: 'transparent',
380
+ border: '1px solid transparent',
381
+ borderRadius: 5,
382
+ color: 'var(--studio-text-muted)',
383
+ cursor: 'pointer',
384
+ padding: 4,
385
+ display: 'inline-flex',
386
+ alignItems: 'center',
387
+ }}
388
+ >
389
+ <X size={15} />
390
+ </button>
391
+ </div>
392
+ </div>
393
+
394
+ {/* Live Save Status Indicator */}
395
+ {(isLoading || isSaving || statusMessage) && (
396
+ <div
397
+ style={{
398
+ padding: '6px 16px',
399
+ backgroundColor: 'rgba(30, 27, 75, 0.65)',
400
+ borderBottom: '1px solid rgba(99, 102, 241, 0.3)',
401
+ fontSize: 11,
402
+ fontFamily: 'var(--font-mono)',
403
+ color: '#A5B4FC',
404
+ display: 'flex',
405
+ alignItems: 'center',
406
+ justifyContent: 'space-between',
407
+ }}
408
+ >
409
+ <span>
410
+ {isSaving
411
+ ? 'Patching AST & writing .tsx...'
412
+ : isLoading
413
+ ? 'Inspecting AST node...'
414
+ : statusMessage}
415
+ </span>
416
+ <span style={{ color: '#34D399' }}>● AST Sync</span>
417
+ </div>
418
+ )}
419
+
420
+ {/* Navigation Flow Link Banner if element triggers navigation */}
421
+ {inspected.navigationTarget && (
422
+ <div
423
+ style={{
424
+ margin: '12px 14px 0',
425
+ padding: '9px 12px',
426
+ backgroundColor: 'rgba(6, 78, 59, 0.45)',
427
+ border: '1px solid rgba(16, 185, 129, 0.45)',
428
+ borderRadius: 8,
429
+ display: 'flex',
430
+ alignItems: 'center',
431
+ justifyContent: 'space-between',
432
+ }}
433
+ >
434
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, color: '#D1FAE5' }}>
435
+ <Navigation size={13} color="#34D399" />
436
+ <span>
437
+ Flow → <strong>{inspected.navigationTarget}</strong>
438
+ </span>
439
+ </div>
440
+ <button
441
+ type="button"
442
+ className="zelune-interactive"
443
+ onClick={() => onJumpToScreen(inspected.navigationTarget!)}
444
+ style={{
445
+ backgroundColor: '#10B981',
446
+ color: '#052E16',
447
+ border: 'none',
448
+ borderRadius: 5,
449
+ padding: '4px 8px',
450
+ fontSize: 11,
451
+ fontWeight: 700,
452
+ cursor: 'pointer',
453
+ }}
454
+ >
455
+ Focus Target →
456
+ </button>
457
+ </div>
458
+ )}
459
+
460
+ <div className="zelune-inspector-body" style={{ display: 'flex', flexDirection: 'column' }}>
461
+ {/* SECTION 1: Text Copy Editor */}
462
+ {(inspected.isTextEditable || inspected.hasDynamicText) && (
463
+ <section style={{ display: 'flex', flexDirection: 'column', gap: 7 }}>
464
+ <div
465
+ style={{
466
+ display: 'flex',
467
+ alignItems: 'center',
468
+ gap: 6,
469
+ fontSize: 10.5,
470
+ fontFamily: 'var(--font-mono)',
471
+ fontWeight: 600,
472
+ color: 'var(--studio-text-muted)',
473
+ letterSpacing: '0.06em',
474
+ }}
475
+ >
476
+ <Type size={12} color="#38BDF8" />
477
+ <span>TEXT COPY</span>
478
+ </div>
479
+
480
+ {inspected.hasDynamicText ? (
481
+ <div
482
+ style={{
483
+ padding: 10,
484
+ backgroundColor: 'var(--studio-surface-2)',
485
+ border: '1px solid var(--studio-border-subtle)',
486
+ borderRadius: 7,
487
+ fontSize: 11.5,
488
+ color: '#CBD5E1',
489
+ lineHeight: 1.5,
490
+ }}
491
+ >
492
+ Contains dynamic JSX expression (<code style={{ color: '#38BDF8' }}>{'{...}'}</code>). Protected from static overwrite—use <strong>Ask Antigravity</strong> below to refactor expression logic.
493
+ </div>
494
+ ) : (
495
+ <textarea
496
+ rows={2}
497
+ value={localText}
498
+ onChange={(e) => handleTextChange(e.target.value)}
499
+ placeholder="Enter text content..."
500
+ className="zelune-input"
501
+ style={{
502
+ width: '100%',
503
+ backgroundColor: 'var(--studio-canvas-bg)',
504
+ border: '1px solid var(--studio-border-subtle)',
505
+ borderRadius: 7,
506
+ paddingLeft: 10,
507
+ paddingRight: 10,
508
+ color: '#F8FAFC',
509
+ fontSize: 12.5,
510
+ resize: 'vertical',
511
+ fontFamily: 'var(--font-sans)',
512
+ }}
513
+ />
514
+ )}
515
+ </section>
516
+ )}
517
+
518
+ {/* SECTION 2: Colors (Background & Text/Foreground) */}
519
+ <section style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
520
+ <div
521
+ style={{
522
+ display: 'flex',
523
+ alignItems: 'center',
524
+ gap: 6,
525
+ fontSize: 10.5,
526
+ fontFamily: 'var(--font-mono)',
527
+ fontWeight: 600,
528
+ color: 'var(--studio-text-muted)',
529
+ letterSpacing: '0.06em',
530
+ }}
531
+ >
532
+ <Palette size={12} color="#38BDF8" />
533
+ <span>COLORS &amp; SURFACES</span>
534
+ </div>
535
+
536
+ {/* Background Color */}
537
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
538
+ <label style={{ fontSize: 11.5, color: '#CBD5E1' }}>Background Color</label>
539
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
540
+ <input
541
+ type="color"
542
+ value={
543
+ typeof localStyles.backgroundColor === 'string' &&
544
+ localStyles.backgroundColor.startsWith('#') &&
545
+ localStyles.backgroundColor.length === 7
546
+ ? localStyles.backgroundColor
547
+ : '#13161E'
548
+ }
549
+ onChange={(e) => handleStyleChange('backgroundColor', e.target.value)}
550
+ style={{
551
+ width: 30,
552
+ height: 30,
553
+ border: '1px solid var(--studio-border-subtle)',
554
+ borderRadius: 6,
555
+ backgroundColor: 'transparent',
556
+ cursor: 'pointer',
557
+ }}
558
+ />
559
+ <input
560
+ type="text"
561
+ value={String(localStyles.backgroundColor ?? '')}
562
+ placeholder="#13161E"
563
+ onChange={(e) => handleStyleChange('backgroundColor', e.target.value)}
564
+ className="zelune-input tabular-nums"
565
+ style={{
566
+ flex: 1,
567
+ backgroundColor: 'var(--studio-canvas-bg)',
568
+ border: '1px solid var(--studio-border-subtle)',
569
+ borderRadius: 6,
570
+ paddingLeft: 10,
571
+ paddingRight: 10,
572
+ color: '#F8FAFC',
573
+ fontSize: 12,
574
+ fontFamily: 'var(--font-mono)',
575
+ }}
576
+ />
577
+ </div>
578
+ <div style={{ display: 'flex', gap: 5, flexWrap: 'wrap', marginTop: 2 }}>
579
+ {COLOR_SWATCHES.map((hex) => (
580
+ <button
581
+ key={`bg-${hex}`}
582
+ type="button"
583
+ className="zelune-interactive"
584
+ onClick={() => handleStyleChange('backgroundColor', hex)}
585
+ title={`Set backgroundColor to ${hex}`}
586
+ style={{
587
+ width: 20,
588
+ height: 20,
589
+ borderRadius: 5,
590
+ backgroundColor: hex,
591
+ border:
592
+ localStyles.backgroundColor === hex
593
+ ? '2px solid #38BDF8'
594
+ : '1px solid rgba(255, 255, 255, 0.14)',
595
+ cursor: 'pointer',
596
+ }}
597
+ />
598
+ ))}
599
+ </div>
600
+ </div>
601
+
602
+ {/* Text Color */}
603
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
604
+ <label style={{ fontSize: 11.5, color: '#CBD5E1' }}>Text Color</label>
605
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
606
+ <input
607
+ type="color"
608
+ value={
609
+ typeof localStyles.color === 'string' &&
610
+ localStyles.color.startsWith('#') &&
611
+ localStyles.color.length === 7
612
+ ? localStyles.color
613
+ : '#F8FAFC'
614
+ }
615
+ onChange={(e) => handleStyleChange('color', e.target.value)}
616
+ style={{
617
+ width: 30,
618
+ height: 30,
619
+ border: '1px solid var(--studio-border-subtle)',
620
+ borderRadius: 6,
621
+ backgroundColor: 'transparent',
622
+ cursor: 'pointer',
623
+ }}
624
+ />
625
+ <input
626
+ type="text"
627
+ value={String(localStyles.color ?? '')}
628
+ placeholder="#F8FAFC"
629
+ onChange={(e) => handleStyleChange('color', e.target.value)}
630
+ className="zelune-input tabular-nums"
631
+ style={{
632
+ flex: 1,
633
+ backgroundColor: 'var(--studio-canvas-bg)',
634
+ border: '1px solid var(--studio-border-subtle)',
635
+ borderRadius: 6,
636
+ paddingLeft: 10,
637
+ paddingRight: 10,
638
+ color: '#F8FAFC',
639
+ fontSize: 12,
640
+ fontFamily: 'var(--font-mono)',
641
+ }}
642
+ />
643
+ </div>
644
+ </div>
645
+ </section>
646
+
647
+ {/* SECTION 3: Typography */}
648
+ <section style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
649
+ <div
650
+ style={{
651
+ display: 'flex',
652
+ alignItems: 'center',
653
+ gap: 6,
654
+ fontSize: 10.5,
655
+ fontFamily: 'var(--font-mono)',
656
+ fontWeight: 600,
657
+ color: 'var(--studio-text-muted)',
658
+ letterSpacing: '0.06em',
659
+ }}
660
+ >
661
+ <Type size={12} color="#38BDF8" />
662
+ <span>TYPOGRAPHY</span>
663
+ </div>
664
+
665
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
666
+ <div>
667
+ <label style={{ fontSize: 11, color: 'var(--studio-text-secondary)', display: 'block', marginBottom: 4 }}>
668
+ Font Size (px)
669
+ </label>
670
+ <input
671
+ type="number"
672
+ value={localStyles.fontSize ?? ''}
673
+ placeholder="15"
674
+ onChange={(e) => handleNumericStyleChange('fontSize', e.target.value)}
675
+ className="zelune-input tabular-nums"
676
+ style={{
677
+ width: '100%',
678
+ backgroundColor: 'var(--studio-canvas-bg)',
679
+ border: '1px solid var(--studio-border-subtle)',
680
+ borderRadius: 6,
681
+ paddingLeft: 8,
682
+ paddingRight: 8,
683
+ color: '#F8FAFC',
684
+ fontSize: 12,
685
+ fontFamily: 'var(--font-mono)',
686
+ }}
687
+ />
688
+ </div>
689
+
690
+ <div>
691
+ <label style={{ fontSize: 11, color: 'var(--studio-text-secondary)', display: 'block', marginBottom: 4 }}>
692
+ Font Weight
693
+ </label>
694
+ <select
695
+ value={String(localStyles.fontWeight ?? '600')}
696
+ onChange={(e) => handleStyleChange('fontWeight', e.target.value)}
697
+ className="zelune-input tabular-nums"
698
+ style={{
699
+ width: '100%',
700
+ backgroundColor: 'var(--studio-canvas-bg)',
701
+ border: '1px solid var(--studio-border-subtle)',
702
+ borderRadius: 6,
703
+ paddingLeft: 8,
704
+ paddingRight: 8,
705
+ color: '#F8FAFC',
706
+ fontSize: 12,
707
+ fontFamily: 'var(--font-mono)',
708
+ }}
709
+ >
710
+ {FONT_WEIGHTS.map((w) => (
711
+ <option key={w} value={w}>
712
+ {w}
713
+ </option>
714
+ ))}
715
+ </select>
716
+ </div>
717
+ </div>
718
+ </section>
719
+
720
+ {/* SECTION 4: Layout, Spacing & Radius */}
721
+ <section style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
722
+ <div
723
+ style={{
724
+ display: 'flex',
725
+ alignItems: 'center',
726
+ gap: 6,
727
+ fontSize: 10.5,
728
+ fontFamily: 'var(--font-mono)',
729
+ fontWeight: 600,
730
+ color: 'var(--studio-text-muted)',
731
+ letterSpacing: '0.06em',
732
+ }}
733
+ >
734
+ <Box size={12} color="#38BDF8" />
735
+ <span>SPACING &amp; GEOMETRY</span>
736
+ </div>
737
+
738
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
739
+ {[
740
+ { key: 'borderRadius', label: 'Corner Radius' },
741
+ { key: 'padding', label: 'Padding (All)' },
742
+ { key: 'paddingHorizontal', label: 'Padding X' },
743
+ { key: 'paddingVertical', label: 'Padding Y' },
744
+ { key: 'gap', label: 'Flex Gap' },
745
+ { key: 'borderWidth', label: 'Border Width' },
746
+ ].map(({ key, label }) => (
747
+ <div key={key}>
748
+ <label style={{ fontSize: 11, color: 'var(--studio-text-secondary)', display: 'block', marginBottom: 4 }}>
749
+ {label}
750
+ </label>
751
+ <input
752
+ type="number"
753
+ value={localStyles[key] ?? ''}
754
+ placeholder="—"
755
+ onChange={(e) => handleNumericStyleChange(key, e.target.value)}
756
+ className="zelune-input tabular-nums"
757
+ style={{
758
+ width: '100%',
759
+ backgroundColor: 'var(--studio-canvas-bg)',
760
+ border: '1px solid var(--studio-border-subtle)',
761
+ borderRadius: 6,
762
+ paddingLeft: 8,
763
+ paddingRight: 8,
764
+ color: '#F8FAFC',
765
+ fontSize: 12,
766
+ fontFamily: 'var(--font-mono)',
767
+ }}
768
+ />
769
+ </div>
770
+ ))}
771
+ </div>
772
+ </section>
773
+
774
+ {/* SECTION 5: Tier-2 Antigravity Structural Refactor Button */}
775
+ <section style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
776
+ <button
777
+ type="button"
778
+ className="zelune-interactive"
779
+ onClick={() => dialogRef.current?.showModal()}
780
+ style={{
781
+ display: 'flex',
782
+ alignItems: 'center',
783
+ justifyContent: 'center',
784
+ gap: 8,
785
+ width: '100%',
786
+ padding: '10px 14px',
787
+ backgroundColor: 'var(--studio-surface-3)',
788
+ color: '#F8FAFC',
789
+ border: '1px solid rgba(56, 189, 248, 0.42)',
790
+ borderRadius: 8,
791
+ fontSize: 12.5,
792
+ fontWeight: 600,
793
+ cursor: 'pointer',
794
+ boxShadow: 'inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 8px 20px -8px rgba(0, 0, 0, 0.6)',
795
+ }}
796
+ >
797
+ <Sparkles size={14} color="#38BDF8" />
798
+ <span>Ask Antigravity to Refactor</span>
799
+ </button>
800
+ </section>
801
+
802
+ {/* SECTION 6: Live JSX Code Snippet */}
803
+ <section style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
804
+ <div
805
+ style={{
806
+ display: 'flex',
807
+ alignItems: 'center',
808
+ gap: 6,
809
+ fontSize: 10.5,
810
+ fontFamily: 'var(--font-mono)',
811
+ fontWeight: 600,
812
+ color: 'var(--studio-text-muted)',
813
+ letterSpacing: '0.06em',
814
+ }}
815
+ >
816
+ <Code2 size={12} color="#38BDF8" />
817
+ <span>LIVE JSX AST NODE</span>
818
+ </div>
819
+ <pre
820
+ className="zelune-scroller"
821
+ style={{
822
+ backgroundColor: 'var(--studio-canvas-bg)',
823
+ border: '1px solid var(--studio-border-hairline)',
824
+ borderRadius: 7,
825
+ padding: 10,
826
+ fontSize: 11,
827
+ fontFamily: 'var(--font-mono)',
828
+ color: '#CBD5E1',
829
+ overflowX: 'auto',
830
+ margin: 0,
831
+ lineHeight: 1.45,
832
+ }}
833
+ >
834
+ {inspected.codeSnippet}
835
+ </pre>
836
+ </section>
837
+ </div>
838
+
839
+ {/* Native Modal Dialog with closedby="any" + Emil Kowalski --ease-out-expo entry choreography */}
840
+ <dialog
841
+ ref={dialogRef}
842
+ closedby="any"
843
+ aria-labelledby="agyDialogTitle"
844
+ className="agy-modal-dialog"
845
+ >
846
+ <div
847
+ style={{
848
+ backgroundColor: 'var(--studio-surface-1)',
849
+ color: '#F8FAFC',
850
+ border: '1px solid var(--studio-border-strong)',
851
+ borderRadius: 14,
852
+ padding: 22,
853
+ width: 480,
854
+ maxWidth: '92vw',
855
+ display: 'flex',
856
+ flexDirection: 'column',
857
+ gap: 15,
858
+ boxShadow: '0 28px 60px -12px rgba(0, 0, 0, 0.85)',
859
+ }}
860
+ >
861
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
862
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
863
+ <Sparkles size={16} color="#38BDF8" />
864
+ <h2
865
+ id="agyDialogTitle"
866
+ className="balance-heading"
867
+ style={{ margin: 0, fontSize: 15, fontWeight: 700, letterSpacing: '-0.015em' }}
868
+ >
869
+ Ask Google Antigravity — Structural AST Edit
870
+ </h2>
871
+ </div>
872
+ <button
873
+ type="button"
874
+ className="zelune-interactive"
875
+ onClick={() => dialogRef.current?.close()}
876
+ style={{
877
+ background: 'transparent',
878
+ border: '1px solid transparent',
879
+ borderRadius: 5,
880
+ color: '#94A3B8',
881
+ cursor: 'pointer',
882
+ padding: 4,
883
+ }}
884
+ >
885
+ <X size={16} />
886
+ </button>
887
+ </div>
888
+
889
+ <p style={{ margin: 0, fontSize: 12.5, color: '#94A3B8', lineHeight: 1.55 }}>
890
+ Describe the structural or stateful change you want for{' '}
891
+ <code style={{ color: '#38BDF8', fontFamily: 'var(--font-mono)' }}>&lt;{inspected.tagName}&gt;</code> in{' '}
892
+ <code className="tabular-nums" style={{ color: '#A5B4FC', fontFamily: 'var(--font-mono)' }}>
893
+ {inspected.file}:{inspected.line}
894
+ </code>
895
+ .
896
+ </p>
897
+
898
+ <textarea
899
+ rows={3}
900
+ value={agentInstruction}
901
+ onChange={(e) => setAgentInstruction(e.target.value)}
902
+ placeholder="e.g., Convert this spec grid into an asymmetric 2-column acoustic telemetry readout..."
903
+ style={{
904
+ width: '100%',
905
+ backgroundColor: 'var(--studio-canvas-bg)',
906
+ border: '1px solid var(--studio-border-subtle)',
907
+ borderRadius: 8,
908
+ padding: 11,
909
+ color: '#F8FAFC',
910
+ fontSize: 12.5,
911
+ fontFamily: 'var(--font-sans)',
912
+ }}
913
+ />
914
+
915
+ <div
916
+ className="zelune-scroller"
917
+ style={{
918
+ backgroundColor: 'var(--studio-canvas-bg)',
919
+ border: '1px solid var(--studio-border-hairline)',
920
+ borderRadius: 7,
921
+ padding: 10,
922
+ maxHeight: 150,
923
+ overflowY: 'auto',
924
+ fontSize: 11,
925
+ fontFamily: 'var(--font-mono)',
926
+ color: '#94A3B8',
927
+ whiteSpace: 'pre-wrap',
928
+ }}
929
+ >
930
+ {buildAntigravityPrompt()}
931
+ </div>
932
+
933
+ <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
934
+ <button
935
+ type="button"
936
+ className="zelune-interactive"
937
+ onClick={() => dialogRef.current?.close()}
938
+ style={{
939
+ padding: '8px 13px',
940
+ borderRadius: 7,
941
+ border: '1px solid var(--studio-border-subtle)',
942
+ backgroundColor: 'var(--studio-surface-2)',
943
+ color: '#CBD5E1',
944
+ fontSize: 12,
945
+ fontWeight: 600,
946
+ cursor: 'pointer',
947
+ }}
948
+ >
949
+ Cancel
950
+ </button>
951
+ <button
952
+ type="button"
953
+ className="zelune-interactive"
954
+ onClick={handleCopyPrompt}
955
+ style={{
956
+ display: 'inline-flex',
957
+ alignItems: 'center',
958
+ gap: 6,
959
+ padding: '8px 14px',
960
+ borderRadius: 7,
961
+ border: '1px solid rgba(56, 189, 248, 0.45)',
962
+ backgroundColor: 'var(--studio-surface-3)',
963
+ color: '#F8FAFC',
964
+ fontSize: 12,
965
+ fontWeight: 600,
966
+ cursor: 'pointer',
967
+ }}
968
+ >
969
+ {copiedPrompt ? <Check size={14} color="#34D399" /> : <Copy size={14} color="#38BDF8" />}
970
+ <span>{copiedPrompt ? 'Copied Context for Antigravity!' : 'Copy Agent Prompt Context'}</span>
971
+ </button>
972
+ </div>
973
+ </div>
974
+ </dialog>
975
+ </div>
976
+ )}
977
+ </aside>
978
+ );
979
+ };