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.
@@ -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 { InspectedElement, PatchPayload, StyleValue } from '../server/astEngine';
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 &lt;{inspected.tagName}&gt;
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: 14,
237
- fontSize: 12.5,
238
- lineHeight: 1.6,
586
+ padding: 12,
587
+ fontSize: 12,
588
+ lineHeight: 1.5,
239
589
  }}
240
590
  >
241
- Select any node on a <code style={{ color: '#0284C7', fontFamily: 'var(--font-mono)' }}>393 × 852</code>{' '}
242
- artboard to inspect and live-patch its{' '}
243
- <strong style={{ color: 'var(--studio-text-primary)' }}>StyleSheet.create</strong> rules or{' '}
244
- <strong style={{ color: 'var(--studio-text-primary)' }}>static copy</strong> directly in the{' '}
245
- <code style={{ color: '#6366F1', fontFamily: 'var(--font-mono)' }}>.tsx</code> AST.
591
+ Select any node on an artboard to inspect &amp; 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
- <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 4 }}>
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: 'var(--studio-text-muted)',
626
+ color: '#38BDF8',
255
627
  letterSpacing: '0.06em',
256
628
  }}
257
629
  >
258
- DESIGN &amp; SYNC GOVERNANCE
259
- </div>
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 &amp; 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 &amp; Taste Guardrails</strong>: Governed by{' '}
268
- <code style={{ fontFamily: 'var(--font-mono)', color: '#0284C7' }}>PRODUCT.md</code> &amp;{' '}
269
- <code style={{ fontFamily: 'var(--font-mono)', color: '#0284C7' }}>DESIGN.md</code>.
630
+ <Plus size={12} color="#38BDF8" />
631
+ <span>ADD ELEMENTS &amp; 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;