zelune 1.5.7 → 1.6.1
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/CHANGELOG.md +27 -0
- package/bin/cli.mjs +22 -42
- package/mcp/server.mjs +2228 -0
- package/mcp/server.ts +367 -117
- package/package.json +3 -5
- package/plugin.json +1 -1
- package/plugins/viteReactNativeFallbackPlugin.ts +2 -11
- package/src/canvas/CanvasApp.tsx +174 -3
- package/src/canvas/FlowArrowsLayer.tsx +84 -45
- package/src/canvas/InspectorPanel.tsx +529 -23
- package/src/runtime/MockNavigationContext.tsx +59 -0
- package/src/server/apiMiddleware.ts +48 -0
- package/src/server/astEngine.ts +1291 -33
- package/vite.config.ts +177 -115
|
@@ -11,21 +11,115 @@ import {
|
|
|
11
11
|
Sliders,
|
|
12
12
|
X,
|
|
13
13
|
PanelRightClose,
|
|
14
|
+
Plus,
|
|
15
|
+
Trash2,
|
|
16
|
+
ArrowUp,
|
|
17
|
+
ArrowDown,
|
|
18
|
+
LayoutTemplate,
|
|
19
|
+
MousePointerClick,
|
|
20
|
+
FormInput,
|
|
21
|
+
Tag,
|
|
22
|
+
Minus,
|
|
23
|
+
Columns,
|
|
24
|
+
Heading,
|
|
25
|
+
AlignLeft,
|
|
14
26
|
} from 'lucide-react';
|
|
15
|
-
import type {
|
|
27
|
+
import type {
|
|
28
|
+
InspectedElement,
|
|
29
|
+
PatchPayload,
|
|
30
|
+
ReorderPayload,
|
|
31
|
+
InsertElementPayload,
|
|
32
|
+
InsertElementPreset,
|
|
33
|
+
DuplicateElementPayload,
|
|
34
|
+
DeleteElementPayload,
|
|
35
|
+
ScreenMetadata,
|
|
36
|
+
StyleValue,
|
|
37
|
+
} from '../server/astEngine';
|
|
16
38
|
|
|
17
39
|
interface InspectorPanelProps {
|
|
18
40
|
inspected: InspectedElement | null;
|
|
41
|
+
screens?: ScreenMetadata[];
|
|
42
|
+
activeScreenId?: string | null;
|
|
19
43
|
isOpen: boolean;
|
|
20
44
|
isLoading: boolean;
|
|
21
45
|
isSaving: boolean;
|
|
22
46
|
statusMessage: string | null;
|
|
23
47
|
onPatch: (payload: PatchPayload) => Promise<void>;
|
|
48
|
+
onInsert?: (payload: InsertElementPayload) => Promise<void>;
|
|
49
|
+
onDuplicate?: (payload: DuplicateElementPayload) => Promise<void>;
|
|
50
|
+
onDelete?: (payload: DeleteElementPayload) => Promise<void>;
|
|
51
|
+
onReorder?: (payload: ReorderPayload) => Promise<void>;
|
|
52
|
+
onSelectScreen?: (screenId: string) => void;
|
|
24
53
|
onClearSelection: () => void;
|
|
25
54
|
onCollapse: () => void;
|
|
26
55
|
onJumpToScreen: (routeName: string) => void;
|
|
27
56
|
}
|
|
28
57
|
|
|
58
|
+
const INSERT_PRESETS: Array<{
|
|
59
|
+
id: InsertElementPreset;
|
|
60
|
+
title: string;
|
|
61
|
+
subtitle: string;
|
|
62
|
+
tagBadge: string;
|
|
63
|
+
icon: React.ComponentType<{ size?: number; color?: string }>;
|
|
64
|
+
}> = [
|
|
65
|
+
{
|
|
66
|
+
id: 'button',
|
|
67
|
+
title: 'Button',
|
|
68
|
+
subtitle: 'CTA + Optional Flow',
|
|
69
|
+
tagBadge: '<TouchableOpacity>',
|
|
70
|
+
icon: MousePointerClick,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'section',
|
|
74
|
+
title: 'Section Card',
|
|
75
|
+
subtitle: 'Title + Body Card',
|
|
76
|
+
tagBadge: '<View>',
|
|
77
|
+
icon: LayoutTemplate,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'row',
|
|
81
|
+
title: 'Row Item',
|
|
82
|
+
subtitle: 'Horizontal Flex Row',
|
|
83
|
+
tagBadge: '<View row>',
|
|
84
|
+
icon: Columns,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'heading',
|
|
88
|
+
title: 'Heading',
|
|
89
|
+
subtitle: 'Bold Title Copy',
|
|
90
|
+
tagBadge: '<Text>',
|
|
91
|
+
icon: Heading,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'text',
|
|
95
|
+
title: 'Paragraph',
|
|
96
|
+
subtitle: 'Supporting Copy',
|
|
97
|
+
tagBadge: '<Text>',
|
|
98
|
+
icon: AlignLeft,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'input',
|
|
102
|
+
title: 'Text Input',
|
|
103
|
+
subtitle: 'Form Input Field',
|
|
104
|
+
tagBadge: '<TextInput>',
|
|
105
|
+
icon: FormInput,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'badge',
|
|
109
|
+
title: 'Badge Pill',
|
|
110
|
+
subtitle: 'Status Tag Pill',
|
|
111
|
+
tagBadge: '<View>',
|
|
112
|
+
icon: Tag,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 'divider',
|
|
116
|
+
title: 'Divider',
|
|
117
|
+
subtitle: '1px Hairline Rule',
|
|
118
|
+
tagBadge: '<View>',
|
|
119
|
+
icon: Minus,
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
|
|
29
123
|
const COLOR_SWATCHES = [
|
|
30
124
|
'#0A0B0E',
|
|
31
125
|
'#13161E',
|
|
@@ -42,11 +136,18 @@ const FONT_WEIGHTS = ['400', '500', '600', '700', '800'];
|
|
|
42
136
|
|
|
43
137
|
export const InspectorPanel: React.FC<InspectorPanelProps> = ({
|
|
44
138
|
inspected,
|
|
139
|
+
screens = [],
|
|
140
|
+
activeScreenId = null,
|
|
45
141
|
isOpen,
|
|
46
142
|
isLoading,
|
|
47
143
|
isSaving,
|
|
48
144
|
statusMessage,
|
|
49
145
|
onPatch,
|
|
146
|
+
onInsert,
|
|
147
|
+
onDuplicate,
|
|
148
|
+
onDelete,
|
|
149
|
+
onReorder,
|
|
150
|
+
onSelectScreen,
|
|
50
151
|
onClearSelection,
|
|
51
152
|
onCollapse,
|
|
52
153
|
onJumpToScreen,
|
|
@@ -56,9 +157,258 @@ export const InspectorPanel: React.FC<InspectorPanelProps> = ({
|
|
|
56
157
|
const [agentInstruction, setAgentInstruction] = useState('');
|
|
57
158
|
const [copiedPrompt, setCopiedPrompt] = useState(false);
|
|
58
159
|
|
|
160
|
+
// Add Component / Section builder state
|
|
161
|
+
const [showInsertDrawer, setShowInsertDrawer] = useState(false);
|
|
162
|
+
const [insertPlacement, setInsertPlacement] = useState<'after' | 'before' | 'inside'>('after');
|
|
163
|
+
const [insertLabel, setInsertLabel] = useState('');
|
|
164
|
+
const [insertNavigateTo, setInsertNavigateTo] = useState('');
|
|
165
|
+
|
|
59
166
|
const dialogRef = useRef<HTMLDialogElement | null>(null);
|
|
60
167
|
const debounceTimerRef = useRef<number | null>(null);
|
|
61
168
|
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
const handleOpenInsert = () => {
|
|
171
|
+
setShowInsertDrawer(true);
|
|
172
|
+
};
|
|
173
|
+
window.addEventListener('zelune:open-insert-palette', handleOpenInsert);
|
|
174
|
+
return () => window.removeEventListener('zelune:open-insert-palette', handleOpenInsert);
|
|
175
|
+
}, []);
|
|
176
|
+
|
|
177
|
+
const activeScreenObj =
|
|
178
|
+
screens.find((s) => s.id === activeScreenId) ?? screens[0] ?? null;
|
|
179
|
+
|
|
180
|
+
const handleTriggerInsert = (preset: InsertElementPreset) => {
|
|
181
|
+
if (!onInsert) return;
|
|
182
|
+
const targetFile = inspected?.file ?? activeScreenObj?.filePath;
|
|
183
|
+
if (!targetFile) return;
|
|
184
|
+
|
|
185
|
+
void onInsert({
|
|
186
|
+
file: targetFile,
|
|
187
|
+
astPath: inspected?.astPath,
|
|
188
|
+
placement: inspected ? insertPlacement : 'inside',
|
|
189
|
+
preset,
|
|
190
|
+
label: insertLabel.trim() || undefined,
|
|
191
|
+
navigateTo: preset === 'button' && insertNavigateTo ? insertNavigateTo : undefined,
|
|
192
|
+
});
|
|
193
|
+
setInsertLabel('');
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const renderInsertPaletteContent = (isCompactDrawer: boolean) => (
|
|
197
|
+
<div
|
|
198
|
+
style={{
|
|
199
|
+
display: 'flex',
|
|
200
|
+
flexDirection: 'column',
|
|
201
|
+
gap: 10,
|
|
202
|
+
padding: isCompactDrawer ? 12 : 0,
|
|
203
|
+
backgroundColor: isCompactDrawer ? 'var(--studio-surface-2)' : 'transparent',
|
|
204
|
+
borderRadius: isCompactDrawer ? 9 : 0,
|
|
205
|
+
border: isCompactDrawer ? '1px solid rgba(56, 189, 248, 0.3)' : 'none',
|
|
206
|
+
}}
|
|
207
|
+
>
|
|
208
|
+
{/* Target Screen or Placement Selector */}
|
|
209
|
+
{inspected ? (
|
|
210
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
|
|
211
|
+
<label
|
|
212
|
+
style={{
|
|
213
|
+
fontSize: 10,
|
|
214
|
+
fontFamily: 'var(--font-mono)',
|
|
215
|
+
fontWeight: 600,
|
|
216
|
+
color: 'var(--studio-text-muted)',
|
|
217
|
+
letterSpacing: '0.05em',
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
220
|
+
PLACEMENT RELATIVE TO <{inspected.tagName}>
|
|
221
|
+
</label>
|
|
222
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 4 }}>
|
|
223
|
+
{(['after', 'before', 'inside'] as const).map((place) => (
|
|
224
|
+
<button
|
|
225
|
+
key={place}
|
|
226
|
+
type="button"
|
|
227
|
+
className="zelune-interactive"
|
|
228
|
+
onClick={() => setInsertPlacement(place)}
|
|
229
|
+
style={{
|
|
230
|
+
padding: '5px 6px',
|
|
231
|
+
borderRadius: 6,
|
|
232
|
+
border:
|
|
233
|
+
insertPlacement === place
|
|
234
|
+
? '1px solid #38BDF8'
|
|
235
|
+
: '1px solid var(--studio-border-subtle)',
|
|
236
|
+
backgroundColor:
|
|
237
|
+
insertPlacement === place
|
|
238
|
+
? 'rgba(56, 189, 248, 0.14)'
|
|
239
|
+
: 'var(--studio-surface-1)',
|
|
240
|
+
color:
|
|
241
|
+
insertPlacement === place
|
|
242
|
+
? 'var(--studio-text-primary)'
|
|
243
|
+
: 'var(--studio-text-secondary)',
|
|
244
|
+
fontSize: 11,
|
|
245
|
+
fontWeight: 600,
|
|
246
|
+
fontFamily: 'var(--font-mono)',
|
|
247
|
+
textTransform: 'capitalize',
|
|
248
|
+
cursor: 'pointer',
|
|
249
|
+
}}
|
|
250
|
+
>
|
|
251
|
+
{place}
|
|
252
|
+
</button>
|
|
253
|
+
))}
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
) : (
|
|
257
|
+
screens.length > 0 && (
|
|
258
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
|
|
259
|
+
<label
|
|
260
|
+
style={{
|
|
261
|
+
fontSize: 10,
|
|
262
|
+
fontFamily: 'var(--font-mono)',
|
|
263
|
+
fontWeight: 600,
|
|
264
|
+
color: 'var(--studio-text-muted)',
|
|
265
|
+
letterSpacing: '0.05em',
|
|
266
|
+
}}
|
|
267
|
+
>
|
|
268
|
+
TARGET ARTBOARD SCREEN
|
|
269
|
+
</label>
|
|
270
|
+
<select
|
|
271
|
+
value={activeScreenObj?.id ?? ''}
|
|
272
|
+
onChange={(e) => onSelectScreen?.(e.target.value)}
|
|
273
|
+
style={{
|
|
274
|
+
padding: '7px 9px',
|
|
275
|
+
borderRadius: 7,
|
|
276
|
+
border: '1px solid var(--studio-border-subtle)',
|
|
277
|
+
backgroundColor: 'var(--studio-surface-2)',
|
|
278
|
+
color: 'var(--studio-text-primary)',
|
|
279
|
+
fontSize: 12,
|
|
280
|
+
fontFamily: 'var(--font-mono)',
|
|
281
|
+
}}
|
|
282
|
+
>
|
|
283
|
+
{screens.map((s) => (
|
|
284
|
+
<option key={s.id} value={s.id}>
|
|
285
|
+
{s.name} ({s.filePath})
|
|
286
|
+
</option>
|
|
287
|
+
))}
|
|
288
|
+
</select>
|
|
289
|
+
</div>
|
|
290
|
+
)
|
|
291
|
+
)}
|
|
292
|
+
|
|
293
|
+
{/* Optional Custom Label & Navigation Link */}
|
|
294
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
|
|
295
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
296
|
+
<label
|
|
297
|
+
style={{
|
|
298
|
+
fontSize: 10,
|
|
299
|
+
fontFamily: 'var(--font-mono)',
|
|
300
|
+
color: 'var(--studio-text-muted)',
|
|
301
|
+
}}
|
|
302
|
+
>
|
|
303
|
+
CUSTOM LABEL / COPY
|
|
304
|
+
</label>
|
|
305
|
+
<input
|
|
306
|
+
type="text"
|
|
307
|
+
value={insertLabel}
|
|
308
|
+
onChange={(e) => setInsertLabel(e.target.value)}
|
|
309
|
+
placeholder="e.g. Book Now"
|
|
310
|
+
style={{
|
|
311
|
+
padding: '6px 8px',
|
|
312
|
+
borderRadius: 6,
|
|
313
|
+
border: '1px solid var(--studio-border-subtle)',
|
|
314
|
+
backgroundColor: 'var(--studio-surface-1)',
|
|
315
|
+
color: 'var(--studio-text-primary)',
|
|
316
|
+
fontSize: 11.5,
|
|
317
|
+
}}
|
|
318
|
+
/>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
322
|
+
<label
|
|
323
|
+
style={{
|
|
324
|
+
fontSize: 10,
|
|
325
|
+
fontFamily: 'var(--font-mono)',
|
|
326
|
+
color: 'var(--studio-text-muted)',
|
|
327
|
+
}}
|
|
328
|
+
>
|
|
329
|
+
BUTTON FLOW LINK
|
|
330
|
+
</label>
|
|
331
|
+
<select
|
|
332
|
+
value={insertNavigateTo}
|
|
333
|
+
onChange={(e) => setInsertNavigateTo(e.target.value)}
|
|
334
|
+
style={{
|
|
335
|
+
padding: '6px 7px',
|
|
336
|
+
borderRadius: 6,
|
|
337
|
+
border: '1px solid var(--studio-border-subtle)',
|
|
338
|
+
backgroundColor: 'var(--studio-surface-1)',
|
|
339
|
+
color: 'var(--studio-text-primary)',
|
|
340
|
+
fontSize: 11.5,
|
|
341
|
+
fontFamily: 'var(--font-mono)',
|
|
342
|
+
}}
|
|
343
|
+
>
|
|
344
|
+
<option value="">None</option>
|
|
345
|
+
{screens.map((s) => (
|
|
346
|
+
<option key={s.id} value={s.id}>
|
|
347
|
+
→ {s.id}
|
|
348
|
+
</option>
|
|
349
|
+
))}
|
|
350
|
+
</select>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
|
|
354
|
+
{/* 2-Column Component Preset Grid */}
|
|
355
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 7 }}>
|
|
356
|
+
{INSERT_PRESETS.map((preset) => {
|
|
357
|
+
const IconComp = preset.icon;
|
|
358
|
+
return (
|
|
359
|
+
<button
|
|
360
|
+
key={preset.id}
|
|
361
|
+
type="button"
|
|
362
|
+
className="zelune-interactive"
|
|
363
|
+
disabled={isSaving}
|
|
364
|
+
onClick={() => handleTriggerInsert(preset.id)}
|
|
365
|
+
style={{
|
|
366
|
+
display: 'flex',
|
|
367
|
+
flexDirection: 'column',
|
|
368
|
+
alignItems: 'flex-start',
|
|
369
|
+
gap: 4,
|
|
370
|
+
padding: '9px 10px',
|
|
371
|
+
borderRadius: 8,
|
|
372
|
+
border: '1px solid var(--studio-border-subtle)',
|
|
373
|
+
backgroundColor: 'var(--studio-surface-1)',
|
|
374
|
+
color: 'var(--studio-text-primary)',
|
|
375
|
+
cursor: isSaving ? 'wait' : 'pointer',
|
|
376
|
+
textAlign: 'left',
|
|
377
|
+
}}
|
|
378
|
+
>
|
|
379
|
+
<div
|
|
380
|
+
style={{
|
|
381
|
+
display: 'flex',
|
|
382
|
+
alignItems: 'center',
|
|
383
|
+
justifyContent: 'space-between',
|
|
384
|
+
width: '100%',
|
|
385
|
+
}}
|
|
386
|
+
>
|
|
387
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12, fontWeight: 600 }}>
|
|
388
|
+
<IconComp size={13} color="#38BDF8" />
|
|
389
|
+
<span>{preset.title}</span>
|
|
390
|
+
</span>
|
|
391
|
+
<Plus size={11} color="var(--studio-text-muted)" />
|
|
392
|
+
</div>
|
|
393
|
+
<span style={{ fontSize: 10.5, color: 'var(--studio-text-secondary)' }}>
|
|
394
|
+
{preset.subtitle}
|
|
395
|
+
</span>
|
|
396
|
+
<span
|
|
397
|
+
style={{
|
|
398
|
+
fontSize: 9.5,
|
|
399
|
+
fontFamily: 'var(--font-mono)',
|
|
400
|
+
color: '#6366F1',
|
|
401
|
+
}}
|
|
402
|
+
>
|
|
403
|
+
{preset.tagBadge}
|
|
404
|
+
</span>
|
|
405
|
+
</button>
|
|
406
|
+
);
|
|
407
|
+
})}
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
);
|
|
411
|
+
|
|
62
412
|
useEffect(() => {
|
|
63
413
|
if (!inspected) {
|
|
64
414
|
setLocalText('');
|
|
@@ -233,41 +583,54 @@ export const InspectorPanel: React.FC<InspectorPanelProps> = ({
|
|
|
233
583
|
backgroundColor: 'var(--studio-surface-2)',
|
|
234
584
|
border: '1px solid var(--studio-border-hairline)',
|
|
235
585
|
borderRadius: 10,
|
|
236
|
-
padding:
|
|
237
|
-
fontSize: 12
|
|
238
|
-
lineHeight: 1.
|
|
586
|
+
padding: 12,
|
|
587
|
+
fontSize: 12,
|
|
588
|
+
lineHeight: 1.5,
|
|
239
589
|
}}
|
|
240
590
|
>
|
|
241
|
-
Select any node on
|
|
242
|
-
|
|
243
|
-
<strong style={{ color: 'var(--studio-text-primary)' }}>
|
|
244
|
-
|
|
245
|
-
<code style={{ color: '#6366F1', fontFamily: 'var(--font-mono)' }}>.tsx</code> AST.
|
|
591
|
+
Select any node on an artboard to inspect & live-patch its{' '}
|
|
592
|
+
<strong style={{ color: 'var(--studio-text-primary)' }}>StyleSheet</strong> or{' '}
|
|
593
|
+
<strong style={{ color: 'var(--studio-text-primary)' }}>copy</strong>, or add new
|
|
594
|
+
buttons, sections, and inputs below:
|
|
246
595
|
</div>
|
|
247
596
|
|
|
248
|
-
|
|
597
|
+
{(isSaving || statusMessage) && (
|
|
249
598
|
<div
|
|
250
599
|
style={{
|
|
600
|
+
padding: '7px 12px',
|
|
601
|
+
backgroundColor: 'var(--studio-badge-indigo-bg)',
|
|
602
|
+
border: '1px solid var(--studio-badge-indigo-border)',
|
|
603
|
+
borderRadius: 7,
|
|
604
|
+
fontSize: 11,
|
|
605
|
+
fontFamily: 'var(--font-mono)',
|
|
606
|
+
color: 'var(--studio-badge-indigo-text)',
|
|
607
|
+
display: 'flex',
|
|
608
|
+
alignItems: 'center',
|
|
609
|
+
justifyContent: 'space-between',
|
|
610
|
+
}}
|
|
611
|
+
>
|
|
612
|
+
<span>{isSaving ? 'Writing AST to .tsx...' : statusMessage}</span>
|
|
613
|
+
<span style={{ color: 'var(--studio-accent-emerald)', fontWeight: 600 }}>● Sync</span>
|
|
614
|
+
</div>
|
|
615
|
+
)}
|
|
616
|
+
|
|
617
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
618
|
+
<div
|
|
619
|
+
style={{
|
|
620
|
+
display: 'flex',
|
|
621
|
+
alignItems: 'center',
|
|
622
|
+
gap: 6,
|
|
251
623
|
fontSize: 10.5,
|
|
252
624
|
fontFamily: 'var(--font-mono)',
|
|
253
625
|
fontWeight: 600,
|
|
254
|
-
color: '
|
|
626
|
+
color: '#38BDF8',
|
|
255
627
|
letterSpacing: '0.06em',
|
|
256
628
|
}}
|
|
257
629
|
>
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
<div style={{ fontSize: 12, color: 'var(--studio-text-secondary)', lineHeight: 1.5 }}>
|
|
261
|
-
• <strong style={{ color: 'var(--studio-text-primary)' }}>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: 'var(--studio-text-primary)' }}>Tier 2 (Antigravity Bridge)</strong>: Dispatch structural refactors with exact file & AST path coordinates.
|
|
265
|
-
</div>
|
|
266
|
-
<div style={{ fontSize: 12, color: 'var(--studio-text-secondary)', lineHeight: 1.5 }}>
|
|
267
|
-
• <strong style={{ color: 'var(--studio-text-primary)' }}>Impeccable & Taste Guardrails</strong>: Governed by{' '}
|
|
268
|
-
<code style={{ fontFamily: 'var(--font-mono)', color: '#0284C7' }}>PRODUCT.md</code> &{' '}
|
|
269
|
-
<code style={{ fontFamily: 'var(--font-mono)', color: '#0284C7' }}>DESIGN.md</code>.
|
|
630
|
+
<Plus size={12} color="#38BDF8" />
|
|
631
|
+
<span>ADD ELEMENTS & SECTIONS</span>
|
|
270
632
|
</div>
|
|
633
|
+
{renderInsertPaletteContent(false)}
|
|
271
634
|
</div>
|
|
272
635
|
</div>
|
|
273
636
|
|
|
@@ -418,6 +781,149 @@ export const InspectorPanel: React.FC<InspectorPanelProps> = ({
|
|
|
418
781
|
</div>
|
|
419
782
|
)}
|
|
420
783
|
|
|
784
|
+
{/* Structural Element Action Bar: + Add Element, Duplicate, Reorder Up/Down, Delete */}
|
|
785
|
+
<div
|
|
786
|
+
style={{
|
|
787
|
+
padding: '8px 14px',
|
|
788
|
+
borderBottom: '1px solid var(--studio-border-hairline)',
|
|
789
|
+
backgroundColor: 'var(--studio-surface-1)',
|
|
790
|
+
display: 'flex',
|
|
791
|
+
flexDirection: 'column',
|
|
792
|
+
gap: 8,
|
|
793
|
+
}}
|
|
794
|
+
>
|
|
795
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 5 }}>
|
|
796
|
+
<button
|
|
797
|
+
type="button"
|
|
798
|
+
className="zelune-interactive"
|
|
799
|
+
onClick={() => setShowInsertDrawer((prev) => !prev)}
|
|
800
|
+
style={{
|
|
801
|
+
display: 'inline-flex',
|
|
802
|
+
alignItems: 'center',
|
|
803
|
+
gap: 5,
|
|
804
|
+
padding: '5px 9px',
|
|
805
|
+
borderRadius: 6,
|
|
806
|
+
border: showInsertDrawer
|
|
807
|
+
? '1px solid #38BDF8'
|
|
808
|
+
: '1px solid rgba(56, 189, 248, 0.35)',
|
|
809
|
+
backgroundColor: showInsertDrawer
|
|
810
|
+
? 'rgba(56, 189, 248, 0.18)'
|
|
811
|
+
: 'rgba(56, 189, 248, 0.08)',
|
|
812
|
+
color: '#38BDF8',
|
|
813
|
+
fontSize: 11,
|
|
814
|
+
fontWeight: 600,
|
|
815
|
+
cursor: 'pointer',
|
|
816
|
+
}}
|
|
817
|
+
>
|
|
818
|
+
<Plus size={12} />
|
|
819
|
+
<span>Add Element</span>
|
|
820
|
+
</button>
|
|
821
|
+
|
|
822
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
823
|
+
<button
|
|
824
|
+
type="button"
|
|
825
|
+
className="zelune-interactive"
|
|
826
|
+
disabled={isSaving || inspected.astPath === '0'}
|
|
827
|
+
onClick={() =>
|
|
828
|
+
onDuplicate?.({ file: inspected.file, astPath: inspected.astPath })
|
|
829
|
+
}
|
|
830
|
+
title="Duplicate Selected Element"
|
|
831
|
+
style={{
|
|
832
|
+
display: 'inline-flex',
|
|
833
|
+
alignItems: 'center',
|
|
834
|
+
gap: 4,
|
|
835
|
+
padding: '5px 7px',
|
|
836
|
+
borderRadius: 6,
|
|
837
|
+
border: '1px solid var(--studio-border-subtle)',
|
|
838
|
+
backgroundColor: 'var(--studio-surface-2)',
|
|
839
|
+
color: 'var(--studio-text-secondary)',
|
|
840
|
+
fontSize: 11,
|
|
841
|
+
cursor: inspected.astPath === '0' ? 'not-allowed' : 'pointer',
|
|
842
|
+
}}
|
|
843
|
+
>
|
|
844
|
+
<Copy size={11} />
|
|
845
|
+
<span>Clone</span>
|
|
846
|
+
</button>
|
|
847
|
+
|
|
848
|
+
<button
|
|
849
|
+
type="button"
|
|
850
|
+
className="zelune-interactive"
|
|
851
|
+
disabled={isSaving || inspected.astPath === '0'}
|
|
852
|
+
onClick={() =>
|
|
853
|
+
onReorder?.({
|
|
854
|
+
file: inspected.file,
|
|
855
|
+
astPath: inspected.astPath,
|
|
856
|
+
direction: 'up',
|
|
857
|
+
})
|
|
858
|
+
}
|
|
859
|
+
title="Move Element Up"
|
|
860
|
+
style={{
|
|
861
|
+
padding: '5px 6px',
|
|
862
|
+
borderRadius: 6,
|
|
863
|
+
border: '1px solid var(--studio-border-subtle)',
|
|
864
|
+
backgroundColor: 'var(--studio-surface-2)',
|
|
865
|
+
color: 'var(--studio-text-secondary)',
|
|
866
|
+
cursor: inspected.astPath === '0' ? 'not-allowed' : 'pointer',
|
|
867
|
+
display: 'inline-flex',
|
|
868
|
+
alignItems: 'center',
|
|
869
|
+
}}
|
|
870
|
+
>
|
|
871
|
+
<ArrowUp size={12} />
|
|
872
|
+
</button>
|
|
873
|
+
|
|
874
|
+
<button
|
|
875
|
+
type="button"
|
|
876
|
+
className="zelune-interactive"
|
|
877
|
+
disabled={isSaving || inspected.astPath === '0'}
|
|
878
|
+
onClick={() =>
|
|
879
|
+
onReorder?.({
|
|
880
|
+
file: inspected.file,
|
|
881
|
+
astPath: inspected.astPath,
|
|
882
|
+
direction: 'down',
|
|
883
|
+
})
|
|
884
|
+
}
|
|
885
|
+
title="Move Element Down"
|
|
886
|
+
style={{
|
|
887
|
+
padding: '5px 6px',
|
|
888
|
+
borderRadius: 6,
|
|
889
|
+
border: '1px solid var(--studio-border-subtle)',
|
|
890
|
+
backgroundColor: 'var(--studio-surface-2)',
|
|
891
|
+
color: 'var(--studio-text-secondary)',
|
|
892
|
+
cursor: inspected.astPath === '0' ? 'not-allowed' : 'pointer',
|
|
893
|
+
display: 'inline-flex',
|
|
894
|
+
alignItems: 'center',
|
|
895
|
+
}}
|
|
896
|
+
>
|
|
897
|
+
<ArrowDown size={12} />
|
|
898
|
+
</button>
|
|
899
|
+
|
|
900
|
+
<button
|
|
901
|
+
type="button"
|
|
902
|
+
className="zelune-interactive"
|
|
903
|
+
disabled={isSaving || inspected.astPath === '0'}
|
|
904
|
+
onClick={() =>
|
|
905
|
+
onDelete?.({ file: inspected.file, astPath: inspected.astPath })
|
|
906
|
+
}
|
|
907
|
+
title="Delete Element from .tsx (Cmd+Z to Undo)"
|
|
908
|
+
style={{
|
|
909
|
+
padding: '5px 6px',
|
|
910
|
+
borderRadius: 6,
|
|
911
|
+
border: '1px solid rgba(244, 63, 94, 0.35)',
|
|
912
|
+
backgroundColor: 'rgba(244, 63, 94, 0.1)',
|
|
913
|
+
color: '#F43F5E',
|
|
914
|
+
cursor: inspected.astPath === '0' ? 'not-allowed' : 'pointer',
|
|
915
|
+
display: 'inline-flex',
|
|
916
|
+
alignItems: 'center',
|
|
917
|
+
}}
|
|
918
|
+
>
|
|
919
|
+
<Trash2 size={12} />
|
|
920
|
+
</button>
|
|
921
|
+
</div>
|
|
922
|
+
</div>
|
|
923
|
+
|
|
924
|
+
{showInsertDrawer && renderInsertPaletteContent(true)}
|
|
925
|
+
</div>
|
|
926
|
+
|
|
421
927
|
{/* Navigation Flow Link Banner if element triggers navigation */}
|
|
422
928
|
{inspected.navigationTarget && (
|
|
423
929
|
<div
|
|
@@ -13,6 +13,65 @@ import {
|
|
|
13
13
|
} from 'react-native';
|
|
14
14
|
import { withNativeWind, resolveTailwindClassNameToStyle } from './nativewindShim';
|
|
15
15
|
|
|
16
|
+
declare global {
|
|
17
|
+
interface Window {
|
|
18
|
+
__ZELUNE_PUBLIC_ENV__?: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (typeof window !== 'undefined') {
|
|
23
|
+
const baseEnv: Record<string, string> = window.__ZELUNE_PUBLIC_ENV__ ?? {
|
|
24
|
+
NODE_ENV: 'development',
|
|
25
|
+
EXPO_PUBLIC_PLATFORM: 'web',
|
|
26
|
+
};
|
|
27
|
+
const envProxy = new Proxy(baseEnv, {
|
|
28
|
+
get(target, prop) {
|
|
29
|
+
if (typeof prop !== 'string') return Reflect.get(target, prop);
|
|
30
|
+
if (prop in target && target[prop] !== undefined && target[prop] !== '') {
|
|
31
|
+
return target[prop];
|
|
32
|
+
}
|
|
33
|
+
return 'zelune-preview';
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const createAssetStub = (): unknown => {
|
|
38
|
+
const fn = () => proxy;
|
|
39
|
+
const proxy: unknown = new Proxy(fn, {
|
|
40
|
+
get(_t, prop) {
|
|
41
|
+
if (prop === Symbol.toPrimitive) return (hint: string) => (hint === 'number' ? 0 : '');
|
|
42
|
+
if (prop === 'valueOf') return () => 0;
|
|
43
|
+
if (prop === 'toString') return () => '';
|
|
44
|
+
if (prop === 'uri' || prop === 'default') return '';
|
|
45
|
+
return proxy;
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
return proxy;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const win = window as unknown as Record<string, unknown>;
|
|
52
|
+
win.global = window;
|
|
53
|
+
win.__DEV__ = true;
|
|
54
|
+
if (!win.process || typeof win.process !== 'object') {
|
|
55
|
+
win.process = {
|
|
56
|
+
env: envProxy,
|
|
57
|
+
platform: 'browser',
|
|
58
|
+
version: '',
|
|
59
|
+
cwd: () => '/',
|
|
60
|
+
};
|
|
61
|
+
} else {
|
|
62
|
+
(win.process as Record<string, unknown>).env = envProxy;
|
|
63
|
+
}
|
|
64
|
+
const reqProp = ['req', 'uire'].join('');
|
|
65
|
+
if (typeof win[reqProp] !== 'function') {
|
|
66
|
+
win[reqProp] = (modId: unknown) => {
|
|
67
|
+
if (typeof modId === 'string' && /\.(png|jpe?g|gif|webp|svg|avif|bmp)$/i.test(modId)) {
|
|
68
|
+
return { uri: modId, default: modId };
|
|
69
|
+
}
|
|
70
|
+
return createAssetStub();
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
16
75
|
export interface NavigationController {
|
|
17
76
|
navigate: (targetRoute: string, params?: Record<string, unknown>) => void;
|
|
18
77
|
goBack: () => void;
|