@jsonpages/cli 3.0.46 → 3.0.48
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/assets/src_tenant_alpha.sh +902 -204
- package/package.json +1 -1
|
@@ -294,22 +294,639 @@ export const NotFound: React.FC = () => {
|
|
|
294
294
|
|
|
295
295
|
END_OF_FILE_CONTENT
|
|
296
296
|
mkdir -p "src/components/SaveDrawer"
|
|
297
|
+
echo "Creating src/components/SaveDrawer/DeployConnector.tsx..."
|
|
298
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/DeployConnector.tsx"
|
|
299
|
+
import type { StepState } from './types';
|
|
300
|
+
|
|
301
|
+
interface DeployConnectorProps {
|
|
302
|
+
fromState: StepState;
|
|
303
|
+
toState: StepState;
|
|
304
|
+
color: string;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function DeployConnector({ fromState, toState, color }: DeployConnectorProps) {
|
|
308
|
+
const filled = fromState === 'done' && toState === 'done';
|
|
309
|
+
const filling = fromState === 'done' && toState === 'active';
|
|
310
|
+
const lit = filled || filling;
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<div className="relative flex-1 z-0" style={{ height: 2, marginTop: -24 }}>
|
|
314
|
+
<div className="absolute inset-0 bg-white/8 rounded-full" />
|
|
315
|
+
|
|
316
|
+
<div
|
|
317
|
+
className="absolute top-0 left-0 bottom-0 rounded-full"
|
|
318
|
+
style={{
|
|
319
|
+
background: `linear-gradient(90deg, ${color}cc, ${color}66)`,
|
|
320
|
+
width: filled ? '100%' : filling ? '100%' : '0%',
|
|
321
|
+
transition: filling ? 'width 2s cubic-bezier(0.4,0,0.2,1)' : 'none',
|
|
322
|
+
boxShadow: lit ? `0 0 8px ${color}77` : 'none',
|
|
323
|
+
}}
|
|
324
|
+
/>
|
|
325
|
+
|
|
326
|
+
{filling && (
|
|
327
|
+
<div
|
|
328
|
+
className="absolute rounded-full"
|
|
329
|
+
style={{
|
|
330
|
+
top: '50%',
|
|
331
|
+
transform: 'translateY(-50%)',
|
|
332
|
+
width: 10,
|
|
333
|
+
height: 10,
|
|
334
|
+
background: color,
|
|
335
|
+
boxShadow: `0 0 14px ${color}, 0 0 28px ${color}88`,
|
|
336
|
+
animation: 'save-orb-travel 2s cubic-bezier(0.4,0,0.6,1) forwards',
|
|
337
|
+
}}
|
|
338
|
+
/>
|
|
339
|
+
)}
|
|
340
|
+
</div>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
END_OF_FILE_CONTENT
|
|
345
|
+
echo "Creating src/components/SaveDrawer/DeployNode.tsx..."
|
|
346
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/DeployNode.tsx"
|
|
347
|
+
import type { DeployStep, StepState } from './types';
|
|
348
|
+
import { cn } from '@/lib/utils';
|
|
349
|
+
|
|
350
|
+
interface DeployNodeProps {
|
|
351
|
+
step: DeployStep;
|
|
352
|
+
state: StepState;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function DeployNode({ step, state }: DeployNodeProps) {
|
|
356
|
+
const isActive = state === 'active';
|
|
357
|
+
const isDone = state === 'done';
|
|
358
|
+
const isPending = state === 'pending';
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<div className="flex flex-col items-center gap-2.5 relative z-10">
|
|
362
|
+
<div
|
|
363
|
+
className={cn(
|
|
364
|
+
'relative flex h-12 w-12 items-center justify-center rounded-full border transition-all duration-500',
|
|
365
|
+
isPending && 'border-white/8 bg-white/[0.02]',
|
|
366
|
+
isActive && 'border',
|
|
367
|
+
isDone && 'border-0'
|
|
368
|
+
)}
|
|
369
|
+
style={{
|
|
370
|
+
background: isDone ? step.color : isActive ? 'rgba(0,0,0,0.5)' : undefined,
|
|
371
|
+
borderColor: isActive ? `${step.color}80` : undefined,
|
|
372
|
+
boxShadow: isDone
|
|
373
|
+
? `0 0 20px ${step.color}55, 0 0 40px ${step.color}22`
|
|
374
|
+
: isActive
|
|
375
|
+
? `0 0 14px ${step.color}33`
|
|
376
|
+
: undefined,
|
|
377
|
+
animation: isActive ? 'save-node-glow 2s ease infinite' : undefined,
|
|
378
|
+
['--glow-color' as string]: step.color,
|
|
379
|
+
}}
|
|
380
|
+
>
|
|
381
|
+
{isDone && (
|
|
382
|
+
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" aria-label="Done">
|
|
383
|
+
<path
|
|
384
|
+
className="stroke-dash-30 save-check-draw"
|
|
385
|
+
d="M5 13l4 4L19 7"
|
|
386
|
+
stroke="#0a0f1a"
|
|
387
|
+
strokeWidth="2.5"
|
|
388
|
+
strokeLinecap="round"
|
|
389
|
+
strokeLinejoin="round"
|
|
390
|
+
/>
|
|
391
|
+
</svg>
|
|
392
|
+
)}
|
|
393
|
+
|
|
394
|
+
{isActive && (
|
|
395
|
+
<span
|
|
396
|
+
className="text-lg leading-none"
|
|
397
|
+
style={{
|
|
398
|
+
color: step.color,
|
|
399
|
+
animation: 'save-glyph-rotate 9s linear infinite',
|
|
400
|
+
display: 'inline-block',
|
|
401
|
+
}}
|
|
402
|
+
aria-hidden
|
|
403
|
+
>
|
|
404
|
+
{step.glyph}
|
|
405
|
+
</span>
|
|
406
|
+
)}
|
|
407
|
+
|
|
408
|
+
{isPending && (
|
|
409
|
+
<span className="text-lg leading-none text-white/15" aria-hidden>
|
|
410
|
+
{step.glyph}
|
|
411
|
+
</span>
|
|
412
|
+
)}
|
|
413
|
+
|
|
414
|
+
{isActive && (
|
|
415
|
+
<span
|
|
416
|
+
className="absolute rounded-full border"
|
|
417
|
+
style={{
|
|
418
|
+
inset: -7,
|
|
419
|
+
borderColor: `${step.color}50`,
|
|
420
|
+
animation: 'save-ring-expand 2s ease-out infinite',
|
|
421
|
+
}}
|
|
422
|
+
/>
|
|
423
|
+
)}
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
<span
|
|
427
|
+
className="text-[10px] font-semibold uppercase tracking-[0.1em] transition-colors duration-500"
|
|
428
|
+
style={{
|
|
429
|
+
color: isDone ? step.color : isActive ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.18)',
|
|
430
|
+
}}
|
|
431
|
+
>
|
|
432
|
+
{step.label}
|
|
433
|
+
</span>
|
|
434
|
+
</div>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
END_OF_FILE_CONTENT
|
|
297
439
|
echo "Creating src/components/SaveDrawer/SaveDrawer.tsx..."
|
|
298
440
|
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/SaveDrawer.tsx"
|
|
299
441
|
/**
|
|
300
|
-
* SaveDrawer —
|
|
301
|
-
*
|
|
302
|
-
*
|
|
442
|
+
* SaveDrawer — Identical to prototype DopaDrawer (dopa-save).
|
|
443
|
+
* Bottom sheet for Cloud save; driven by real SSE (save-stream).
|
|
444
|
+
* Done: countdown 3s + "Chiusura in Xs" + Chiudi + Open site → auto-close. Error: Annulla + Retry.
|
|
303
445
|
*/
|
|
304
|
-
import { useState
|
|
446
|
+
import { useState } from 'react';
|
|
447
|
+
import { cn } from '@/lib/utils';
|
|
448
|
+
import { Separator } from '@/components/ui/separator';
|
|
449
|
+
import { Progress } from '@/components/ui/progress';
|
|
450
|
+
import { DeployNode } from './DeployNode';
|
|
451
|
+
import { DeployConnector } from './DeployConnector';
|
|
452
|
+
import { Particles, BuildBars, SuccessBurst, ElapsedTimer } from './Visuals';
|
|
453
|
+
import { useSaveStream } from './useSaveStream';
|
|
454
|
+
import { DEPLOY_STEPS } from './steps';
|
|
305
455
|
import type { ProjectState } from '@jsonpages/core';
|
|
306
456
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
457
|
+
export interface SaveDrawerProps {
|
|
458
|
+
cloudUrl: string;
|
|
459
|
+
apiKey: string;
|
|
460
|
+
state: ProjectState;
|
|
461
|
+
slug: string;
|
|
462
|
+
onClose: () => void;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export function SaveDrawer({ cloudUrl, apiKey, state, slug, onClose }: SaveDrawerProps) {
|
|
466
|
+
const [burst, setBurst] = useState(false);
|
|
467
|
+
|
|
468
|
+
const {
|
|
469
|
+
phase,
|
|
470
|
+
currentStep,
|
|
471
|
+
stepState,
|
|
472
|
+
doneSteps,
|
|
473
|
+
progress,
|
|
474
|
+
deployUrl,
|
|
475
|
+
countdown,
|
|
476
|
+
logs,
|
|
477
|
+
errorMessage,
|
|
478
|
+
retry,
|
|
479
|
+
} = useSaveStream({
|
|
480
|
+
cloudUrl,
|
|
481
|
+
apiKey,
|
|
482
|
+
state,
|
|
483
|
+
slug,
|
|
484
|
+
onClose,
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const isRunning = phase === 'running';
|
|
488
|
+
const isDone = phase === 'done';
|
|
489
|
+
const isError = phase === 'error';
|
|
490
|
+
|
|
491
|
+
if (isDone && !burst) setBurst(true);
|
|
492
|
+
|
|
493
|
+
const activeColor = isDone
|
|
494
|
+
? '#34d399'
|
|
495
|
+
: isError
|
|
496
|
+
? '#f87171'
|
|
497
|
+
: (currentStep?.color ?? '#60a5fa');
|
|
498
|
+
|
|
499
|
+
const particleCount =
|
|
500
|
+
isDone ? 40
|
|
501
|
+
: doneSteps.length === 3 ? 28
|
|
502
|
+
: doneSteps.length === 2 ? 16
|
|
503
|
+
: doneSteps.length === 1 ? 8
|
|
504
|
+
: 4;
|
|
505
|
+
|
|
506
|
+
return (
|
|
507
|
+
<>
|
|
508
|
+
<div
|
|
509
|
+
className="fixed inset-0 bg-black/40 backdrop-blur-[2px] z-40 save-fade-in"
|
|
510
|
+
onClick={isDone || isError ? onClose : undefined}
|
|
511
|
+
aria-hidden
|
|
512
|
+
/>
|
|
513
|
+
|
|
514
|
+
<div
|
|
515
|
+
role="status"
|
|
516
|
+
aria-live="polite"
|
|
517
|
+
aria-label={isDone ? 'Deploy completed' : isError ? 'Deploy failed' : 'Deploying'}
|
|
518
|
+
className="fixed inset-x-0 bottom-0 z-50 flex justify-center pb-6 px-4 save-drawer-up"
|
|
519
|
+
>
|
|
520
|
+
<div
|
|
521
|
+
className={cn(
|
|
522
|
+
'relative w-full max-w-2xl overflow-hidden rounded-2xl',
|
|
523
|
+
'border border-white/[0.07] bg-[hsl(222,18%,7%)]'
|
|
524
|
+
)}
|
|
525
|
+
style={{
|
|
526
|
+
boxShadow: `0 0 0 1px rgba(255,255,255,0.04), 0 -20px 60px rgba(0,0,0,0.6), 0 0 80px ${activeColor}0d`,
|
|
527
|
+
transition: 'box-shadow 1.2s ease',
|
|
528
|
+
}}
|
|
529
|
+
>
|
|
530
|
+
<div
|
|
531
|
+
className="pointer-events-none absolute inset-0"
|
|
532
|
+
style={{
|
|
533
|
+
background: `radial-gradient(ellipse 70% 60% at 50% 110%, ${activeColor}12 0%, transparent 65%)`,
|
|
534
|
+
transition: 'background 1.5s ease',
|
|
535
|
+
animation: 'save-ambient-pulse 3.5s ease infinite',
|
|
536
|
+
}}
|
|
537
|
+
aria-hidden
|
|
538
|
+
/>
|
|
539
|
+
|
|
540
|
+
{isDone && (
|
|
541
|
+
<div className="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden>
|
|
542
|
+
<div
|
|
543
|
+
className="absolute inset-y-0 w-[35%]"
|
|
544
|
+
style={{
|
|
545
|
+
background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.04), transparent)',
|
|
546
|
+
animation: 'save-shimmer-sweep 1.4s 0.1s ease forwards',
|
|
547
|
+
}}
|
|
548
|
+
/>
|
|
549
|
+
</div>
|
|
550
|
+
)}
|
|
551
|
+
|
|
552
|
+
<Particles count={particleCount} color={activeColor} />
|
|
553
|
+
{burst && <SuccessBurst />}
|
|
554
|
+
|
|
555
|
+
<div className="relative z-10 px-8 pt-7 pb-6">
|
|
556
|
+
<div className="flex items-start justify-between mb-7">
|
|
557
|
+
<div className="flex flex-col gap-2.5">
|
|
558
|
+
<div
|
|
559
|
+
className="flex items-center gap-2 text-[11px] font-bold uppercase tracking-[0.1em] transition-colors duration-500"
|
|
560
|
+
style={{ color: activeColor }}
|
|
561
|
+
>
|
|
562
|
+
<span
|
|
563
|
+
className="inline-block h-1.5 w-1.5 rounded-full"
|
|
564
|
+
style={{
|
|
565
|
+
background: activeColor,
|
|
566
|
+
boxShadow: `0 0 6px ${activeColor}`,
|
|
567
|
+
animation: isRunning ? 'save-ambient-pulse 1.5s ease infinite' : 'none',
|
|
568
|
+
}}
|
|
569
|
+
aria-hidden
|
|
570
|
+
/>
|
|
571
|
+
{isDone ? 'Live' : isError ? 'Build failed' : currentStep?.verb ?? 'Saving'}
|
|
572
|
+
</div>
|
|
573
|
+
|
|
574
|
+
<div key={currentStep?.id ?? phase} className="save-text-in min-h-[42px]">
|
|
575
|
+
{isDone ? (
|
|
576
|
+
<div className="save-success-pop">
|
|
577
|
+
<p className="text-lg font-semibold text-white leading-snug">Your content is live.</p>
|
|
578
|
+
<p className="text-sm text-white/40 mt-0.5">Deployed to production successfully</p>
|
|
579
|
+
</div>
|
|
580
|
+
) : isError ? (
|
|
581
|
+
<>
|
|
582
|
+
<p className="text-base font-semibold text-white leading-snug">
|
|
583
|
+
Deploy failed at build.
|
|
584
|
+
</p>
|
|
585
|
+
<p className="text-sm text-white/35 mt-0.5">
|
|
586
|
+
{errorMessage ?? 'Check your Vercel logs or retry below'}
|
|
587
|
+
</p>
|
|
588
|
+
</>
|
|
589
|
+
) : currentStep ? (
|
|
590
|
+
<>
|
|
591
|
+
<p className="text-sm font-light text-white/55 leading-relaxed">{currentStep.poem[0]}</p>
|
|
592
|
+
<p className="text-sm font-light text-white/30 leading-relaxed">{currentStep.poem[1]}</p>
|
|
593
|
+
</>
|
|
594
|
+
) : null}
|
|
595
|
+
</div>
|
|
596
|
+
</div>
|
|
597
|
+
|
|
598
|
+
<div className="flex flex-col items-end gap-1.5 shrink-0 ml-6">
|
|
599
|
+
{isDone ? (
|
|
600
|
+
<div className="flex flex-col items-end gap-1.5 save-fade-up">
|
|
601
|
+
<span
|
|
602
|
+
className="font-mono text-xs text-emerald-400 font-semibold"
|
|
603
|
+
aria-live="polite"
|
|
604
|
+
>
|
|
605
|
+
Chiusura in {countdown}s
|
|
606
|
+
</span>
|
|
607
|
+
<div className="w-24 h-0.5 bg-white/10 rounded-full overflow-hidden">
|
|
608
|
+
<div
|
|
609
|
+
className="h-full bg-emerald-400 rounded-full countdown-bar"
|
|
610
|
+
style={{ boxShadow: '0 0 6px #34d39988' }}
|
|
611
|
+
/>
|
|
612
|
+
</div>
|
|
613
|
+
</div>
|
|
614
|
+
) : (
|
|
615
|
+
<ElapsedTimer running={isRunning} />
|
|
616
|
+
)}
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
|
|
620
|
+
<div className="flex items-center mb-5">
|
|
621
|
+
{DEPLOY_STEPS.map((step, i) => (
|
|
622
|
+
<div
|
|
623
|
+
key={step.id}
|
|
624
|
+
className={cn('flex items-center', i < DEPLOY_STEPS.length - 1 ? 'flex-1' : 'flex-none')}
|
|
625
|
+
>
|
|
626
|
+
<DeployNode step={step} state={stepState(i)} />
|
|
627
|
+
{i < DEPLOY_STEPS.length - 1 && (
|
|
628
|
+
<DeployConnector
|
|
629
|
+
fromState={stepState(i)}
|
|
630
|
+
toState={stepState(i + 1)}
|
|
631
|
+
color={DEPLOY_STEPS[i + 1].color}
|
|
632
|
+
/>
|
|
633
|
+
)}
|
|
634
|
+
</div>
|
|
635
|
+
))}
|
|
636
|
+
</div>
|
|
637
|
+
|
|
638
|
+
<div className="flex justify-center mb-5">
|
|
639
|
+
<BuildBars active={stepState(2) === 'active'} />
|
|
640
|
+
</div>
|
|
641
|
+
|
|
642
|
+
<Separator className="mb-5 bg-white/[0.06]" />
|
|
643
|
+
|
|
644
|
+
{isRunning && currentStep && (logs[currentStep.id]?.length ?? 0) > 0 && (
|
|
645
|
+
<div className="mb-4 max-h-16 overflow-y-auto rounded-md bg-white/[0.04] px-3 py-2 font-mono text-[11px] text-white/50 leading-relaxed">
|
|
646
|
+
{logs[currentStep.id]?.map((line, i) => (
|
|
647
|
+
<div key={i}>{line}</div>
|
|
648
|
+
))}
|
|
649
|
+
</div>
|
|
650
|
+
)}
|
|
651
|
+
|
|
652
|
+
<div className="flex items-center gap-4">
|
|
653
|
+
<div className="flex-1">
|
|
654
|
+
<Progress
|
|
655
|
+
value={progress}
|
|
656
|
+
className="h-[2px] bg-white/[0.06]"
|
|
657
|
+
indicatorClassName="transition-none"
|
|
658
|
+
indicatorStyle={{
|
|
659
|
+
background: `linear-gradient(90deg, ${DEPLOY_STEPS[0].color}, ${activeColor})`,
|
|
660
|
+
}}
|
|
661
|
+
/>
|
|
662
|
+
</div>
|
|
663
|
+
|
|
664
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
665
|
+
{isDone && (
|
|
666
|
+
<div className="flex gap-2 save-fade-up">
|
|
667
|
+
<button
|
|
668
|
+
type="button"
|
|
669
|
+
onClick={onClose}
|
|
670
|
+
className="px-3 py-1.5 rounded-md border border-white/20 text-white/90 text-sm hover:bg-white/10"
|
|
671
|
+
>
|
|
672
|
+
Chiudi
|
|
673
|
+
</button>
|
|
674
|
+
<button
|
|
675
|
+
type="button"
|
|
676
|
+
onClick={() =>
|
|
677
|
+
deployUrl ? window.open(deployUrl, '_blank') : window.location.reload()
|
|
678
|
+
}
|
|
679
|
+
className="px-3 py-1.5 rounded-md bg-emerald-400 text-zinc-900 text-sm font-semibold hover:bg-emerald-300 gap-1.5 inline-flex items-center"
|
|
680
|
+
>
|
|
681
|
+
<span aria-hidden>↗</span> Open site
|
|
682
|
+
</button>
|
|
683
|
+
</div>
|
|
684
|
+
)}
|
|
685
|
+
|
|
686
|
+
{isError && (
|
|
687
|
+
<div className="flex gap-2 save-fade-up">
|
|
688
|
+
<button
|
|
689
|
+
type="button"
|
|
690
|
+
onClick={onClose}
|
|
691
|
+
className="px-3 py-1.5 rounded-md text-white/80 text-sm hover:bg-white/10"
|
|
692
|
+
>
|
|
693
|
+
Annulla
|
|
694
|
+
</button>
|
|
695
|
+
<button
|
|
696
|
+
type="button"
|
|
697
|
+
onClick={retry}
|
|
698
|
+
className="px-3 py-1.5 rounded-md bg-red-500/90 text-white text-sm font-medium hover:bg-red-500"
|
|
699
|
+
>
|
|
700
|
+
Retry
|
|
701
|
+
</button>
|
|
702
|
+
</div>
|
|
703
|
+
)}
|
|
704
|
+
|
|
705
|
+
{isRunning && (
|
|
706
|
+
<span className="font-mono text-xs text-white/20" aria-hidden>
|
|
707
|
+
{doneSteps.length + 1} / {DEPLOY_STEPS.length}
|
|
708
|
+
</span>
|
|
709
|
+
)}
|
|
710
|
+
</div>
|
|
711
|
+
</div>
|
|
712
|
+
</div>
|
|
713
|
+
</div>
|
|
714
|
+
</div>
|
|
715
|
+
</>
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
END_OF_FILE_CONTENT
|
|
720
|
+
echo "Creating src/components/SaveDrawer/Visuals.tsx..."
|
|
721
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/Visuals.tsx"
|
|
722
|
+
import { useRef, useState, useEffect } from 'react';
|
|
723
|
+
|
|
724
|
+
const POOL: { id: number; x: number; y: number; size: number; dur: number; delay: number }[] = Array.from(
|
|
725
|
+
{ length: 44 },
|
|
726
|
+
(_, i) => ({
|
|
727
|
+
id: i,
|
|
728
|
+
x: 5 + Math.random() * 90,
|
|
729
|
+
y: 15 + Math.random() * 70,
|
|
730
|
+
size: 1.5 + Math.random() * 2.5,
|
|
731
|
+
dur: 2.8 + Math.random() * 3.5,
|
|
732
|
+
delay: Math.random() * 4,
|
|
733
|
+
})
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
interface ParticlesProps {
|
|
737
|
+
count: number;
|
|
738
|
+
color: string;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
export function Particles({ count, color }: ParticlesProps) {
|
|
742
|
+
return (
|
|
743
|
+
<div className="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden>
|
|
744
|
+
{POOL.slice(0, count).map((p) => (
|
|
745
|
+
<div
|
|
746
|
+
key={p.id}
|
|
747
|
+
className="absolute rounded-full"
|
|
748
|
+
style={{
|
|
749
|
+
left: `${p.x}%`,
|
|
750
|
+
bottom: `${p.y}%`,
|
|
751
|
+
width: p.size,
|
|
752
|
+
height: p.size,
|
|
753
|
+
background: color,
|
|
754
|
+
boxShadow: `0 0 ${p.size * 3}px ${color}`,
|
|
755
|
+
opacity: 0,
|
|
756
|
+
animation: `save-particle-float ${p.dur}s ${p.delay}s ease-out infinite`,
|
|
757
|
+
}}
|
|
758
|
+
/>
|
|
759
|
+
))}
|
|
760
|
+
</div>
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const BAR_H = [0.45, 0.75, 0.55, 0.9, 0.65, 0.8, 0.5, 0.72, 0.6, 0.85, 0.42, 0.7];
|
|
765
|
+
|
|
766
|
+
interface BuildBarsProps {
|
|
767
|
+
active: boolean;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export function BuildBars({ active }: BuildBarsProps) {
|
|
771
|
+
if (!active) return <div className="h-7" />;
|
|
772
|
+
return (
|
|
773
|
+
<div className="flex items-end gap-[3px] h-7" aria-hidden>
|
|
774
|
+
{BAR_H.map((h, i) => (
|
|
775
|
+
<div
|
|
776
|
+
key={i}
|
|
777
|
+
className="w-[3px] rounded-sm"
|
|
778
|
+
style={{
|
|
779
|
+
background: '#f59e0b',
|
|
780
|
+
height: `${h * 100}%`,
|
|
781
|
+
transformOrigin: 'bottom',
|
|
782
|
+
animation: `save-bar-eq ${0.42 + i * 0.06}s ${i * 0.04}s ease-in-out infinite alternate`,
|
|
783
|
+
}}
|
|
784
|
+
/>
|
|
785
|
+
))}
|
|
786
|
+
</div>
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const BURST_COLORS = ['#34d399', '#60a5fa', '#a78bfa', '#f59e0b', '#f472b6'];
|
|
791
|
+
|
|
792
|
+
export function SuccessBurst() {
|
|
793
|
+
return (
|
|
794
|
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center" aria-hidden>
|
|
795
|
+
{Array.from({ length: 16 }).map((_, i) => (
|
|
796
|
+
<div
|
|
797
|
+
key={i}
|
|
798
|
+
className="absolute rounded-full"
|
|
799
|
+
style={{
|
|
800
|
+
width: 5,
|
|
801
|
+
height: 5,
|
|
802
|
+
background: BURST_COLORS[i % BURST_COLORS.length],
|
|
803
|
+
['--r' as string]: `${i * 22.5}deg`,
|
|
804
|
+
animation: `save-burst-ray 0.85s ${i * 0.03}s cubic-bezier(0,0.6,0.5,1) forwards`,
|
|
805
|
+
transform: `rotate(${i * 22.5}deg)`,
|
|
806
|
+
transformOrigin: '50% 50%',
|
|
807
|
+
opacity: 0,
|
|
808
|
+
}}
|
|
809
|
+
/>
|
|
810
|
+
))}
|
|
811
|
+
</div>
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
interface ElapsedTimerProps {
|
|
816
|
+
running: boolean;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
export function ElapsedTimer({ running }: ElapsedTimerProps) {
|
|
820
|
+
const [elapsed, setElapsed] = useState(0);
|
|
821
|
+
const startRef = useRef<number | null>(null);
|
|
822
|
+
const rafRef = useRef<number | null>(null);
|
|
823
|
+
|
|
824
|
+
useEffect(() => {
|
|
825
|
+
if (!running) return;
|
|
826
|
+
if (!startRef.current) startRef.current = performance.now();
|
|
827
|
+
|
|
828
|
+
const tick = (): void => {
|
|
829
|
+
setElapsed(Math.floor((performance.now() - (startRef.current ?? 0)) / 1000));
|
|
830
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
831
|
+
};
|
|
832
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
833
|
+
return () => {
|
|
834
|
+
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
835
|
+
};
|
|
836
|
+
}, [running]);
|
|
837
|
+
|
|
838
|
+
const s = String(elapsed % 60).padStart(2, '0');
|
|
839
|
+
const m = String(Math.floor(elapsed / 60)).padStart(2, '0');
|
|
840
|
+
|
|
841
|
+
return (
|
|
842
|
+
<span className="font-mono text-xs text-white/25 tracking-widest" aria-live="off">
|
|
843
|
+
{m}:{s}
|
|
844
|
+
</span>
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
END_OF_FILE_CONTENT
|
|
849
|
+
echo "Creating src/components/SaveDrawer/index.ts..."
|
|
850
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/index.ts"
|
|
851
|
+
export { SaveDrawer } from './SaveDrawer';
|
|
852
|
+
|
|
853
|
+
END_OF_FILE_CONTENT
|
|
854
|
+
echo "Creating src/components/SaveDrawer/steps.ts..."
|
|
855
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/steps.ts"
|
|
856
|
+
import type { DeployStep } from './types';
|
|
857
|
+
|
|
858
|
+
export const DEPLOY_STEPS: readonly DeployStep[] = [
|
|
859
|
+
{
|
|
860
|
+
id: 'commit',
|
|
861
|
+
label: 'Commit',
|
|
862
|
+
verb: 'Committing',
|
|
863
|
+
poem: ['Crystallizing your edit', 'into permanent history.'],
|
|
864
|
+
color: '#60a5fa',
|
|
865
|
+
glyph: '◈',
|
|
866
|
+
duration: 2200,
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
id: 'push',
|
|
870
|
+
label: 'Push',
|
|
871
|
+
verb: 'Pushing',
|
|
872
|
+
poem: ['Sending your vision', 'across the wire.'],
|
|
873
|
+
color: '#a78bfa',
|
|
874
|
+
glyph: '◎',
|
|
875
|
+
duration: 2800,
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
id: 'build',
|
|
879
|
+
label: 'Build',
|
|
880
|
+
verb: 'Building',
|
|
881
|
+
poem: ['Assembling the pieces,', 'brick by digital brick.'],
|
|
882
|
+
color: '#f59e0b',
|
|
883
|
+
glyph: '⬡',
|
|
884
|
+
duration: 7500,
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
id: 'live',
|
|
888
|
+
label: 'Live',
|
|
889
|
+
verb: 'Going live',
|
|
890
|
+
poem: ['Your content', 'is now breathing.'],
|
|
891
|
+
color: '#34d399',
|
|
892
|
+
glyph: '✦',
|
|
893
|
+
duration: 1600,
|
|
894
|
+
},
|
|
895
|
+
] as const;
|
|
896
|
+
|
|
897
|
+
END_OF_FILE_CONTENT
|
|
898
|
+
echo "Creating src/components/SaveDrawer/types.ts..."
|
|
899
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/types.ts"
|
|
900
|
+
/** Save/deploy flow types (aligned with prototype) */
|
|
901
|
+
|
|
902
|
+
export type StepId = 'commit' | 'push' | 'build' | 'live';
|
|
903
|
+
|
|
904
|
+
export type StepState = 'pending' | 'active' | 'done';
|
|
905
|
+
|
|
906
|
+
export type DeployPhase = 'running' | 'done' | 'error';
|
|
907
|
+
|
|
908
|
+
export interface DeployStep {
|
|
909
|
+
id: StepId;
|
|
910
|
+
label: string;
|
|
911
|
+
verb: string;
|
|
912
|
+
poem: [string, string];
|
|
913
|
+
color: string;
|
|
914
|
+
glyph: string;
|
|
915
|
+
duration: number;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
END_OF_FILE_CONTENT
|
|
919
|
+
echo "Creating src/components/SaveDrawer/useSaveStream.ts..."
|
|
920
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/useSaveStream.ts"
|
|
921
|
+
/**
|
|
922
|
+
* useSaveStream — Same API as prototype useSaveOrchestrator, driven by SSE from save-stream.
|
|
923
|
+
* Event handling aligned with CreateTenantFlow (parseSSE, step/log/error/done, post-loop, try/catch).
|
|
924
|
+
* Payload is read from a ref at effect start so the effect does not depend on state.page (avoids re-run and stream kill).
|
|
925
|
+
*/
|
|
926
|
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
927
|
+
import type { ProjectState } from '@jsonpages/core';
|
|
928
|
+
import type { DeployPhase, StepId, StepState } from './types';
|
|
929
|
+
import { DEPLOY_STEPS } from './steps';
|
|
313
930
|
|
|
314
931
|
function parseSSE(buffer: string): { event: string; data: string }[] {
|
|
315
932
|
const lines = buffer.split('\n');
|
|
@@ -329,7 +946,7 @@ function parseSSE(buffer: string): { event: string; data: string }[] {
|
|
|
329
946
|
return out;
|
|
330
947
|
}
|
|
331
948
|
|
|
332
|
-
|
|
949
|
+
interface UseSaveStreamOptions {
|
|
333
950
|
cloudUrl: string;
|
|
334
951
|
apiKey: string;
|
|
335
952
|
state: ProjectState;
|
|
@@ -337,91 +954,152 @@ export interface SaveDrawerProps {
|
|
|
337
954
|
onClose: () => void;
|
|
338
955
|
}
|
|
339
956
|
|
|
340
|
-
export function
|
|
341
|
-
|
|
342
|
-
|
|
957
|
+
export function useSaveStream({
|
|
958
|
+
cloudUrl,
|
|
959
|
+
apiKey,
|
|
960
|
+
state,
|
|
961
|
+
slug,
|
|
962
|
+
onClose,
|
|
963
|
+
}: UseSaveStreamOptions) {
|
|
964
|
+
const [phase, setPhase] = useState<DeployPhase>('running');
|
|
965
|
+
const [doneSteps, setDoneSteps] = useState<StepId[]>([]);
|
|
966
|
+
const [activeStepId, setActiveStepId] = useState<StepId | null>('commit');
|
|
967
|
+
const [progress, setProgress] = useState(0);
|
|
968
|
+
const [deployUrl, setDeployUrl] = useState<string | null>(null);
|
|
969
|
+
const [countdown, setCountdown] = useState(3);
|
|
343
970
|
const [logs, setLogs] = useState<Record<string, string[]>>({});
|
|
344
|
-
const [phase, setPhase] = useState<'running' | 'done' | 'error'>('running');
|
|
345
971
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
972
|
+
const progressRef = useRef(0);
|
|
973
|
+
const [runId, setRunId] = useState(0);
|
|
974
|
+
|
|
975
|
+
const payloadRef = useRef<{ path: string; content: unknown; message: string } | null>(null);
|
|
346
976
|
|
|
347
977
|
const addLog = useCallback((stepId: string, message: string) => {
|
|
348
978
|
setLogs((prev) => ({ ...prev, [stepId]: [...(prev[stepId] || []), message] }));
|
|
349
979
|
}, []);
|
|
350
980
|
|
|
981
|
+
const stepState = useCallback(
|
|
982
|
+
(i: number): StepState => {
|
|
983
|
+
const step = DEPLOY_STEPS[i];
|
|
984
|
+
if (!step) return 'pending';
|
|
985
|
+
if (doneSteps.includes(step.id)) return 'done';
|
|
986
|
+
if (activeStepId === step.id && phase === 'running') return 'active';
|
|
987
|
+
return 'pending';
|
|
988
|
+
},
|
|
989
|
+
[doneSteps, activeStepId, phase]
|
|
990
|
+
);
|
|
991
|
+
|
|
992
|
+
const currentStep = DEPLOY_STEPS.find((s) => s.id === activeStepId) ?? DEPLOY_STEPS[0] ?? null;
|
|
993
|
+
|
|
351
994
|
useEffect(() => {
|
|
352
995
|
let cancelled = false;
|
|
353
996
|
let gotDone = false;
|
|
997
|
+
setPhase('running');
|
|
998
|
+
setDoneSteps([]);
|
|
999
|
+
setActiveStepId('commit');
|
|
1000
|
+
setProgress(0);
|
|
1001
|
+
setDeployUrl(null);
|
|
1002
|
+
setCountdown(3);
|
|
1003
|
+
setLogs({});
|
|
1004
|
+
setErrorMessage(null);
|
|
1005
|
+
progressRef.current = 0;
|
|
1006
|
+
|
|
1007
|
+
payloadRef.current = {
|
|
1008
|
+
path: `src/data/pages/${slug}.json`,
|
|
1009
|
+
content: state.page,
|
|
1010
|
+
message: `Content update for ${slug} via Visual Editor`,
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
const url = `${cloudUrl.replace(/\/$/, '')}/save-stream`;
|
|
354
1014
|
|
|
355
1015
|
const run = async () => {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
headers: {
|
|
360
|
-
'Content-Type': 'application/json',
|
|
361
|
-
Authorization: `Bearer ${apiKey}`,
|
|
362
|
-
},
|
|
363
|
-
body: JSON.stringify({
|
|
364
|
-
path: `src/data/pages/${slug}.json`,
|
|
365
|
-
content: state.page,
|
|
366
|
-
message: `Content update for ${slug} via Visual Editor`,
|
|
367
|
-
}),
|
|
368
|
-
});
|
|
1016
|
+
try {
|
|
1017
|
+
const payload = payloadRef.current;
|
|
1018
|
+
if (!payload || cancelled) return;
|
|
369
1019
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
1020
|
+
const res = await fetch(url, {
|
|
1021
|
+
method: 'POST',
|
|
1022
|
+
headers: {
|
|
1023
|
+
'Content-Type': 'application/json',
|
|
1024
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1025
|
+
},
|
|
1026
|
+
body: JSON.stringify(payload),
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
if (!res.ok || !res.body) {
|
|
1030
|
+
if (!cancelled) {
|
|
1031
|
+
setPhase('error');
|
|
1032
|
+
setErrorMessage('Request failed');
|
|
1033
|
+
}
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
376
1036
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
1037
|
+
const reader = res.body.getReader();
|
|
1038
|
+
const dec = new TextDecoder();
|
|
1039
|
+
let buffer = '';
|
|
380
1040
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
1041
|
+
while (true) {
|
|
1042
|
+
const { value, done: streamDone } = await reader.read();
|
|
1043
|
+
if (streamDone || cancelled) break;
|
|
1044
|
+
buffer += dec.decode(value, { stream: true });
|
|
1045
|
+
const parts = buffer.split('\n\n');
|
|
1046
|
+
buffer = parts.pop() || '';
|
|
387
1047
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
1048
|
+
for (const part of parts) {
|
|
1049
|
+
const parsed = parseSSE(part + '\n\n');
|
|
1050
|
+
for (const { event: ev, data: raw } of parsed) {
|
|
1051
|
+
if (cancelled) return;
|
|
1052
|
+
try {
|
|
1053
|
+
const data = JSON.parse(raw) as Record<string, unknown>;
|
|
1054
|
+
if (ev === 'step') {
|
|
1055
|
+
const id = data.id as StepId | undefined;
|
|
1056
|
+
const status = data.status as string | undefined;
|
|
1057
|
+
if (id && status === 'running') {
|
|
1058
|
+
setActiveStepId(id);
|
|
1059
|
+
const idx = DEPLOY_STEPS.findIndex((s) => s.id === id);
|
|
1060
|
+
if (idx >= 0) progressRef.current = Math.round((idx / DEPLOY_STEPS.length) * 100);
|
|
1061
|
+
setProgress(progressRef.current);
|
|
1062
|
+
}
|
|
1063
|
+
if (id && status === 'done') {
|
|
1064
|
+
setDoneSteps((prev) => [...prev, id].filter(Boolean));
|
|
1065
|
+
setActiveStepId(null);
|
|
1066
|
+
const idx = DEPLOY_STEPS.findIndex((s) => s.id === id);
|
|
1067
|
+
progressRef.current = idx >= 0 ? Math.round(((idx + 1) / DEPLOY_STEPS.length) * 100) : 100;
|
|
1068
|
+
setProgress(progressRef.current);
|
|
1069
|
+
}
|
|
1070
|
+
} else if (ev === 'log' && data.stepId && data.message) {
|
|
1071
|
+
addLog(data.stepId as string, data.message as string);
|
|
1072
|
+
} else if (ev === 'error') {
|
|
1073
|
+
if (!cancelled) {
|
|
1074
|
+
setErrorMessage((data.message as string) || 'Errore');
|
|
1075
|
+
setPhase('error');
|
|
1076
|
+
}
|
|
1077
|
+
return;
|
|
1078
|
+
} else if (ev === 'done') {
|
|
1079
|
+
gotDone = true;
|
|
1080
|
+
setDeployUrl((data.deployUrl as string) ?? null);
|
|
1081
|
+
setProgress(100);
|
|
1082
|
+
setDoneSteps(['commit', 'push', 'build', 'live']);
|
|
400
1083
|
setActiveStepId(null);
|
|
1084
|
+
setPhase('done');
|
|
1085
|
+
return;
|
|
401
1086
|
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
} else if (ev === 'error') {
|
|
405
|
-
setErrorMessage((data.message as string) || 'Error');
|
|
406
|
-
setPhase('error');
|
|
407
|
-
return;
|
|
408
|
-
} else if (ev === 'done') {
|
|
409
|
-
gotDone = true;
|
|
410
|
-
setCompletedSteps((prev) => [...new Set([...prev, 'commit', 'push', 'build', 'live'])]);
|
|
411
|
-
setActiveStepId(null);
|
|
412
|
-
setPhase('done');
|
|
413
|
-
return;
|
|
1087
|
+
} catch {
|
|
1088
|
+
// ignore parse errors
|
|
414
1089
|
}
|
|
415
|
-
} catch {
|
|
416
|
-
// ignore parse errors
|
|
417
1090
|
}
|
|
418
1091
|
}
|
|
419
1092
|
}
|
|
420
|
-
}
|
|
421
1093
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
1094
|
+
if (!cancelled && !gotDone) {
|
|
1095
|
+
setErrorMessage('Operazione interrotta o build non completato.');
|
|
1096
|
+
setPhase('error');
|
|
1097
|
+
}
|
|
1098
|
+
} catch (e) {
|
|
1099
|
+
if (!cancelled) {
|
|
1100
|
+
setErrorMessage(e instanceof Error ? e.message : 'Errore');
|
|
1101
|
+
setPhase('error');
|
|
1102
|
+
}
|
|
425
1103
|
}
|
|
426
1104
|
};
|
|
427
1105
|
|
|
@@ -429,142 +1107,40 @@ export function SaveDrawer({ cloudUrl, apiKey, state, slug, onClose }: SaveDrawe
|
|
|
429
1107
|
return () => {
|
|
430
1108
|
cancelled = true;
|
|
431
1109
|
};
|
|
432
|
-
}, [cloudUrl, apiKey,
|
|
433
|
-
|
|
434
|
-
const progressPct =
|
|
435
|
-
phase === 'done' ? 100 : SAVE_STEPS.length > 0 ? Math.round((completedSteps.length / SAVE_STEPS.length) * 100) : 0;
|
|
436
|
-
|
|
437
|
-
const stepState = (id: string) => {
|
|
438
|
-
if (completedSteps.includes(id)) return 'done';
|
|
439
|
-
if (activeStepId === id) return 'active';
|
|
440
|
-
return 'pending';
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
return (
|
|
444
|
-
<>
|
|
445
|
-
<div
|
|
446
|
-
className="fixed inset-0 z-40 bg-black/40 backdrop-blur-[2px]"
|
|
447
|
-
onClick={phase === 'done' || phase === 'error' ? onClose : undefined}
|
|
448
|
-
aria-hidden
|
|
449
|
-
/>
|
|
450
|
-
<div
|
|
451
|
-
role="status"
|
|
452
|
-
aria-live="polite"
|
|
453
|
-
className="fixed inset-x-0 bottom-0 z-50 flex justify-center pb-6 px-4"
|
|
454
|
-
aria-label={phase === 'done' ? 'Save completed' : phase === 'error' ? 'Save failed' : 'Saving'}
|
|
455
|
-
>
|
|
456
|
-
<div
|
|
457
|
-
className="relative w-full max-w-2xl overflow-hidden rounded-2xl border border-white/[0.07] bg-[hsl(222,18%,7%)] shadow-lg"
|
|
458
|
-
onClick={(e) => e.stopPropagation()}
|
|
459
|
-
>
|
|
460
|
-
<div className="relative z-10 px-6 pt-6 pb-5">
|
|
461
|
-
<div className="flex items-center justify-between mb-4">
|
|
462
|
-
<h2 className="text-lg font-semibold text-white">
|
|
463
|
-
{phase === 'done' ? 'Content is live' : phase === 'error' ? 'Save failed' : 'Saving to Cloud'}
|
|
464
|
-
</h2>
|
|
465
|
-
{phase === 'running' && (
|
|
466
|
-
<span className="text-xs font-mono text-white/50">{completedSteps.length} / {SAVE_STEPS.length}</span>
|
|
467
|
-
)}
|
|
468
|
-
</div>
|
|
1110
|
+
}, [cloudUrl, apiKey, slug, runId, addLog]);
|
|
469
1111
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
>
|
|
486
|
-
{stepState(step.id) === 'done' ? (
|
|
487
|
-
<span className="text-emerald-400" aria-hidden>✓</span>
|
|
488
|
-
) : stepState(step.id) === 'active' ? (
|
|
489
|
-
<span className="w-3 h-3 border-2 border-blue-400 border-t-transparent rounded-full animate-spin" aria-hidden />
|
|
490
|
-
) : (
|
|
491
|
-
<span className="text-xs text-white/40">{i + 1}</span>
|
|
492
|
-
)}
|
|
493
|
-
</div>
|
|
494
|
-
<span
|
|
495
|
-
className={`ml-2 text-xs font-medium truncate ${
|
|
496
|
-
stepState(step.id) !== 'pending' ? 'text-white' : 'text-white/40'
|
|
497
|
-
}`}
|
|
498
|
-
>
|
|
499
|
-
{step.label}
|
|
500
|
-
</span>
|
|
501
|
-
{i < SAVE_STEPS.length - 1 && (
|
|
502
|
-
<div
|
|
503
|
-
className={`flex-1 h-0.5 mx-2 min-w-[8px] ${
|
|
504
|
-
stepState(step.id) === 'done' ? 'bg-emerald-500/50' : 'bg-white/10'
|
|
505
|
-
}`}
|
|
506
|
-
/>
|
|
507
|
-
)}
|
|
508
|
-
</div>
|
|
509
|
-
))}
|
|
510
|
-
</div>
|
|
511
|
-
|
|
512
|
-
<div className="h-1 bg-white/10 rounded-full overflow-hidden mb-4">
|
|
513
|
-
<div
|
|
514
|
-
className="h-full bg-emerald-500 rounded-full transition-all duration-300"
|
|
515
|
-
style={{ width: `${progressPct}%` }}
|
|
516
|
-
/>
|
|
517
|
-
</div>
|
|
1112
|
+
useEffect(() => {
|
|
1113
|
+
if (phase !== 'done') return;
|
|
1114
|
+
setCountdown(3);
|
|
1115
|
+
const iv = setInterval(() => {
|
|
1116
|
+
setCountdown((s) => {
|
|
1117
|
+
if (s <= 1) {
|
|
1118
|
+
clearInterval(iv);
|
|
1119
|
+
onClose();
|
|
1120
|
+
return 0;
|
|
1121
|
+
}
|
|
1122
|
+
return s - 1;
|
|
1123
|
+
});
|
|
1124
|
+
}, 1000);
|
|
1125
|
+
return () => clearInterval(iv);
|
|
1126
|
+
}, [phase, onClose]);
|
|
518
1127
|
|
|
519
|
-
|
|
520
|
-
<div className="mb-4 p-2 rounded bg-black/30 border border-white/10 font-mono text-xs text-white/60 space-y-0.5 max-h-20 overflow-y-auto">
|
|
521
|
-
{(logs[activeStepId || ''] || []).slice(-3).map((line, i) => (
|
|
522
|
-
<div key={i}>$ {line}</div>
|
|
523
|
-
))}
|
|
524
|
-
</div>
|
|
525
|
-
)}
|
|
1128
|
+
const retry = useCallback(() => setRunId((r) => r + 1), []);
|
|
526
1129
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
onClick={onClose}
|
|
540
|
-
className="px-4 py-2 rounded-lg border border-white/20 text-white/90 text-sm hover:bg-white/10"
|
|
541
|
-
>
|
|
542
|
-
Close
|
|
543
|
-
</button>
|
|
544
|
-
</>
|
|
545
|
-
)}
|
|
546
|
-
{phase === 'error' && (
|
|
547
|
-
<button
|
|
548
|
-
type="button"
|
|
549
|
-
onClick={onClose}
|
|
550
|
-
className="px-4 py-2 rounded-lg border border-white/20 text-white/90 text-sm hover:bg-white/10"
|
|
551
|
-
>
|
|
552
|
-
Close
|
|
553
|
-
</button>
|
|
554
|
-
)}
|
|
555
|
-
</div>
|
|
556
|
-
</div>
|
|
557
|
-
</div>
|
|
558
|
-
</div>
|
|
559
|
-
</>
|
|
560
|
-
);
|
|
1130
|
+
return {
|
|
1131
|
+
phase,
|
|
1132
|
+
currentStep,
|
|
1133
|
+
stepState,
|
|
1134
|
+
doneSteps,
|
|
1135
|
+
progress,
|
|
1136
|
+
deployUrl,
|
|
1137
|
+
countdown,
|
|
1138
|
+
logs,
|
|
1139
|
+
errorMessage,
|
|
1140
|
+
retry,
|
|
1141
|
+
};
|
|
561
1142
|
}
|
|
562
1143
|
|
|
563
|
-
END_OF_FILE_CONTENT
|
|
564
|
-
echo "Creating src/components/SaveDrawer/index.ts..."
|
|
565
|
-
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/index.ts"
|
|
566
|
-
export { SaveDrawer } from './SaveDrawer';
|
|
567
|
-
|
|
568
1144
|
END_OF_FILE_CONTENT
|
|
569
1145
|
mkdir -p "src/components/arch-layers"
|
|
570
1146
|
echo "Creating src/components/arch-layers/View.tsx..."
|
|
@@ -3403,6 +3979,46 @@ export { Label }
|
|
|
3403
3979
|
|
|
3404
3980
|
|
|
3405
3981
|
|
|
3982
|
+
END_OF_FILE_CONTENT
|
|
3983
|
+
echo "Creating src/components/ui/progress.tsx..."
|
|
3984
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/ui/progress.tsx"
|
|
3985
|
+
import * as React from 'react';
|
|
3986
|
+
import { cn } from '@/lib/utils';
|
|
3987
|
+
|
|
3988
|
+
interface ProgressProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
3989
|
+
value?: number | null;
|
|
3990
|
+
indicatorClassName?: string;
|
|
3991
|
+
indicatorStyle?: React.CSSProperties;
|
|
3992
|
+
}
|
|
3993
|
+
|
|
3994
|
+
const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
|
|
3995
|
+
({ className, value, indicatorClassName, indicatorStyle, ...props }, ref) => (
|
|
3996
|
+
<div
|
|
3997
|
+
ref={ref}
|
|
3998
|
+
role="progressbar"
|
|
3999
|
+
aria-valuenow={value ?? 0}
|
|
4000
|
+
aria-valuemin={0}
|
|
4001
|
+
aria-valuemax={100}
|
|
4002
|
+
className={cn('relative h-2 w-full overflow-hidden rounded-full bg-secondary', className)}
|
|
4003
|
+
{...props}
|
|
4004
|
+
>
|
|
4005
|
+
<div
|
|
4006
|
+
className={cn(
|
|
4007
|
+
'h-full w-full flex-1 bg-primary transition-all duration-700 ease-out',
|
|
4008
|
+
indicatorClassName
|
|
4009
|
+
)}
|
|
4010
|
+
style={{
|
|
4011
|
+
transform: `translateX(-${100 - (value ?? 0)}%)`,
|
|
4012
|
+
...indicatorStyle,
|
|
4013
|
+
}}
|
|
4014
|
+
/>
|
|
4015
|
+
</div>
|
|
4016
|
+
)
|
|
4017
|
+
);
|
|
4018
|
+
Progress.displayName = 'Progress';
|
|
4019
|
+
|
|
4020
|
+
export { Progress };
|
|
4021
|
+
|
|
3406
4022
|
END_OF_FILE_CONTENT
|
|
3407
4023
|
echo "Creating src/components/ui/select.tsx..."
|
|
3408
4024
|
cat << 'END_OF_FILE_CONTENT' > "src/components/ui/select.tsx"
|
|
@@ -4798,6 +5414,88 @@ html {
|
|
|
4798
5414
|
opacity: 0;
|
|
4799
5415
|
}
|
|
4800
5416
|
|
|
5417
|
+
/* ==========================================================================
|
|
5418
|
+
SAVE DRAWER (prototype-identical animations)
|
|
5419
|
+
========================================================================== */
|
|
5420
|
+
@keyframes save-drawer-up {
|
|
5421
|
+
from { transform: translateY(100%); opacity: 0; }
|
|
5422
|
+
to { transform: translateY(0); opacity: 1; }
|
|
5423
|
+
}
|
|
5424
|
+
@keyframes save-fade-in {
|
|
5425
|
+
from { opacity: 0; }
|
|
5426
|
+
to { opacity: 1; }
|
|
5427
|
+
}
|
|
5428
|
+
@keyframes save-fade-up {
|
|
5429
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
5430
|
+
to { opacity: 1; transform: translateY(0); }
|
|
5431
|
+
}
|
|
5432
|
+
@keyframes save-success-pop {
|
|
5433
|
+
0% { transform: scale(0.88); opacity: 0; }
|
|
5434
|
+
60% { transform: scale(1.04); }
|
|
5435
|
+
100% { transform: scale(1); opacity: 1; }
|
|
5436
|
+
}
|
|
5437
|
+
@keyframes save-ring-expand {
|
|
5438
|
+
from { transform: scale(1); opacity: 0.7; }
|
|
5439
|
+
to { transform: scale(2.1); opacity: 0; }
|
|
5440
|
+
}
|
|
5441
|
+
@keyframes save-orb-travel {
|
|
5442
|
+
from { left: 0%; }
|
|
5443
|
+
to { left: calc(100% - 10px); }
|
|
5444
|
+
}
|
|
5445
|
+
@keyframes save-glyph-rotate {
|
|
5446
|
+
from { transform: rotate(0deg); }
|
|
5447
|
+
to { transform: rotate(360deg); }
|
|
5448
|
+
}
|
|
5449
|
+
@keyframes save-particle-float {
|
|
5450
|
+
0% { transform: translateY(0) scale(1); opacity: 0; }
|
|
5451
|
+
15% { opacity: 1; }
|
|
5452
|
+
100% { transform: translateY(-90px) scale(0.3); opacity: 0; }
|
|
5453
|
+
}
|
|
5454
|
+
@keyframes save-bar-eq {
|
|
5455
|
+
from { transform: scaleY(0.4); }
|
|
5456
|
+
to { transform: scaleY(1); }
|
|
5457
|
+
}
|
|
5458
|
+
@keyframes save-ambient-pulse {
|
|
5459
|
+
0%, 100% { opacity: 0.3; }
|
|
5460
|
+
50% { opacity: 0.65; }
|
|
5461
|
+
}
|
|
5462
|
+
@keyframes save-shimmer-sweep {
|
|
5463
|
+
from { transform: translateX(-100%); }
|
|
5464
|
+
to { transform: translateX(250%); }
|
|
5465
|
+
}
|
|
5466
|
+
@keyframes save-text-in {
|
|
5467
|
+
from { opacity: 0; transform: translateX(-6px); }
|
|
5468
|
+
to { opacity: 1; transform: translateX(0); }
|
|
5469
|
+
}
|
|
5470
|
+
@keyframes save-node-glow {
|
|
5471
|
+
0%, 100% { box-shadow: 0 0 12px var(--glow-color, #60a5fa55); }
|
|
5472
|
+
50% { box-shadow: 0 0 28px var(--glow-color, #60a5fa88), 0 0 48px var(--glow-color, #60a5fa22); }
|
|
5473
|
+
}
|
|
5474
|
+
@keyframes save-burst-ray {
|
|
5475
|
+
0% { transform: rotate(var(--r, 0deg)) translateX(0); opacity: 1; }
|
|
5476
|
+
100% { transform: rotate(var(--r, 0deg)) translateX(56px); opacity: 0; }
|
|
5477
|
+
}
|
|
5478
|
+
|
|
5479
|
+
.save-drawer-up { animation: save-drawer-up 0.45s cubic-bezier(0.22, 1, 0.36, 1) forwards; }
|
|
5480
|
+
.save-fade-in { animation: save-fade-in 0.25s ease forwards; }
|
|
5481
|
+
.save-fade-up { animation: save-fade-up 0.35s ease forwards; }
|
|
5482
|
+
.save-success-pop { animation: save-success-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }
|
|
5483
|
+
.save-text-in { animation: save-text-in 0.3s ease forwards; }
|
|
5484
|
+
|
|
5485
|
+
.stroke-dash-30 { stroke-dasharray: 30; stroke-dashoffset: 30; }
|
|
5486
|
+
.save-check-draw { animation: save-check-draw 0.4s 0.05s ease forwards; }
|
|
5487
|
+
@keyframes save-check-draw {
|
|
5488
|
+
to { stroke-dashoffset: 0; }
|
|
5489
|
+
}
|
|
5490
|
+
|
|
5491
|
+
/* Countdown drain — 3s linear (dopa-save identical) */
|
|
5492
|
+
.countdown-bar {
|
|
5493
|
+
animation: countdown-drain 3s linear forwards;
|
|
5494
|
+
}
|
|
5495
|
+
@keyframes countdown-drain {
|
|
5496
|
+
from { width: 100%; }
|
|
5497
|
+
to { width: 0%; }
|
|
5498
|
+
}
|
|
4801
5499
|
|
|
4802
5500
|
|
|
4803
5501
|
END_OF_FILE_CONTENT
|