create-visualbuild-app 0.1.0 → 1.0.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/README.md +919 -58
- package/docs/user-guide.md +759 -0
- package/package.json +92 -85
- package/scripts/create-builder-app.mjs +513 -501
- package/scripts/vb-dev.mjs +41 -32
- package/scripts/vb-generate.mjs +332 -224
- package/templates/default/app/.vercelignore +6 -0
- package/templates/default/app/README.md +56 -21
- package/templates/default/app/visualbuild/components.json +3 -0
- package/templates/default/app/visualbuild/config.d.ts +48 -0
- package/templates/default/app/visualbuild.config.ts +38 -0
- package/templates/default/builder/README.md +37 -21
- package/visual-app-builder/server/parseReactPage.ts +776 -571
- package/visual-app-builder/shared/createReactComponentSource.d.mts +2 -0
- package/visual-app-builder/shared/createReactComponentSource.mjs +45 -0
- package/visual-app-builder/shared/generateReactSource.d.mts +57 -37
- package/visual-app-builder/shared/generateReactSource.mjs +608 -443
- package/visual-app-builder/shared/npmBuildCommand.d.mts +17 -0
- package/visual-app-builder/shared/npmBuildCommand.mjs +26 -0
- package/visual-app-builder/shared/syncCustomComponentSource.d.mts +13 -0
- package/visual-app-builder/shared/syncCustomComponentSource.mjs +345 -0
- package/visual-app-builder/shared/vercelDeployment.d.mts +31 -0
- package/visual-app-builder/shared/vercelDeployment.mjs +266 -0
- package/visual-app-builder/shared/visualbuildConfig.d.mts +144 -0
- package/visual-app-builder/shared/visualbuildConfig.mjs +578 -0
- package/visual-app-builder/src/App.tsx +1090 -874
- package/visual-app-builder/src/components/Canvas.tsx +1116 -1059
- package/visual-app-builder/src/components/CodePanel.tsx +1147 -812
- package/visual-app-builder/src/components/ComponentSidebar.tsx +365 -302
- package/visual-app-builder/src/components/DeploymentModal.tsx +295 -0
- package/visual-app-builder/src/components/PageSwitcher.tsx +133 -133
- package/visual-app-builder/src/components/ProjectExplorer.tsx +1054 -1054
- package/visual-app-builder/src/components/PropertiesPanel.tsx +792 -692
- package/visual-app-builder/src/components/Topbar.tsx +257 -128
- package/visual-app-builder/src/data/componentRegistry.tsx +613 -292
- package/visual-app-builder/src/data/tailwindClassCatalog.ts +95 -0
- package/visual-app-builder/src/index.css +383 -111
- package/visual-app-builder/src/stores/useAppStore.ts +2385 -1265
- package/visual-app-builder/src/theme.ts +71 -0
- package/visual-app-builder/src/types/index.ts +350 -261
- package/visual-app-builder/src/utils/codegen.ts +90 -66
- package/visual-app-builder/src/utils/deployment.ts +54 -0
- package/visual-app-builder/src/utils/projectPersistence.ts +218 -177
- package/visual-app-builder/vite.config.ts +1946 -1479
|
@@ -1,812 +1,1147 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
-
import { createPortal } from 'react-dom'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
>
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
return
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
)
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
import { syncCustomComponentRoot } from '../../shared/syncCustomComponentSource.mjs'
|
|
4
|
+
import { useAppStore } from '../stores/useAppStore'
|
|
5
|
+
import {
|
|
6
|
+
getPageSourcePath,
|
|
7
|
+
generateAppCode,
|
|
8
|
+
generateLayoutCode,
|
|
9
|
+
generatePageCode,
|
|
10
|
+
generateProjectSchema,
|
|
11
|
+
} from '../utils/codegen'
|
|
12
|
+
import {
|
|
13
|
+
importProjectPage,
|
|
14
|
+
registerProjectComponent,
|
|
15
|
+
} from '../utils/projectPersistence'
|
|
16
|
+
import {
|
|
17
|
+
loadProjectFile,
|
|
18
|
+
saveProjectFile,
|
|
19
|
+
type ProjectFile,
|
|
20
|
+
} from '../utils/projectFiles'
|
|
21
|
+
import type { Page, VisualNode } from '../types'
|
|
22
|
+
import ProjectExplorer from './ProjectExplorer'
|
|
23
|
+
|
|
24
|
+
export type CodeView = 'page' | 'app' | 'layout' | 'schema' | 'file'
|
|
25
|
+
|
|
26
|
+
interface FileSystemChangeDetail {
|
|
27
|
+
type: 'filesystem-change'
|
|
28
|
+
path: string
|
|
29
|
+
action: 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const externalMoveMatchWindowMs = 1600
|
|
33
|
+
|
|
34
|
+
function getProjectFileName(path: string) {
|
|
35
|
+
return path.split('/').pop() ?? path
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function findVisualNodeById(
|
|
39
|
+
nodes: VisualNode[],
|
|
40
|
+
nodeId: string
|
|
41
|
+
): VisualNode | null {
|
|
42
|
+
for (const node of nodes) {
|
|
43
|
+
if (node.id === nodeId) return node
|
|
44
|
+
const child = findVisualNodeById(node.children, nodeId)
|
|
45
|
+
if (child) return child
|
|
46
|
+
const tree = node.props.componentTree
|
|
47
|
+
if (
|
|
48
|
+
typeof tree === 'object' &&
|
|
49
|
+
tree !== null &&
|
|
50
|
+
Array.isArray((tree as { children?: unknown }).children)
|
|
51
|
+
) {
|
|
52
|
+
const definitionChild = findVisualNodeById(
|
|
53
|
+
(tree as { children: VisualNode[] }).children,
|
|
54
|
+
nodeId
|
|
55
|
+
)
|
|
56
|
+
if (definitionChild) return definitionChild
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function findCustomOwnerNode(
|
|
64
|
+
nodes: VisualNode[],
|
|
65
|
+
nodeId: string,
|
|
66
|
+
names: Set<string>
|
|
67
|
+
): VisualNode | null {
|
|
68
|
+
for (const node of nodes) {
|
|
69
|
+
if (names.has(node.type)) {
|
|
70
|
+
const tree = node.props.componentTree
|
|
71
|
+
const children =
|
|
72
|
+
typeof tree === 'object' &&
|
|
73
|
+
tree !== null &&
|
|
74
|
+
Array.isArray((tree as { children?: unknown }).children)
|
|
75
|
+
? (tree as { children: VisualNode[] }).children
|
|
76
|
+
: []
|
|
77
|
+
if (node.id === nodeId || findVisualNodeById(children, nodeId)) {
|
|
78
|
+
return node
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const owner = findCustomOwnerNode(node.children, nodeId, names)
|
|
82
|
+
if (owner) return owner
|
|
83
|
+
}
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getCustomComponentTreeChildren(node: VisualNode) {
|
|
88
|
+
const tree = node.props.componentTree
|
|
89
|
+
return typeof tree === 'object' &&
|
|
90
|
+
tree !== null &&
|
|
91
|
+
Array.isArray((tree as { children?: unknown }).children)
|
|
92
|
+
? (tree as { children: VisualNode[] }).children
|
|
93
|
+
: []
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export default function CodePanel({
|
|
97
|
+
view,
|
|
98
|
+
onViewChange,
|
|
99
|
+
onPageRegistered,
|
|
100
|
+
onWorkspaceSelectFile,
|
|
101
|
+
filePath,
|
|
102
|
+
embedded = false,
|
|
103
|
+
}: {
|
|
104
|
+
view: CodeView
|
|
105
|
+
onViewChange: (view: CodeView) => void
|
|
106
|
+
onPageRegistered?: (page: Page) => void
|
|
107
|
+
onWorkspaceSelectFile?: (path: string) => void
|
|
108
|
+
filePath?: string | null
|
|
109
|
+
embedded?: boolean
|
|
110
|
+
}) {
|
|
111
|
+
const {
|
|
112
|
+
appShell,
|
|
113
|
+
pages,
|
|
114
|
+
activePageId,
|
|
115
|
+
selectedComponentId,
|
|
116
|
+
projectName,
|
|
117
|
+
registerPage,
|
|
118
|
+
saveProject,
|
|
119
|
+
setActivePage,
|
|
120
|
+
setSelectedComponent,
|
|
121
|
+
customComponents,
|
|
122
|
+
registerCustomComponent,
|
|
123
|
+
projectConfig,
|
|
124
|
+
} = useAppStore()
|
|
125
|
+
const [copied, setCopied] = useState(false)
|
|
126
|
+
const [projectFile, setProjectFile] = useState<ProjectFile | null>(null)
|
|
127
|
+
const [fileContent, setFileContent] = useState('')
|
|
128
|
+
const fileContentOriginRef = useRef<'disk' | 'generated' | 'user'>('disk')
|
|
129
|
+
const [fileLoading, setFileLoading] = useState(false)
|
|
130
|
+
const [fileError, setFileError] = useState<string | null>(null)
|
|
131
|
+
const [fileSaveState, setFileSaveState] = useState<
|
|
132
|
+
'idle' | 'saving' | 'saved'
|
|
133
|
+
>('idle')
|
|
134
|
+
const [fileReloadVersion, setFileReloadVersion] = useState(0)
|
|
135
|
+
const [externalFileChangePending, setExternalFileChangePending] =
|
|
136
|
+
useState(false)
|
|
137
|
+
const fileStateRef = useRef({
|
|
138
|
+
view,
|
|
139
|
+
filePath,
|
|
140
|
+
projectFile,
|
|
141
|
+
fileContent,
|
|
142
|
+
})
|
|
143
|
+
const workspaceSelectFileRef = useRef(onWorkspaceSelectFile)
|
|
144
|
+
const recentAddedFilesRef = useRef(new Map<string, number>())
|
|
145
|
+
const pendingMovedFileRef = useRef<{
|
|
146
|
+
path: string
|
|
147
|
+
fileName: string
|
|
148
|
+
} | null>(null)
|
|
149
|
+
const missingFileTimerRef = useRef<number | null>(null)
|
|
150
|
+
const [registrationOpen, setRegistrationOpen] = useState(false)
|
|
151
|
+
const [registrationKind, setRegistrationKind] =
|
|
152
|
+
useState<'page' | 'component'>('page')
|
|
153
|
+
const [registerName, setRegisterName] = useState('')
|
|
154
|
+
const [registerRoute, setRegisterRoute] = useState('')
|
|
155
|
+
const [registerError, setRegisterError] = useState<string | null>(null)
|
|
156
|
+
const [registerBusy, setRegisterBusy] = useState(false)
|
|
157
|
+
const [workspaceOpen, setWorkspaceOpen] = useState(false)
|
|
158
|
+
const activePage = pages.find((page) => page.id === activePageId) ?? pages[0]
|
|
159
|
+
const selectedCustomSource = useMemo(() => {
|
|
160
|
+
if (!selectedComponentId || !filePath) return null
|
|
161
|
+
|
|
162
|
+
const projectNodes = [
|
|
163
|
+
...(appShell.header ? [appShell.header] : []),
|
|
164
|
+
...pages.flatMap((page) => [page.root, ...page.components]),
|
|
165
|
+
...(appShell.footer ? [appShell.footer] : []),
|
|
166
|
+
]
|
|
167
|
+
const selectedNode = findVisualNodeById(
|
|
168
|
+
projectNodes,
|
|
169
|
+
selectedComponentId
|
|
170
|
+
)
|
|
171
|
+
const customOwner = findCustomOwnerNode(
|
|
172
|
+
projectNodes,
|
|
173
|
+
selectedComponentId,
|
|
174
|
+
new Set(customComponents.map((component) => component.name))
|
|
175
|
+
)
|
|
176
|
+
const definition = customComponents.find(
|
|
177
|
+
(component) =>
|
|
178
|
+
component.name === (customOwner?.type ?? selectedNode?.type)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if (
|
|
182
|
+
!selectedNode ||
|
|
183
|
+
!definition ||
|
|
184
|
+
definition.importPath.replace(/\\/g, '/') !==
|
|
185
|
+
filePath.replace(/\\/g, '/')
|
|
186
|
+
) {
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
definition,
|
|
192
|
+
node: customOwner ?? selectedNode,
|
|
193
|
+
}
|
|
194
|
+
}, [
|
|
195
|
+
appShell,
|
|
196
|
+
customComponents,
|
|
197
|
+
filePath,
|
|
198
|
+
pages,
|
|
199
|
+
selectedComponentId,
|
|
200
|
+
])
|
|
201
|
+
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (!workspaceOpen) return
|
|
204
|
+
|
|
205
|
+
const previousOverflow = document.body.style.overflow
|
|
206
|
+
const closeOnEscape = (event: KeyboardEvent) => {
|
|
207
|
+
if (event.key === 'Escape') {
|
|
208
|
+
setWorkspaceOpen(false)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
document.body.style.overflow = 'hidden'
|
|
213
|
+
window.addEventListener('keydown', closeOnEscape)
|
|
214
|
+
|
|
215
|
+
return () => {
|
|
216
|
+
document.body.style.overflow = previousOverflow
|
|
217
|
+
window.removeEventListener('keydown', closeOnEscape)
|
|
218
|
+
}
|
|
219
|
+
}, [workspaceOpen])
|
|
220
|
+
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
fileStateRef.current = {
|
|
223
|
+
view,
|
|
224
|
+
filePath,
|
|
225
|
+
projectFile,
|
|
226
|
+
fileContent,
|
|
227
|
+
}
|
|
228
|
+
}, [fileContent, filePath, projectFile, view])
|
|
229
|
+
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
workspaceSelectFileRef.current = onWorkspaceSelectFile
|
|
232
|
+
}, [onWorkspaceSelectFile])
|
|
233
|
+
|
|
234
|
+
useEffect(
|
|
235
|
+
() => () => {
|
|
236
|
+
if (missingFileTimerRef.current !== null) {
|
|
237
|
+
window.clearTimeout(missingFileTimerRef.current)
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
[]
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
if (view !== 'file' || !filePath) return
|
|
245
|
+
let cancelled = false
|
|
246
|
+
|
|
247
|
+
pendingMovedFileRef.current = null
|
|
248
|
+
if (missingFileTimerRef.current !== null) {
|
|
249
|
+
window.clearTimeout(missingFileTimerRef.current)
|
|
250
|
+
missingFileTimerRef.current = null
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
void Promise.resolve().then(async () => {
|
|
254
|
+
if (cancelled) return
|
|
255
|
+
setFileLoading(true)
|
|
256
|
+
setFileError(null)
|
|
257
|
+
setFileSaveState('idle')
|
|
258
|
+
setExternalFileChangePending(false)
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const nextFile = await loadProjectFile(filePath)
|
|
262
|
+
|
|
263
|
+
if (cancelled) return
|
|
264
|
+
setProjectFile(nextFile)
|
|
265
|
+
setFileContent(nextFile.content)
|
|
266
|
+
fileContentOriginRef.current = 'disk'
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (cancelled) return
|
|
269
|
+
setProjectFile(null)
|
|
270
|
+
setFileError(getErrorMessage(error))
|
|
271
|
+
} finally {
|
|
272
|
+
if (!cancelled) setFileLoading(false)
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
return () => {
|
|
277
|
+
cancelled = true
|
|
278
|
+
}
|
|
279
|
+
}, [filePath, fileReloadVersion, view])
|
|
280
|
+
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
const handleFileSystemChange = (event: Event) => {
|
|
283
|
+
const detail = (event as CustomEvent<FileSystemChangeDetail>).detail
|
|
284
|
+
const current = fileStateRef.current
|
|
285
|
+
const normalizedCurrentPath = current.filePath?.replace(/\\/g, '/')
|
|
286
|
+
|
|
287
|
+
if (
|
|
288
|
+
!detail &&
|
|
289
|
+
current.view === 'file' &&
|
|
290
|
+
normalizedCurrentPath
|
|
291
|
+
) {
|
|
292
|
+
const hasLocalEdits =
|
|
293
|
+
fileContentOriginRef.current === 'user' &&
|
|
294
|
+
current.projectFile?.content !== current.fileContent
|
|
295
|
+
|
|
296
|
+
if (hasLocalEdits) {
|
|
297
|
+
setExternalFileChangePending(true)
|
|
298
|
+
} else {
|
|
299
|
+
setFileReloadVersion((version) => version + 1)
|
|
300
|
+
}
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const normalizedEventPath = detail?.path.replace(/\\/g, '/') ?? ''
|
|
305
|
+
const now = Date.now()
|
|
306
|
+
|
|
307
|
+
if (
|
|
308
|
+
!detail ||
|
|
309
|
+
detail.type !== 'filesystem-change' ||
|
|
310
|
+
current.view !== 'file' ||
|
|
311
|
+
!normalizedCurrentPath
|
|
312
|
+
) {
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
for (const [addedPath, addedAt] of recentAddedFilesRef.current) {
|
|
317
|
+
if (now - addedAt > externalMoveMatchWindowMs) {
|
|
318
|
+
recentAddedFilesRef.current.delete(addedPath)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (detail.action === 'add') {
|
|
323
|
+
recentAddedFilesRef.current.set(normalizedEventPath, now)
|
|
324
|
+
const pendingMove = pendingMovedFileRef.current
|
|
325
|
+
|
|
326
|
+
if (
|
|
327
|
+
pendingMove &&
|
|
328
|
+
normalizedEventPath !== pendingMove.path &&
|
|
329
|
+
getProjectFileName(normalizedEventPath) === pendingMove.fileName
|
|
330
|
+
) {
|
|
331
|
+
pendingMovedFileRef.current = null
|
|
332
|
+
if (missingFileTimerRef.current !== null) {
|
|
333
|
+
window.clearTimeout(missingFileTimerRef.current)
|
|
334
|
+
missingFileTimerRef.current = null
|
|
335
|
+
}
|
|
336
|
+
setFileError(null)
|
|
337
|
+
workspaceSelectFileRef.current?.(normalizedEventPath)
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const selectedPathWasRemoved =
|
|
343
|
+
(detail.action === 'unlink' &&
|
|
344
|
+
normalizedEventPath === normalizedCurrentPath) ||
|
|
345
|
+
(detail.action === 'unlinkDir' &&
|
|
346
|
+
normalizedCurrentPath.startsWith(`${normalizedEventPath}/`))
|
|
347
|
+
|
|
348
|
+
if (selectedPathWasRemoved) {
|
|
349
|
+
const hasLocalEdits =
|
|
350
|
+
fileContentOriginRef.current === 'user' &&
|
|
351
|
+
current.projectFile?.content !== current.fileContent
|
|
352
|
+
|
|
353
|
+
if (hasLocalEdits) {
|
|
354
|
+
setExternalFileChangePending(true)
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const fileName = getProjectFileName(normalizedCurrentPath)
|
|
359
|
+
const movedPath = Array.from(
|
|
360
|
+
recentAddedFilesRef.current.entries()
|
|
361
|
+
)
|
|
362
|
+
.filter(
|
|
363
|
+
([addedPath, addedAt]) =>
|
|
364
|
+
addedPath !== normalizedCurrentPath &&
|
|
365
|
+
now - addedAt <= externalMoveMatchWindowMs &&
|
|
366
|
+
getProjectFileName(addedPath) === fileName
|
|
367
|
+
)
|
|
368
|
+
.sort((left, right) => right[1] - left[1])[0]?.[0]
|
|
369
|
+
|
|
370
|
+
if (movedPath) {
|
|
371
|
+
setFileError(null)
|
|
372
|
+
workspaceSelectFileRef.current?.(movedPath)
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
pendingMovedFileRef.current = {
|
|
377
|
+
path: normalizedCurrentPath,
|
|
378
|
+
fileName,
|
|
379
|
+
}
|
|
380
|
+
setFileError(null)
|
|
381
|
+
|
|
382
|
+
if (missingFileTimerRef.current !== null) {
|
|
383
|
+
window.clearTimeout(missingFileTimerRef.current)
|
|
384
|
+
}
|
|
385
|
+
missingFileTimerRef.current = window.setTimeout(() => {
|
|
386
|
+
if (pendingMovedFileRef.current?.path !== normalizedCurrentPath) {
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
pendingMovedFileRef.current = null
|
|
391
|
+
missingFileTimerRef.current = null
|
|
392
|
+
setProjectFile(null)
|
|
393
|
+
setFileError('This file was removed from the project.')
|
|
394
|
+
}, externalMoveMatchWindowMs)
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (normalizedEventPath !== normalizedCurrentPath) {
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const hasLocalEdits =
|
|
403
|
+
fileContentOriginRef.current === 'user' &&
|
|
404
|
+
current.projectFile?.content !== current.fileContent
|
|
405
|
+
|
|
406
|
+
if (hasLocalEdits) {
|
|
407
|
+
setExternalFileChangePending(true)
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
setFileReloadVersion((version) => version + 1)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
window.addEventListener(
|
|
415
|
+
'visualbuild:filesystem-changed',
|
|
416
|
+
handleFileSystemChange
|
|
417
|
+
)
|
|
418
|
+
return () =>
|
|
419
|
+
window.removeEventListener(
|
|
420
|
+
'visualbuild:filesystem-changed',
|
|
421
|
+
handleFileSystemChange
|
|
422
|
+
)
|
|
423
|
+
}, [])
|
|
424
|
+
|
|
425
|
+
const generated = useMemo(() => {
|
|
426
|
+
if (view === 'file') {
|
|
427
|
+
return {
|
|
428
|
+
filename: filePath ?? 'No file selected',
|
|
429
|
+
code: fileContent,
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (view === 'page') {
|
|
434
|
+
return {
|
|
435
|
+
filename: getPageSourcePath(activePage),
|
|
436
|
+
code: generatePageCode(activePage, customComponents),
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (view === 'app') {
|
|
441
|
+
return {
|
|
442
|
+
filename: projectConfig.paths.appFile,
|
|
443
|
+
code: generateAppCode(pages, appShell, {
|
|
444
|
+
appFile: projectConfig.paths.appFile,
|
|
445
|
+
layoutFile: `${projectConfig.paths.layoutsDir}/AppLayout.tsx`,
|
|
446
|
+
emitLayout: projectConfig.generator.emitLayout,
|
|
447
|
+
}),
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (view === 'layout') {
|
|
452
|
+
return {
|
|
453
|
+
filename: `${projectConfig.paths.layoutsDir}/AppLayout.tsx`,
|
|
454
|
+
code: generateLayoutCode(appShell, customComponents, {
|
|
455
|
+
layoutFile: `${projectConfig.paths.layoutsDir}/AppLayout.tsx`,
|
|
456
|
+
}),
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (view === 'schema') {
|
|
461
|
+
return {
|
|
462
|
+
filename: projectConfig.paths.schemaFile,
|
|
463
|
+
code: generateProjectSchema(pages, appShell, projectName),
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
filename: projectConfig.paths.schemaFile,
|
|
469
|
+
code: generateProjectSchema(pages, appShell, projectName),
|
|
470
|
+
}
|
|
471
|
+
}, [
|
|
472
|
+
activePage,
|
|
473
|
+
appShell,
|
|
474
|
+
customComponents,
|
|
475
|
+
fileContent,
|
|
476
|
+
filePath,
|
|
477
|
+
pages,
|
|
478
|
+
projectConfig,
|
|
479
|
+
projectName,
|
|
480
|
+
view,
|
|
481
|
+
])
|
|
482
|
+
|
|
483
|
+
useEffect(() => {
|
|
484
|
+
if (
|
|
485
|
+
view !== 'file' ||
|
|
486
|
+
!filePath ||
|
|
487
|
+
!projectFile ||
|
|
488
|
+
fileLoading ||
|
|
489
|
+
fileContentOriginRef.current === 'user'
|
|
490
|
+
) {
|
|
491
|
+
return
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const normalizedPath = filePath.replace(/\\/g, '/')
|
|
495
|
+
const managedPage = pages.find(
|
|
496
|
+
(page) => getPageSourcePath(page) === normalizedPath
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
if (!managedPage) return
|
|
500
|
+
|
|
501
|
+
const nextContent = generatePageCode(managedPage, customComponents)
|
|
502
|
+
const timer = window.setTimeout(() => {
|
|
503
|
+
setFileContent(nextContent)
|
|
504
|
+
fileContentOriginRef.current =
|
|
505
|
+
nextContent === projectFile.content ? 'disk' : 'generated'
|
|
506
|
+
}, 0)
|
|
507
|
+
|
|
508
|
+
return () => window.clearTimeout(timer)
|
|
509
|
+
}, [
|
|
510
|
+
customComponents,
|
|
511
|
+
fileLoading,
|
|
512
|
+
filePath,
|
|
513
|
+
pages,
|
|
514
|
+
projectFile,
|
|
515
|
+
view,
|
|
516
|
+
])
|
|
517
|
+
|
|
518
|
+
useEffect(() => {
|
|
519
|
+
if (
|
|
520
|
+
view !== 'file' ||
|
|
521
|
+
!filePath ||
|
|
522
|
+
!projectFile ||
|
|
523
|
+
fileLoading ||
|
|
524
|
+
!selectedCustomSource ||
|
|
525
|
+
fileContentOriginRef.current === 'user'
|
|
526
|
+
) {
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
const nextContent = syncCustomComponentRoot(projectFile.content, {
|
|
532
|
+
name: selectedCustomSource.definition.name,
|
|
533
|
+
rootTag:
|
|
534
|
+
typeof selectedCustomSource.node.props.wrapperTag === 'string'
|
|
535
|
+
? selectedCustomSource.node.props.wrapperTag
|
|
536
|
+
: undefined,
|
|
537
|
+
id: selectedCustomSource.node.props.id,
|
|
538
|
+
className: selectedCustomSource.node.props.className,
|
|
539
|
+
attributes: selectedCustomSource.node.props.attributes,
|
|
540
|
+
children: getCustomComponentTreeChildren(
|
|
541
|
+
selectedCustomSource.node
|
|
542
|
+
),
|
|
543
|
+
})
|
|
544
|
+
const timer = window.setTimeout(() => {
|
|
545
|
+
setFileError(null)
|
|
546
|
+
setFileContent(nextContent)
|
|
547
|
+
fileContentOriginRef.current =
|
|
548
|
+
nextContent === projectFile.content ? 'disk' : 'generated'
|
|
549
|
+
}, 0)
|
|
550
|
+
|
|
551
|
+
return () => window.clearTimeout(timer)
|
|
552
|
+
} catch (error) {
|
|
553
|
+
const timer = window.setTimeout(
|
|
554
|
+
() => setFileError(getErrorMessage(error)),
|
|
555
|
+
0
|
|
556
|
+
)
|
|
557
|
+
return () => window.clearTimeout(timer)
|
|
558
|
+
}
|
|
559
|
+
}, [
|
|
560
|
+
fileLoading,
|
|
561
|
+
filePath,
|
|
562
|
+
projectFile,
|
|
563
|
+
selectedCustomSource,
|
|
564
|
+
view,
|
|
565
|
+
])
|
|
566
|
+
|
|
567
|
+
const copyCode = () => {
|
|
568
|
+
void navigator.clipboard?.writeText(generated.code)
|
|
569
|
+
setCopied(true)
|
|
570
|
+
window.setTimeout(() => setCopied(false), 1000)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const saveFile = async () => {
|
|
574
|
+
if (!filePath || !projectFile || projectFile.isBinary || projectFile.isTooLarge) {
|
|
575
|
+
return
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
setFileSaveState('saving')
|
|
579
|
+
setFileError(null)
|
|
580
|
+
|
|
581
|
+
try {
|
|
582
|
+
if (selectedCustomSource) {
|
|
583
|
+
const contentOrigin = fileContentOriginRef.current
|
|
584
|
+
await saveProject()
|
|
585
|
+
|
|
586
|
+
if (contentOrigin === 'user') {
|
|
587
|
+
await saveProjectFile(filePath, fileContent)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const savedFile = await loadProjectFile(filePath)
|
|
591
|
+
setProjectFile(savedFile)
|
|
592
|
+
setFileContent(savedFile.content)
|
|
593
|
+
} else {
|
|
594
|
+
await saveProjectFile(filePath, fileContent)
|
|
595
|
+
setProjectFile({
|
|
596
|
+
...projectFile,
|
|
597
|
+
content: fileContent,
|
|
598
|
+
size: new Blob([fileContent]).size,
|
|
599
|
+
})
|
|
600
|
+
}
|
|
601
|
+
fileContentOriginRef.current = 'disk'
|
|
602
|
+
setExternalFileChangePending(false)
|
|
603
|
+
setFileSaveState('saved')
|
|
604
|
+
window.setTimeout(() => setFileSaveState('idle'), 1200)
|
|
605
|
+
} catch (error) {
|
|
606
|
+
setFileError(getErrorMessage(error))
|
|
607
|
+
setFileSaveState('idle')
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const isFileDirty =
|
|
612
|
+
view === 'file' && projectFile?.content !== fileContent
|
|
613
|
+
const normalizedFilePath = filePath?.replace(/\\/g, '/') ?? ''
|
|
614
|
+
const canRegisterFile =
|
|
615
|
+
view === 'file' &&
|
|
616
|
+
Boolean(projectFile) &&
|
|
617
|
+
normalizedFilePath.startsWith(`${projectConfig.paths.sourceDir}/`) &&
|
|
618
|
+
/\.(jsx|tsx)$/i.test(normalizedFilePath) &&
|
|
619
|
+
!pages.some((page) => getPageSourcePath(page) === normalizedFilePath) &&
|
|
620
|
+
!customComponents.some(
|
|
621
|
+
(component) => component.importPath === normalizedFilePath
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
const openRegistration = () => {
|
|
625
|
+
const name = derivePageName(normalizedFilePath)
|
|
626
|
+
|
|
627
|
+
setRegistrationKind('page')
|
|
628
|
+
setRegisterName(name)
|
|
629
|
+
setRegisterRoute(derivePageRoute(name))
|
|
630
|
+
setRegisterError(null)
|
|
631
|
+
setRegistrationOpen(true)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const registerFileAsPage = async () => {
|
|
635
|
+
const name = registerName.trim()
|
|
636
|
+
const routeInput = registerRoute.trim()
|
|
637
|
+
const route = routeInput.startsWith('/') ? routeInput : `/${routeInput}`
|
|
638
|
+
|
|
639
|
+
if (!filePath || !name || !routeInput) {
|
|
640
|
+
setRegisterError('Enter a page name and route')
|
|
641
|
+
return
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (
|
|
645
|
+
pages.some((page) => page.name.toLowerCase() === name.toLowerCase())
|
|
646
|
+
) {
|
|
647
|
+
setRegisterError('Page name already exists')
|
|
648
|
+
return
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (pages.some((page) => page.route === route)) {
|
|
652
|
+
setRegisterError('Route already exists')
|
|
653
|
+
return
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
setRegisterBusy(true)
|
|
657
|
+
setRegisterError(null)
|
|
658
|
+
|
|
659
|
+
try {
|
|
660
|
+
if (isFileDirty) {
|
|
661
|
+
await saveProjectFile(filePath, fileContent)
|
|
662
|
+
setProjectFile((current) =>
|
|
663
|
+
current
|
|
664
|
+
? {
|
|
665
|
+
...current,
|
|
666
|
+
content: fileContent,
|
|
667
|
+
size: new Blob([fileContent]).size,
|
|
668
|
+
}
|
|
669
|
+
: current
|
|
670
|
+
)
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const page = await importProjectPage(filePath, name, route)
|
|
674
|
+
registerPage(page)
|
|
675
|
+
setActivePage(page.id)
|
|
676
|
+
await saveProject()
|
|
677
|
+
onViewChange('page')
|
|
678
|
+
onPageRegistered?.(page)
|
|
679
|
+
setRegistrationOpen(false)
|
|
680
|
+
} catch (error) {
|
|
681
|
+
setRegisterError(getErrorMessage(error))
|
|
682
|
+
} finally {
|
|
683
|
+
setRegisterBusy(false)
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const registerFileAsComponent = async () => {
|
|
688
|
+
if (!filePath) return
|
|
689
|
+
|
|
690
|
+
setRegisterBusy(true)
|
|
691
|
+
setRegisterError(null)
|
|
692
|
+
|
|
693
|
+
try {
|
|
694
|
+
if (isFileDirty) {
|
|
695
|
+
await saveProjectFile(filePath, fileContent)
|
|
696
|
+
setProjectFile((current) =>
|
|
697
|
+
current
|
|
698
|
+
? {
|
|
699
|
+
...current,
|
|
700
|
+
content: fileContent,
|
|
701
|
+
size: new Blob([fileContent]).size,
|
|
702
|
+
}
|
|
703
|
+
: current
|
|
704
|
+
)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const definition = await registerProjectComponent(filePath)
|
|
708
|
+
registerCustomComponent(definition)
|
|
709
|
+
setRegistrationOpen(false)
|
|
710
|
+
window.dispatchEvent(new Event('visualbuild:filesystem-changed'))
|
|
711
|
+
} catch (error) {
|
|
712
|
+
setRegisterError(getErrorMessage(error))
|
|
713
|
+
} finally {
|
|
714
|
+
setRegisterBusy(false)
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const panelContent = (
|
|
719
|
+
<>
|
|
720
|
+
<div className="h-10 px-3 border-b border-white/[0.06] flex items-center justify-between gap-3">
|
|
721
|
+
<div className="min-w-0">
|
|
722
|
+
<p className="text-[10px] font-semibold uppercase tracking-widest text-gray-500">
|
|
723
|
+
Code
|
|
724
|
+
</p>
|
|
725
|
+
<p className="text-[10px] text-gray-400 truncate">
|
|
726
|
+
{generated.filename}
|
|
727
|
+
</p>
|
|
728
|
+
</div>
|
|
729
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
730
|
+
{!workspaceOpen && (
|
|
731
|
+
<button
|
|
732
|
+
type="button"
|
|
733
|
+
onClick={() => setWorkspaceOpen(true)}
|
|
734
|
+
className="cursor-pointer rounded bg-white/[0.06] px-2 py-1 text-[10px] font-medium text-gray-300 transition-colors hover:bg-white/[0.1] hover:text-white"
|
|
735
|
+
title="Open a larger code workspace"
|
|
736
|
+
>
|
|
737
|
+
Open editor
|
|
738
|
+
</button>
|
|
739
|
+
)}
|
|
740
|
+
{canRegisterFile && (
|
|
741
|
+
<button
|
|
742
|
+
type="button"
|
|
743
|
+
onClick={openRegistration}
|
|
744
|
+
className="cursor-pointer rounded bg-emerald-500/15 px-2 py-1 text-[10px] font-medium text-emerald-300 transition-colors hover:bg-emerald-500/25 hover:text-emerald-200"
|
|
745
|
+
>
|
|
746
|
+
Register
|
|
747
|
+
</button>
|
|
748
|
+
)}
|
|
749
|
+
{view === 'file' && externalFileChangePending && (
|
|
750
|
+
<button
|
|
751
|
+
type="button"
|
|
752
|
+
onClick={() => {
|
|
753
|
+
setExternalFileChangePending(false)
|
|
754
|
+
setFileReloadVersion((version) => version + 1)
|
|
755
|
+
}}
|
|
756
|
+
className="cursor-pointer rounded bg-amber-500/15 px-2 py-1 text-[10px] font-medium text-amber-300 transition-colors hover:bg-amber-500/25 hover:text-amber-200"
|
|
757
|
+
title="Discard unsaved editor text and load the external file version"
|
|
758
|
+
>
|
|
759
|
+
Reload disk
|
|
760
|
+
</button>
|
|
761
|
+
)}
|
|
762
|
+
{view === 'file' && projectFile && !projectFile.isBinary && !projectFile.isTooLarge && (
|
|
763
|
+
<button
|
|
764
|
+
type="button"
|
|
765
|
+
onClick={() => void saveFile()}
|
|
766
|
+
disabled={!isFileDirty || fileSaveState === 'saving'}
|
|
767
|
+
className="cursor-pointer rounded bg-blue-500 px-2 py-1 text-[10px] font-medium text-white transition-colors hover:bg-blue-400 disabled:cursor-default disabled:bg-white/[0.06] disabled:text-gray-500"
|
|
768
|
+
>
|
|
769
|
+
{fileSaveState === 'saving'
|
|
770
|
+
? 'Saving...'
|
|
771
|
+
: fileSaveState === 'saved'
|
|
772
|
+
? 'Saved'
|
|
773
|
+
: 'Save file'}
|
|
774
|
+
</button>
|
|
775
|
+
)}
|
|
776
|
+
<button
|
|
777
|
+
type="button"
|
|
778
|
+
onClick={copyCode}
|
|
779
|
+
className="px-2 py-1 text-[10px] text-gray-300 bg-white/[0.06] hover:bg-white/[0.1] rounded transition-colors cursor-pointer select-none"
|
|
780
|
+
>
|
|
781
|
+
{copied ? 'Copied' : 'Copy'}
|
|
782
|
+
</button>
|
|
783
|
+
</div>
|
|
784
|
+
</div>
|
|
785
|
+
|
|
786
|
+
<div className="px-2 py-2 border-b border-white/[0.06] flex flex-wrap gap-1">
|
|
787
|
+
<CodeTab active={view === 'page'} onClick={() => onViewChange('page')}>
|
|
788
|
+
Page
|
|
789
|
+
</CodeTab>
|
|
790
|
+
<CodeTab active={view === 'app'} onClick={() => onViewChange('app')}>
|
|
791
|
+
App
|
|
792
|
+
</CodeTab>
|
|
793
|
+
{(appShell.header || appShell.footer) && (
|
|
794
|
+
<CodeTab
|
|
795
|
+
active={view === 'layout'}
|
|
796
|
+
onClick={() => onViewChange('layout')}
|
|
797
|
+
>
|
|
798
|
+
Layout
|
|
799
|
+
</CodeTab>
|
|
800
|
+
)}
|
|
801
|
+
<CodeTab
|
|
802
|
+
active={view === 'schema'}
|
|
803
|
+
onClick={() => onViewChange('schema')}
|
|
804
|
+
>
|
|
805
|
+
JSON
|
|
806
|
+
</CodeTab>
|
|
807
|
+
{filePath && (
|
|
808
|
+
<CodeTab active={view === 'file'} onClick={() => onViewChange('file')}>
|
|
809
|
+
File{isFileDirty ? ' *' : ''}
|
|
810
|
+
</CodeTab>
|
|
811
|
+
)}
|
|
812
|
+
</div>
|
|
813
|
+
|
|
814
|
+
{view === 'file' ? (
|
|
815
|
+
<FileEditor
|
|
816
|
+
file={projectFile}
|
|
817
|
+
content={fileContent}
|
|
818
|
+
loading={fileLoading}
|
|
819
|
+
error={fileError}
|
|
820
|
+
onChange={(content) => {
|
|
821
|
+
fileContentOriginRef.current = 'user'
|
|
822
|
+
setFileContent(content)
|
|
823
|
+
}}
|
|
824
|
+
onSave={() => void saveFile()}
|
|
825
|
+
/>
|
|
826
|
+
) : (
|
|
827
|
+
<pre className="flex-1 overflow-auto p-4 text-[11px] leading-5 font-mono text-gray-200 bg-[#0b0d11]">
|
|
828
|
+
<code>{generated.code}</code>
|
|
829
|
+
</pre>
|
|
830
|
+
)}
|
|
831
|
+
{registrationOpen && (
|
|
832
|
+
<RegistrationDialog
|
|
833
|
+
kind={registrationKind}
|
|
834
|
+
componentName={derivePageName(normalizedFilePath)}
|
|
835
|
+
name={registerName}
|
|
836
|
+
route={registerRoute}
|
|
837
|
+
error={registerError}
|
|
838
|
+
busy={registerBusy}
|
|
839
|
+
onKindChange={(kind) => {
|
|
840
|
+
setRegistrationKind(kind)
|
|
841
|
+
setRegisterError(null)
|
|
842
|
+
}}
|
|
843
|
+
onNameChange={setRegisterName}
|
|
844
|
+
onRouteChange={setRegisterRoute}
|
|
845
|
+
onCancel={() => setRegistrationOpen(false)}
|
|
846
|
+
onConfirm={() =>
|
|
847
|
+
void (registrationKind === 'page'
|
|
848
|
+
? registerFileAsPage()
|
|
849
|
+
: registerFileAsComponent())
|
|
850
|
+
}
|
|
851
|
+
/>
|
|
852
|
+
)}
|
|
853
|
+
</>
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
const compactPanel = (
|
|
857
|
+
<aside
|
|
858
|
+
className={`${embedded ? 'h-full w-full' : 'w-[460px] border-l'} relative flex shrink-0 flex-col overflow-hidden border-white/[0.06] bg-[#101318]`}
|
|
859
|
+
onClick={() => setSelectedComponent(null)}
|
|
860
|
+
>
|
|
861
|
+
{panelContent}
|
|
862
|
+
</aside>
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
if (!workspaceOpen) {
|
|
866
|
+
return compactPanel
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
return createPortal(
|
|
870
|
+
<div
|
|
871
|
+
data-vb-editor-chrome
|
|
872
|
+
className="fixed inset-0 z-[140] flex bg-black/70 p-3 backdrop-blur-sm"
|
|
873
|
+
role="dialog"
|
|
874
|
+
aria-modal="true"
|
|
875
|
+
aria-label="VisualBuild code workspace"
|
|
876
|
+
>
|
|
877
|
+
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden rounded-md border border-white/10 bg-[#0b0d11] shadow-2xl">
|
|
878
|
+
<header className="flex h-12 shrink-0 items-center justify-between border-b border-white/[0.08] bg-[#15181e] px-4">
|
|
879
|
+
<div className="min-w-0">
|
|
880
|
+
<p className="text-xs font-semibold text-white">
|
|
881
|
+
VisualBuild Code Editor
|
|
882
|
+
</p>
|
|
883
|
+
<p className="truncate text-[10px] text-gray-500">
|
|
884
|
+
{generated.filename}
|
|
885
|
+
</p>
|
|
886
|
+
</div>
|
|
887
|
+
<button
|
|
888
|
+
type="button"
|
|
889
|
+
onClick={() => setWorkspaceOpen(false)}
|
|
890
|
+
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded text-lg text-gray-400 transition-colors hover:bg-white/[0.08] hover:text-white"
|
|
891
|
+
aria-label="Close code editor"
|
|
892
|
+
title="Close code editor"
|
|
893
|
+
>
|
|
894
|
+
x
|
|
895
|
+
</button>
|
|
896
|
+
</header>
|
|
897
|
+
|
|
898
|
+
<div className="flex min-h-0 flex-1 flex-col sm:flex-row">
|
|
899
|
+
<aside className="flex h-[38%] w-full shrink-0 flex-col overflow-hidden border-b border-white/[0.08] bg-[#101318] sm:h-auto sm:w-[300px] sm:border-r sm:border-b-0">
|
|
900
|
+
<div className="flex h-10 shrink-0 items-center border-b border-white/[0.06] px-3">
|
|
901
|
+
<p className="text-[10px] font-semibold uppercase tracking-widest text-gray-500">
|
|
902
|
+
Project files
|
|
903
|
+
</p>
|
|
904
|
+
</div>
|
|
905
|
+
<div className="min-h-0 flex-1 overflow-hidden">
|
|
906
|
+
<ProjectExplorer
|
|
907
|
+
selectedPath={filePath ?? null}
|
|
908
|
+
onSelectFile={(path) => {
|
|
909
|
+
if (path) onWorkspaceSelectFile?.(path)
|
|
910
|
+
}}
|
|
911
|
+
/>
|
|
912
|
+
</div>
|
|
913
|
+
</aside>
|
|
914
|
+
|
|
915
|
+
<section
|
|
916
|
+
className="relative flex min-w-0 flex-1 flex-col overflow-hidden bg-[#101318]"
|
|
917
|
+
onClick={() => setSelectedComponent(null)}
|
|
918
|
+
>
|
|
919
|
+
{panelContent}
|
|
920
|
+
</section>
|
|
921
|
+
</div>
|
|
922
|
+
</div>
|
|
923
|
+
</div>,
|
|
924
|
+
document.body
|
|
925
|
+
)
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function RegistrationDialog({
|
|
929
|
+
kind,
|
|
930
|
+
componentName,
|
|
931
|
+
name,
|
|
932
|
+
route,
|
|
933
|
+
error,
|
|
934
|
+
busy,
|
|
935
|
+
onKindChange,
|
|
936
|
+
onNameChange,
|
|
937
|
+
onRouteChange,
|
|
938
|
+
onCancel,
|
|
939
|
+
onConfirm,
|
|
940
|
+
}: {
|
|
941
|
+
kind: 'page' | 'component'
|
|
942
|
+
componentName: string
|
|
943
|
+
name: string
|
|
944
|
+
route: string
|
|
945
|
+
error: string | null
|
|
946
|
+
busy: boolean
|
|
947
|
+
onKindChange: (kind: 'page' | 'component') => void
|
|
948
|
+
onNameChange: (value: string) => void
|
|
949
|
+
onRouteChange: (value: string) => void
|
|
950
|
+
onCancel: () => void
|
|
951
|
+
onConfirm: () => void
|
|
952
|
+
}) {
|
|
953
|
+
return (
|
|
954
|
+
<div data-vb-editor-chrome className="absolute inset-0 z-20 flex items-center justify-center bg-black/55 p-5 backdrop-blur-sm">
|
|
955
|
+
<div className="w-full max-w-sm rounded-lg border border-white/10 bg-[#191d24] p-4 shadow-2xl">
|
|
956
|
+
<h2 className="text-sm font-semibold text-white">Register file</h2>
|
|
957
|
+
<p className="mt-1 text-[11px] leading-4 text-gray-400">
|
|
958
|
+
Add this React source file to VisualBuild as a page or component.
|
|
959
|
+
</p>
|
|
960
|
+
<label className="mt-4 block text-[10px] font-semibold uppercase tracking-wider text-gray-500">
|
|
961
|
+
Register as
|
|
962
|
+
<select
|
|
963
|
+
autoFocus
|
|
964
|
+
value={kind}
|
|
965
|
+
onChange={(event) =>
|
|
966
|
+
onKindChange(event.target.value as 'page' | 'component')
|
|
967
|
+
}
|
|
968
|
+
className="mt-1 w-full rounded border border-white/10 bg-[#11151b] px-2.5 py-2 text-xs normal-case tracking-normal text-white outline-none focus:border-blue-400"
|
|
969
|
+
>
|
|
970
|
+
<option value="page">Page</option>
|
|
971
|
+
<option value="component">Component</option>
|
|
972
|
+
</select>
|
|
973
|
+
</label>
|
|
974
|
+
{kind === 'page' ? (
|
|
975
|
+
<>
|
|
976
|
+
<label className="mt-3 block text-[10px] font-semibold uppercase tracking-wider text-gray-500">
|
|
977
|
+
Page name
|
|
978
|
+
<input
|
|
979
|
+
value={name}
|
|
980
|
+
onChange={(event) => onNameChange(event.target.value)}
|
|
981
|
+
className="mt-1 w-full rounded border border-white/10 bg-black/20 px-2.5 py-2 text-xs normal-case tracking-normal text-white outline-none focus:border-blue-400"
|
|
982
|
+
/>
|
|
983
|
+
</label>
|
|
984
|
+
<label className="mt-3 block text-[10px] font-semibold uppercase tracking-wider text-gray-500">
|
|
985
|
+
Route
|
|
986
|
+
<input
|
|
987
|
+
value={route}
|
|
988
|
+
onChange={(event) => onRouteChange(event.target.value)}
|
|
989
|
+
onKeyDown={(event) => {
|
|
990
|
+
if (event.key === 'Enter') onConfirm()
|
|
991
|
+
}}
|
|
992
|
+
className="mt-1 w-full rounded border border-white/10 bg-black/20 px-2.5 py-2 text-xs normal-case tracking-normal text-white outline-none focus:border-blue-400"
|
|
993
|
+
/>
|
|
994
|
+
</label>
|
|
995
|
+
</>
|
|
996
|
+
) : (
|
|
997
|
+
<div className="mt-3 rounded border border-white/[0.08] bg-black/20 px-3 py-2">
|
|
998
|
+
<p className="text-[10px] uppercase tracking-wider text-gray-500">
|
|
999
|
+
Component
|
|
1000
|
+
</p>
|
|
1001
|
+
<p className="mt-1 text-xs font-medium text-white">
|
|
1002
|
+
{componentName}
|
|
1003
|
+
</p>
|
|
1004
|
+
<p className="mt-1 text-[10px] text-gray-500">
|
|
1005
|
+
VisualBuild detects the exported component from the source file.
|
|
1006
|
+
</p>
|
|
1007
|
+
</div>
|
|
1008
|
+
)}
|
|
1009
|
+
{error && <p className="mt-3 text-xs text-red-300">{error}</p>}
|
|
1010
|
+
<div className="mt-4 flex justify-end gap-2">
|
|
1011
|
+
<button
|
|
1012
|
+
type="button"
|
|
1013
|
+
onClick={onCancel}
|
|
1014
|
+
disabled={busy}
|
|
1015
|
+
className="cursor-pointer rounded px-3 py-1.5 text-xs text-gray-400 hover:bg-white/[0.06] hover:text-white disabled:cursor-default"
|
|
1016
|
+
>
|
|
1017
|
+
Keep as file
|
|
1018
|
+
</button>
|
|
1019
|
+
<button
|
|
1020
|
+
type="button"
|
|
1021
|
+
onClick={onConfirm}
|
|
1022
|
+
disabled={busy}
|
|
1023
|
+
className="cursor-pointer rounded bg-blue-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-400 disabled:cursor-default disabled:bg-blue-500/40"
|
|
1024
|
+
>
|
|
1025
|
+
{busy
|
|
1026
|
+
? 'Registering...'
|
|
1027
|
+
: kind === 'page'
|
|
1028
|
+
? 'Register page'
|
|
1029
|
+
: 'Register component'}
|
|
1030
|
+
</button>
|
|
1031
|
+
</div>
|
|
1032
|
+
</div>
|
|
1033
|
+
</div>
|
|
1034
|
+
)
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function FileEditor({
|
|
1038
|
+
file,
|
|
1039
|
+
content,
|
|
1040
|
+
loading,
|
|
1041
|
+
error,
|
|
1042
|
+
onChange,
|
|
1043
|
+
onSave,
|
|
1044
|
+
}: {
|
|
1045
|
+
file: ProjectFile | null
|
|
1046
|
+
content: string
|
|
1047
|
+
loading: boolean
|
|
1048
|
+
error: string | null
|
|
1049
|
+
onChange: (content: string) => void
|
|
1050
|
+
onSave: () => void
|
|
1051
|
+
}) {
|
|
1052
|
+
if (loading) {
|
|
1053
|
+
return (
|
|
1054
|
+
<div className="flex flex-1 items-center justify-center bg-[#0b0d11] text-xs text-gray-500">
|
|
1055
|
+
Opening file...
|
|
1056
|
+
</div>
|
|
1057
|
+
)
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (error) {
|
|
1061
|
+
return (
|
|
1062
|
+
<div className="flex flex-1 items-center justify-center bg-[#0b0d11] p-6 text-center text-xs leading-5 text-red-300">
|
|
1063
|
+
{error}
|
|
1064
|
+
</div>
|
|
1065
|
+
)
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if (file?.isBinary || file?.isTooLarge) {
|
|
1069
|
+
return (
|
|
1070
|
+
<div className="flex flex-1 items-center justify-center bg-[#0b0d11] p-6 text-center text-xs leading-5 text-gray-400">
|
|
1071
|
+
{file.isBinary
|
|
1072
|
+
? 'Binary files cannot be edited in the code panel.'
|
|
1073
|
+
: 'This file is larger than the 2 MB editor limit.'}
|
|
1074
|
+
</div>
|
|
1075
|
+
)
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
return (
|
|
1079
|
+
<textarea
|
|
1080
|
+
value={content}
|
|
1081
|
+
onChange={(event) => onChange(event.target.value)}
|
|
1082
|
+
onKeyDown={(event) => {
|
|
1083
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {
|
|
1084
|
+
event.preventDefault()
|
|
1085
|
+
onSave()
|
|
1086
|
+
}
|
|
1087
|
+
}}
|
|
1088
|
+
spellCheck={false}
|
|
1089
|
+
className="min-h-0 flex-1 resize-none overflow-auto bg-[#0b0d11] p-4 font-mono text-[11px] leading-5 text-gray-200 outline-none"
|
|
1090
|
+
aria-label="Project file editor"
|
|
1091
|
+
/>
|
|
1092
|
+
)
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function CodeTab({
|
|
1096
|
+
active,
|
|
1097
|
+
children,
|
|
1098
|
+
onClick,
|
|
1099
|
+
}: {
|
|
1100
|
+
active: boolean
|
|
1101
|
+
children: React.ReactNode
|
|
1102
|
+
onClick: () => void
|
|
1103
|
+
}) {
|
|
1104
|
+
return (
|
|
1105
|
+
<button
|
|
1106
|
+
type="button"
|
|
1107
|
+
onClick={onClick}
|
|
1108
|
+
className={`px-2 py-1 text-[10px] rounded transition-colors cursor-pointer select-none ${
|
|
1109
|
+
active
|
|
1110
|
+
? 'bg-accent text-white'
|
|
1111
|
+
: 'text-gray-500 hover:text-gray-200 hover:bg-white/[0.06]'
|
|
1112
|
+
}`}
|
|
1113
|
+
>
|
|
1114
|
+
{children}
|
|
1115
|
+
</button>
|
|
1116
|
+
)
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function getErrorMessage(error: unknown) {
|
|
1120
|
+
return error instanceof Error ? error.message : 'Project file operation failed'
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function derivePageName(filePath: string) {
|
|
1124
|
+
const filename = filePath.split('/').pop() ?? 'Page'
|
|
1125
|
+
const baseName = filename
|
|
1126
|
+
.replace(/\.[^.]+$/, '')
|
|
1127
|
+
.replace(/Page$/i, '')
|
|
1128
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
1129
|
+
.replace(/[-_]+/g, ' ')
|
|
1130
|
+
.trim()
|
|
1131
|
+
|
|
1132
|
+
return baseName
|
|
1133
|
+
.split(/\s+/)
|
|
1134
|
+
.filter(Boolean)
|
|
1135
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
1136
|
+
.join(' ') || 'Page'
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function derivePageRoute(name: string) {
|
|
1140
|
+
const slug = name
|
|
1141
|
+
.trim()
|
|
1142
|
+
.toLowerCase()
|
|
1143
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
1144
|
+
.replace(/^-+|-+$/g, '')
|
|
1145
|
+
|
|
1146
|
+
return slug === 'home' ? '/' : `/${slug || 'page'}`
|
|
1147
|
+
}
|