@jsonpages/cli 3.0.46 → 3.0.47
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 +711 -170
- package/package.json +1 -1
|
@@ -294,22 +294,593 @@ 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.
|
|
443
|
+
* Bottom sheet for Cloud save; driven by real SSE (save-stream).
|
|
444
|
+
* Done: "Refresh page" + "Chiudi". 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 { DeployNode } from './DeployNode';
|
|
450
|
+
import { DeployConnector } from './DeployConnector';
|
|
451
|
+
import { Particles, BuildBars, SuccessBurst, ElapsedTimer } from './Visuals';
|
|
452
|
+
import { useSaveStream } from './useSaveStream';
|
|
453
|
+
import { DEPLOY_STEPS } from './steps';
|
|
305
454
|
import type { ProjectState } from '@jsonpages/core';
|
|
306
455
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
456
|
+
export interface SaveDrawerProps {
|
|
457
|
+
cloudUrl: string;
|
|
458
|
+
apiKey: string;
|
|
459
|
+
state: ProjectState;
|
|
460
|
+
slug: string;
|
|
461
|
+
onClose: () => void;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function SaveDrawer({ cloudUrl, apiKey, state, slug, onClose }: SaveDrawerProps) {
|
|
465
|
+
const [burst, setBurst] = useState(false);
|
|
466
|
+
|
|
467
|
+
const { phase, currentStep, stepState, doneSteps, progress, retry } = useSaveStream({
|
|
468
|
+
cloudUrl,
|
|
469
|
+
apiKey,
|
|
470
|
+
state,
|
|
471
|
+
slug,
|
|
472
|
+
onClose,
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
const isRunning = phase === 'running';
|
|
476
|
+
const isDone = phase === 'done';
|
|
477
|
+
const isError = phase === 'error';
|
|
478
|
+
|
|
479
|
+
if (isDone && !burst) setBurst(true);
|
|
480
|
+
|
|
481
|
+
const activeColor = isDone
|
|
482
|
+
? '#34d399'
|
|
483
|
+
: isError
|
|
484
|
+
? '#f87171'
|
|
485
|
+
: (currentStep?.color ?? '#60a5fa');
|
|
486
|
+
|
|
487
|
+
const particleCount =
|
|
488
|
+
isDone ? 40
|
|
489
|
+
: doneSteps.length === 3 ? 28
|
|
490
|
+
: doneSteps.length === 2 ? 16
|
|
491
|
+
: doneSteps.length === 1 ? 8
|
|
492
|
+
: 4;
|
|
493
|
+
|
|
494
|
+
return (
|
|
495
|
+
<>
|
|
496
|
+
<div
|
|
497
|
+
className="fixed inset-0 bg-black/40 backdrop-blur-[2px] z-40 save-fade-in"
|
|
498
|
+
onClick={isDone || isError ? onClose : undefined}
|
|
499
|
+
aria-hidden
|
|
500
|
+
/>
|
|
501
|
+
|
|
502
|
+
<div
|
|
503
|
+
role="status"
|
|
504
|
+
aria-live="polite"
|
|
505
|
+
aria-label={isDone ? 'Deploy completed' : isError ? 'Deploy failed' : 'Deploying'}
|
|
506
|
+
className="fixed inset-x-0 bottom-0 z-50 flex justify-center pb-6 px-4 save-drawer-up"
|
|
507
|
+
>
|
|
508
|
+
<div
|
|
509
|
+
className={cn(
|
|
510
|
+
'relative w-full max-w-2xl overflow-hidden rounded-2xl',
|
|
511
|
+
'border border-white/[0.07] bg-[hsl(222,18%,7%)]'
|
|
512
|
+
)}
|
|
513
|
+
style={{
|
|
514
|
+
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`,
|
|
515
|
+
transition: 'box-shadow 1.2s ease',
|
|
516
|
+
}}
|
|
517
|
+
>
|
|
518
|
+
<div
|
|
519
|
+
className="pointer-events-none absolute inset-0"
|
|
520
|
+
style={{
|
|
521
|
+
background: `radial-gradient(ellipse 70% 60% at 50% 110%, ${activeColor}12 0%, transparent 65%)`,
|
|
522
|
+
transition: 'background 1.5s ease',
|
|
523
|
+
animation: 'save-ambient-pulse 3.5s ease infinite',
|
|
524
|
+
}}
|
|
525
|
+
aria-hidden
|
|
526
|
+
/>
|
|
527
|
+
|
|
528
|
+
{isDone && (
|
|
529
|
+
<div className="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden>
|
|
530
|
+
<div
|
|
531
|
+
className="absolute inset-y-0 w-[35%]"
|
|
532
|
+
style={{
|
|
533
|
+
background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.04), transparent)',
|
|
534
|
+
animation: 'save-shimmer-sweep 1.4s 0.1s ease forwards',
|
|
535
|
+
}}
|
|
536
|
+
/>
|
|
537
|
+
</div>
|
|
538
|
+
)}
|
|
539
|
+
|
|
540
|
+
<Particles count={particleCount} color={activeColor} />
|
|
541
|
+
{burst && <SuccessBurst />}
|
|
542
|
+
|
|
543
|
+
<div className="relative z-10 px-8 pt-7 pb-6">
|
|
544
|
+
<div className="flex items-start justify-between mb-7">
|
|
545
|
+
<div className="flex flex-col gap-2.5">
|
|
546
|
+
<div
|
|
547
|
+
className="flex items-center gap-2 text-[11px] font-bold uppercase tracking-[0.1em] transition-colors duration-500"
|
|
548
|
+
style={{ color: activeColor }}
|
|
549
|
+
>
|
|
550
|
+
<span
|
|
551
|
+
className="inline-block h-1.5 w-1.5 rounded-full"
|
|
552
|
+
style={{
|
|
553
|
+
background: activeColor,
|
|
554
|
+
boxShadow: `0 0 6px ${activeColor}`,
|
|
555
|
+
animation: isRunning ? 'save-ambient-pulse 1.5s ease infinite' : 'none',
|
|
556
|
+
}}
|
|
557
|
+
aria-hidden
|
|
558
|
+
/>
|
|
559
|
+
{isDone ? 'Live' : isError ? 'Build failed' : currentStep?.verb ?? 'Saving'}
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
<div key={currentStep?.id ?? phase} className="save-text-in min-h-[42px]">
|
|
563
|
+
{isDone ? (
|
|
564
|
+
<div className="save-success-pop">
|
|
565
|
+
<p className="text-lg font-semibold text-white leading-snug">Your content is live.</p>
|
|
566
|
+
<p className="text-sm text-white/40 mt-0.5">Deployed to production successfully</p>
|
|
567
|
+
</div>
|
|
568
|
+
) : isError ? (
|
|
569
|
+
<>
|
|
570
|
+
<p className="text-base font-semibold text-white leading-snug">Deploy failed at build.</p>
|
|
571
|
+
<p className="text-sm text-white/35 mt-0.5">Check your Vercel logs or retry below</p>
|
|
572
|
+
</>
|
|
573
|
+
) : currentStep ? (
|
|
574
|
+
<>
|
|
575
|
+
<p className="text-sm font-light text-white/55 leading-relaxed">{currentStep.poem[0]}</p>
|
|
576
|
+
<p className="text-sm font-light text-white/30 leading-relaxed">{currentStep.poem[1]}</p>
|
|
577
|
+
</>
|
|
578
|
+
) : null}
|
|
579
|
+
</div>
|
|
580
|
+
</div>
|
|
581
|
+
|
|
582
|
+
<div className="flex flex-col items-end gap-1.5 shrink-0 ml-6">
|
|
583
|
+
{!isDone && <ElapsedTimer running={isRunning} />}
|
|
584
|
+
</div>
|
|
585
|
+
</div>
|
|
586
|
+
|
|
587
|
+
<div className="flex items-center mb-5">
|
|
588
|
+
{DEPLOY_STEPS.map((step, i) => (
|
|
589
|
+
<div
|
|
590
|
+
key={step.id}
|
|
591
|
+
className={cn('flex items-center', i < DEPLOY_STEPS.length - 1 ? 'flex-1' : 'flex-none')}
|
|
592
|
+
>
|
|
593
|
+
<DeployNode step={step} state={stepState(i)} />
|
|
594
|
+
{i < DEPLOY_STEPS.length - 1 && (
|
|
595
|
+
<DeployConnector
|
|
596
|
+
fromState={stepState(i)}
|
|
597
|
+
toState={stepState(i + 1)}
|
|
598
|
+
color={DEPLOY_STEPS[i + 1].color}
|
|
599
|
+
/>
|
|
600
|
+
)}
|
|
601
|
+
</div>
|
|
602
|
+
))}
|
|
603
|
+
</div>
|
|
604
|
+
|
|
605
|
+
<div className="flex justify-center mb-5">
|
|
606
|
+
<BuildBars active={stepState(2) === 'active'} />
|
|
607
|
+
</div>
|
|
608
|
+
|
|
609
|
+
<Separator className="mb-5 bg-white/[0.06]" />
|
|
610
|
+
|
|
611
|
+
<div className="flex items-center gap-4">
|
|
612
|
+
<div className="flex-1 h-[2px] bg-white/[0.06] rounded-full overflow-hidden">
|
|
613
|
+
<div
|
|
614
|
+
className="h-full rounded-full transition-all duration-300"
|
|
615
|
+
style={{
|
|
616
|
+
width: `${progress}%`,
|
|
617
|
+
background: `linear-gradient(90deg, ${DEPLOY_STEPS[0].color}, ${activeColor})`,
|
|
618
|
+
}}
|
|
619
|
+
/>
|
|
620
|
+
</div>
|
|
621
|
+
|
|
622
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
623
|
+
{isDone && (
|
|
624
|
+
<div className="flex gap-2 save-fade-up">
|
|
625
|
+
<button
|
|
626
|
+
type="button"
|
|
627
|
+
onClick={() => window.location.reload()}
|
|
628
|
+
className="px-3 py-1.5 rounded-md bg-emerald-400 text-zinc-900 text-sm font-semibold hover:bg-emerald-300"
|
|
629
|
+
>
|
|
630
|
+
Refresh page
|
|
631
|
+
</button>
|
|
632
|
+
<button
|
|
633
|
+
type="button"
|
|
634
|
+
onClick={onClose}
|
|
635
|
+
className="px-3 py-1.5 rounded-md border border-white/20 text-white/90 text-sm hover:bg-white/10"
|
|
636
|
+
>
|
|
637
|
+
Chiudi
|
|
638
|
+
</button>
|
|
639
|
+
</div>
|
|
640
|
+
)}
|
|
641
|
+
|
|
642
|
+
{isError && (
|
|
643
|
+
<div className="flex gap-2 save-fade-up">
|
|
644
|
+
<button
|
|
645
|
+
type="button"
|
|
646
|
+
onClick={onClose}
|
|
647
|
+
className="px-3 py-1.5 rounded-md text-white/80 text-sm hover:bg-white/10"
|
|
648
|
+
>
|
|
649
|
+
Annulla
|
|
650
|
+
</button>
|
|
651
|
+
<button
|
|
652
|
+
type="button"
|
|
653
|
+
onClick={retry}
|
|
654
|
+
className="px-3 py-1.5 rounded-md bg-red-500/90 text-white text-sm font-medium hover:bg-red-500"
|
|
655
|
+
>
|
|
656
|
+
Retry
|
|
657
|
+
</button>
|
|
658
|
+
</div>
|
|
659
|
+
)}
|
|
660
|
+
|
|
661
|
+
{isRunning && (
|
|
662
|
+
<span className="font-mono text-xs text-white/20" aria-hidden>
|
|
663
|
+
{doneSteps.length + 1} / {DEPLOY_STEPS.length}
|
|
664
|
+
</span>
|
|
665
|
+
)}
|
|
666
|
+
</div>
|
|
667
|
+
</div>
|
|
668
|
+
</div>
|
|
669
|
+
</div>
|
|
670
|
+
</div>
|
|
671
|
+
</>
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
END_OF_FILE_CONTENT
|
|
676
|
+
echo "Creating src/components/SaveDrawer/Visuals.tsx..."
|
|
677
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/Visuals.tsx"
|
|
678
|
+
import { useRef, useState, useEffect } from 'react';
|
|
679
|
+
|
|
680
|
+
const POOL: { id: number; x: number; y: number; size: number; dur: number; delay: number }[] = Array.from(
|
|
681
|
+
{ length: 44 },
|
|
682
|
+
(_, i) => ({
|
|
683
|
+
id: i,
|
|
684
|
+
x: 5 + Math.random() * 90,
|
|
685
|
+
y: 15 + Math.random() * 70,
|
|
686
|
+
size: 1.5 + Math.random() * 2.5,
|
|
687
|
+
dur: 2.8 + Math.random() * 3.5,
|
|
688
|
+
delay: Math.random() * 4,
|
|
689
|
+
})
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
interface ParticlesProps {
|
|
693
|
+
count: number;
|
|
694
|
+
color: string;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export function Particles({ count, color }: ParticlesProps) {
|
|
698
|
+
return (
|
|
699
|
+
<div className="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden>
|
|
700
|
+
{POOL.slice(0, count).map((p) => (
|
|
701
|
+
<div
|
|
702
|
+
key={p.id}
|
|
703
|
+
className="absolute rounded-full"
|
|
704
|
+
style={{
|
|
705
|
+
left: `${p.x}%`,
|
|
706
|
+
bottom: `${p.y}%`,
|
|
707
|
+
width: p.size,
|
|
708
|
+
height: p.size,
|
|
709
|
+
background: color,
|
|
710
|
+
boxShadow: `0 0 ${p.size * 3}px ${color}`,
|
|
711
|
+
opacity: 0,
|
|
712
|
+
animation: `save-particle-float ${p.dur}s ${p.delay}s ease-out infinite`,
|
|
713
|
+
}}
|
|
714
|
+
/>
|
|
715
|
+
))}
|
|
716
|
+
</div>
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
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];
|
|
721
|
+
|
|
722
|
+
interface BuildBarsProps {
|
|
723
|
+
active: boolean;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
export function BuildBars({ active }: BuildBarsProps) {
|
|
727
|
+
if (!active) return <div className="h-7" />;
|
|
728
|
+
return (
|
|
729
|
+
<div className="flex items-end gap-[3px] h-7" aria-hidden>
|
|
730
|
+
{BAR_H.map((h, i) => (
|
|
731
|
+
<div
|
|
732
|
+
key={i}
|
|
733
|
+
className="w-[3px] rounded-sm"
|
|
734
|
+
style={{
|
|
735
|
+
background: '#f59e0b',
|
|
736
|
+
height: `${h * 100}%`,
|
|
737
|
+
transformOrigin: 'bottom',
|
|
738
|
+
animation: `save-bar-eq ${0.42 + i * 0.06}s ${i * 0.04}s ease-in-out infinite alternate`,
|
|
739
|
+
}}
|
|
740
|
+
/>
|
|
741
|
+
))}
|
|
742
|
+
</div>
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const BURST_COLORS = ['#34d399', '#60a5fa', '#a78bfa', '#f59e0b', '#f472b6'];
|
|
747
|
+
|
|
748
|
+
export function SuccessBurst() {
|
|
749
|
+
return (
|
|
750
|
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center" aria-hidden>
|
|
751
|
+
{Array.from({ length: 16 }).map((_, i) => (
|
|
752
|
+
<div
|
|
753
|
+
key={i}
|
|
754
|
+
className="absolute rounded-full"
|
|
755
|
+
style={{
|
|
756
|
+
width: 5,
|
|
757
|
+
height: 5,
|
|
758
|
+
background: BURST_COLORS[i % BURST_COLORS.length],
|
|
759
|
+
['--r' as string]: `${i * 22.5}deg`,
|
|
760
|
+
animation: `save-burst-ray 0.85s ${i * 0.03}s cubic-bezier(0,0.6,0.5,1) forwards`,
|
|
761
|
+
transform: `rotate(${i * 22.5}deg)`,
|
|
762
|
+
transformOrigin: '50% 50%',
|
|
763
|
+
opacity: 0,
|
|
764
|
+
}}
|
|
765
|
+
/>
|
|
766
|
+
))}
|
|
767
|
+
</div>
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
interface ElapsedTimerProps {
|
|
772
|
+
running: boolean;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
export function ElapsedTimer({ running }: ElapsedTimerProps) {
|
|
776
|
+
const [elapsed, setElapsed] = useState(0);
|
|
777
|
+
const startRef = useRef<number | null>(null);
|
|
778
|
+
const rafRef = useRef<number | null>(null);
|
|
779
|
+
|
|
780
|
+
useEffect(() => {
|
|
781
|
+
if (!running) return;
|
|
782
|
+
if (!startRef.current) startRef.current = performance.now();
|
|
783
|
+
|
|
784
|
+
const tick = (): void => {
|
|
785
|
+
setElapsed(Math.floor((performance.now() - (startRef.current ?? 0)) / 1000));
|
|
786
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
787
|
+
};
|
|
788
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
789
|
+
return () => {
|
|
790
|
+
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
791
|
+
};
|
|
792
|
+
}, [running]);
|
|
793
|
+
|
|
794
|
+
const s = String(elapsed % 60).padStart(2, '0');
|
|
795
|
+
const m = String(Math.floor(elapsed / 60)).padStart(2, '0');
|
|
796
|
+
|
|
797
|
+
return (
|
|
798
|
+
<span className="font-mono text-xs text-white/25 tracking-widest" aria-live="off">
|
|
799
|
+
{m}:{s}
|
|
800
|
+
</span>
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
END_OF_FILE_CONTENT
|
|
805
|
+
echo "Creating src/components/SaveDrawer/index.ts..."
|
|
806
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/index.ts"
|
|
807
|
+
export { SaveDrawer } from './SaveDrawer';
|
|
808
|
+
|
|
809
|
+
END_OF_FILE_CONTENT
|
|
810
|
+
echo "Creating src/components/SaveDrawer/steps.ts..."
|
|
811
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/steps.ts"
|
|
812
|
+
import type { DeployStep } from './types';
|
|
813
|
+
|
|
814
|
+
export const DEPLOY_STEPS: readonly DeployStep[] = [
|
|
815
|
+
{
|
|
816
|
+
id: 'commit',
|
|
817
|
+
label: 'Commit',
|
|
818
|
+
verb: 'Committing',
|
|
819
|
+
poem: ['Crystallizing your edit', 'into permanent history.'],
|
|
820
|
+
color: '#60a5fa',
|
|
821
|
+
glyph: '◈',
|
|
822
|
+
duration: 2200,
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
id: 'push',
|
|
826
|
+
label: 'Push',
|
|
827
|
+
verb: 'Pushing',
|
|
828
|
+
poem: ['Sending your vision', 'across the wire.'],
|
|
829
|
+
color: '#a78bfa',
|
|
830
|
+
glyph: '◎',
|
|
831
|
+
duration: 2800,
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
id: 'build',
|
|
835
|
+
label: 'Build',
|
|
836
|
+
verb: 'Building',
|
|
837
|
+
poem: ['Assembling the pieces,', 'brick by digital brick.'],
|
|
838
|
+
color: '#f59e0b',
|
|
839
|
+
glyph: '⬡',
|
|
840
|
+
duration: 7500,
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
id: 'live',
|
|
844
|
+
label: 'Live',
|
|
845
|
+
verb: 'Going live',
|
|
846
|
+
poem: ['Your content', 'is now breathing.'],
|
|
847
|
+
color: '#34d399',
|
|
848
|
+
glyph: '✦',
|
|
849
|
+
duration: 1600,
|
|
850
|
+
},
|
|
851
|
+
] as const;
|
|
852
|
+
|
|
853
|
+
END_OF_FILE_CONTENT
|
|
854
|
+
echo "Creating src/components/SaveDrawer/types.ts..."
|
|
855
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/types.ts"
|
|
856
|
+
/** Save/deploy flow types (aligned with prototype) */
|
|
857
|
+
|
|
858
|
+
export type StepId = 'commit' | 'push' | 'build' | 'live';
|
|
859
|
+
|
|
860
|
+
export type StepState = 'pending' | 'active' | 'done';
|
|
861
|
+
|
|
862
|
+
export type DeployPhase = 'running' | 'done' | 'error';
|
|
863
|
+
|
|
864
|
+
export interface DeployStep {
|
|
865
|
+
id: StepId;
|
|
866
|
+
label: string;
|
|
867
|
+
verb: string;
|
|
868
|
+
poem: [string, string];
|
|
869
|
+
color: string;
|
|
870
|
+
glyph: string;
|
|
871
|
+
duration: number;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
END_OF_FILE_CONTENT
|
|
875
|
+
echo "Creating src/components/SaveDrawer/useSaveStream.ts..."
|
|
876
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/useSaveStream.ts"
|
|
877
|
+
/**
|
|
878
|
+
* useSaveStream — Same API as prototype useSaveOrchestrator, but driven by real SSE from save-stream.
|
|
879
|
+
*/
|
|
880
|
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
881
|
+
import type { ProjectState } from '@jsonpages/core';
|
|
882
|
+
import type { DeployPhase, StepId, StepState } from './types';
|
|
883
|
+
import { DEPLOY_STEPS } from './steps';
|
|
313
884
|
|
|
314
885
|
function parseSSE(buffer: string): { event: string; data: string }[] {
|
|
315
886
|
const lines = buffer.split('\n');
|
|
@@ -329,7 +900,7 @@ function parseSSE(buffer: string): { event: string; data: string }[] {
|
|
|
329
900
|
return out;
|
|
330
901
|
}
|
|
331
902
|
|
|
332
|
-
|
|
903
|
+
interface UseSaveStreamOptions {
|
|
333
904
|
cloudUrl: string;
|
|
334
905
|
apiKey: string;
|
|
335
906
|
state: ProjectState;
|
|
@@ -337,23 +908,44 @@ export interface SaveDrawerProps {
|
|
|
337
908
|
onClose: () => void;
|
|
338
909
|
}
|
|
339
910
|
|
|
340
|
-
export function
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
911
|
+
export function useSaveStream({
|
|
912
|
+
cloudUrl,
|
|
913
|
+
apiKey,
|
|
914
|
+
state,
|
|
915
|
+
slug,
|
|
916
|
+
onClose: _onClose,
|
|
917
|
+
}: UseSaveStreamOptions) {
|
|
918
|
+
const [phase, setPhase] = useState<DeployPhase>('running');
|
|
919
|
+
const [doneSteps, setDoneSteps] = useState<StepId[]>([]);
|
|
920
|
+
const [activeStepId, setActiveStepId] = useState<StepId | null>('commit');
|
|
921
|
+
const [progress, setProgress] = useState(0);
|
|
922
|
+
const progressRef = useRef(0);
|
|
923
|
+
const [runId, setRunId] = useState(0);
|
|
924
|
+
|
|
925
|
+
const stepState = useCallback(
|
|
926
|
+
(i: number): StepState => {
|
|
927
|
+
const step = DEPLOY_STEPS[i];
|
|
928
|
+
if (!step) return 'pending';
|
|
929
|
+
if (doneSteps.includes(step.id)) return 'done';
|
|
930
|
+
if (activeStepId === step.id && phase === 'running') return 'active';
|
|
931
|
+
return 'pending';
|
|
932
|
+
},
|
|
933
|
+
[doneSteps, activeStepId, phase]
|
|
934
|
+
);
|
|
346
935
|
|
|
347
|
-
const
|
|
348
|
-
setLogs((prev) => ({ ...prev, [stepId]: [...(prev[stepId] || []), message] }));
|
|
349
|
-
}, []);
|
|
936
|
+
const currentStep = DEPLOY_STEPS.find((s) => s.id === activeStepId) ?? DEPLOY_STEPS[0] ?? null;
|
|
350
937
|
|
|
351
938
|
useEffect(() => {
|
|
352
939
|
let cancelled = false;
|
|
353
|
-
|
|
940
|
+
setPhase('running');
|
|
941
|
+
setDoneSteps([]);
|
|
942
|
+
setActiveStepId('commit');
|
|
943
|
+
setProgress(0);
|
|
944
|
+
progressRef.current = 0;
|
|
945
|
+
|
|
946
|
+
const url = `${cloudUrl.replace(/\/$/, '')}/save-stream`;
|
|
354
947
|
|
|
355
948
|
const run = async () => {
|
|
356
|
-
const url = `${cloudUrl.replace(/\/$/, '')}/save-stream`;
|
|
357
949
|
const res = await fetch(url, {
|
|
358
950
|
method: 'POST',
|
|
359
951
|
headers: {
|
|
@@ -368,9 +960,7 @@ export function SaveDrawer({ cloudUrl, apiKey, state, slug, onClose }: SaveDrawe
|
|
|
368
960
|
});
|
|
369
961
|
|
|
370
962
|
if (!res.ok || !res.body) {
|
|
371
|
-
|
|
372
|
-
setErrorMessage((data as { error?: string }).error || `Request failed: ${res.status}`);
|
|
373
|
-
setPhase('error');
|
|
963
|
+
if (!cancelled) setPhase('error');
|
|
374
964
|
return;
|
|
375
965
|
}
|
|
376
966
|
|
|
@@ -392,179 +982,57 @@ export function SaveDrawer({ cloudUrl, apiKey, state, slug, onClose }: SaveDrawe
|
|
|
392
982
|
try {
|
|
393
983
|
const data = JSON.parse(raw) as Record<string, unknown>;
|
|
394
984
|
if (ev === 'step') {
|
|
395
|
-
const id = data.id as
|
|
985
|
+
const id = data.id as StepId | undefined;
|
|
396
986
|
const status = data.status as string | undefined;
|
|
397
|
-
if (id && status === 'running')
|
|
987
|
+
if (id && status === 'running') {
|
|
988
|
+
setActiveStepId(id);
|
|
989
|
+
const idx = DEPLOY_STEPS.findIndex((s) => s.id === id);
|
|
990
|
+
if (idx >= 0) progressRef.current = Math.round((idx / DEPLOY_STEPS.length) * 100);
|
|
991
|
+
setProgress(progressRef.current);
|
|
992
|
+
}
|
|
398
993
|
if (id && status === 'done') {
|
|
399
|
-
|
|
994
|
+
setDoneSteps((prev) => [...prev, id].filter(Boolean));
|
|
400
995
|
setActiveStepId(null);
|
|
996
|
+
const idx = DEPLOY_STEPS.findIndex((s) => s.id === id);
|
|
997
|
+
progressRef.current = idx >= 0 ? Math.round(((idx + 1) / DEPLOY_STEPS.length) * 100) : 100;
|
|
998
|
+
setProgress(progressRef.current);
|
|
401
999
|
}
|
|
402
|
-
} else if (ev === 'log' && data.stepId && data.message) {
|
|
403
|
-
addLog(data.stepId as string, data.message as string);
|
|
404
1000
|
} else if (ev === 'error') {
|
|
405
|
-
setErrorMessage((data.message as string) || 'Error');
|
|
406
1001
|
setPhase('error');
|
|
407
1002
|
return;
|
|
408
1003
|
} else if (ev === 'done') {
|
|
409
|
-
|
|
410
|
-
|
|
1004
|
+
setProgress(100);
|
|
1005
|
+
setDoneSteps(['commit', 'push', 'build', 'live']);
|
|
411
1006
|
setActiveStepId(null);
|
|
412
1007
|
setPhase('done');
|
|
413
1008
|
return;
|
|
414
1009
|
}
|
|
415
1010
|
} catch {
|
|
416
|
-
// ignore
|
|
1011
|
+
// ignore
|
|
417
1012
|
}
|
|
418
1013
|
}
|
|
419
1014
|
}
|
|
420
1015
|
}
|
|
421
|
-
|
|
422
|
-
if (!cancelled && !gotDone) {
|
|
423
|
-
setErrorMessage('Connection closed before completion');
|
|
424
|
-
setPhase('error');
|
|
425
|
-
}
|
|
426
1016
|
};
|
|
427
1017
|
|
|
428
1018
|
run();
|
|
429
1019
|
return () => {
|
|
430
1020
|
cancelled = true;
|
|
431
1021
|
};
|
|
432
|
-
}, [cloudUrl, apiKey, state.page,
|
|
1022
|
+
}, [cloudUrl, apiKey, slug, state.page, runId]);
|
|
433
1023
|
|
|
434
|
-
const
|
|
435
|
-
phase === 'done' ? 100 : SAVE_STEPS.length > 0 ? Math.round((completedSteps.length / SAVE_STEPS.length) * 100) : 0;
|
|
1024
|
+
const retry = useCallback(() => setRunId((r) => r + 1), []);
|
|
436
1025
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
1026
|
+
return {
|
|
1027
|
+
phase,
|
|
1028
|
+
currentStep,
|
|
1029
|
+
stepState,
|
|
1030
|
+
doneSteps,
|
|
1031
|
+
progress,
|
|
1032
|
+
retry,
|
|
441
1033
|
};
|
|
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>
|
|
469
|
-
|
|
470
|
-
{phase === 'error' && errorMessage && (
|
|
471
|
-
<p className="text-sm text-red-400 mb-4">{errorMessage}</p>
|
|
472
|
-
)}
|
|
473
|
-
|
|
474
|
-
<div className="flex items-center gap-2 mb-4">
|
|
475
|
-
{SAVE_STEPS.map((step, i) => (
|
|
476
|
-
<div key={step.id} className="flex items-center flex-1">
|
|
477
|
-
<div
|
|
478
|
-
className={`flex items-center justify-center w-8 h-8 rounded-full border-2 shrink-0 ${
|
|
479
|
-
stepState(step.id) === 'done'
|
|
480
|
-
? 'bg-emerald-500/20 border-emerald-500'
|
|
481
|
-
: stepState(step.id) === 'active'
|
|
482
|
-
? 'bg-blue-500/20 border-blue-500'
|
|
483
|
-
: 'bg-white/10 border-white/20'
|
|
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>
|
|
518
|
-
|
|
519
|
-
{Object.keys(logs).length > 0 && (
|
|
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
|
-
)}
|
|
526
|
-
|
|
527
|
-
<div className="flex items-center gap-2 justify-end">
|
|
528
|
-
{phase === 'done' && (
|
|
529
|
-
<>
|
|
530
|
-
<button
|
|
531
|
-
type="button"
|
|
532
|
-
onClick={() => window.location.reload()}
|
|
533
|
-
className="px-4 py-2 rounded-lg bg-emerald-500 text-zinc-900 font-medium text-sm hover:bg-emerald-400"
|
|
534
|
-
>
|
|
535
|
-
Refresh page
|
|
536
|
-
</button>
|
|
537
|
-
<button
|
|
538
|
-
type="button"
|
|
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
|
-
);
|
|
561
1034
|
}
|
|
562
1035
|
|
|
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
1036
|
END_OF_FILE_CONTENT
|
|
569
1037
|
mkdir -p "src/components/arch-layers"
|
|
570
1038
|
echo "Creating src/components/arch-layers/View.tsx..."
|
|
@@ -4798,6 +5266,79 @@ html {
|
|
|
4798
5266
|
opacity: 0;
|
|
4799
5267
|
}
|
|
4800
5268
|
|
|
5269
|
+
/* ==========================================================================
|
|
5270
|
+
SAVE DRAWER (prototype-identical animations)
|
|
5271
|
+
========================================================================== */
|
|
5272
|
+
@keyframes save-drawer-up {
|
|
5273
|
+
from { transform: translateY(100%); opacity: 0; }
|
|
5274
|
+
to { transform: translateY(0); opacity: 1; }
|
|
5275
|
+
}
|
|
5276
|
+
@keyframes save-fade-in {
|
|
5277
|
+
from { opacity: 0; }
|
|
5278
|
+
to { opacity: 1; }
|
|
5279
|
+
}
|
|
5280
|
+
@keyframes save-fade-up {
|
|
5281
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
5282
|
+
to { opacity: 1; transform: translateY(0); }
|
|
5283
|
+
}
|
|
5284
|
+
@keyframes save-success-pop {
|
|
5285
|
+
0% { transform: scale(0.88); opacity: 0; }
|
|
5286
|
+
60% { transform: scale(1.04); }
|
|
5287
|
+
100% { transform: scale(1); opacity: 1; }
|
|
5288
|
+
}
|
|
5289
|
+
@keyframes save-ring-expand {
|
|
5290
|
+
from { transform: scale(1); opacity: 0.7; }
|
|
5291
|
+
to { transform: scale(2.1); opacity: 0; }
|
|
5292
|
+
}
|
|
5293
|
+
@keyframes save-orb-travel {
|
|
5294
|
+
from { left: 0%; }
|
|
5295
|
+
to { left: calc(100% - 10px); }
|
|
5296
|
+
}
|
|
5297
|
+
@keyframes save-glyph-rotate {
|
|
5298
|
+
from { transform: rotate(0deg); }
|
|
5299
|
+
to { transform: rotate(360deg); }
|
|
5300
|
+
}
|
|
5301
|
+
@keyframes save-particle-float {
|
|
5302
|
+
0% { transform: translateY(0) scale(1); opacity: 0; }
|
|
5303
|
+
15% { opacity: 1; }
|
|
5304
|
+
100% { transform: translateY(-90px) scale(0.3); opacity: 0; }
|
|
5305
|
+
}
|
|
5306
|
+
@keyframes save-bar-eq {
|
|
5307
|
+
from { transform: scaleY(0.4); }
|
|
5308
|
+
to { transform: scaleY(1); }
|
|
5309
|
+
}
|
|
5310
|
+
@keyframes save-ambient-pulse {
|
|
5311
|
+
0%, 100% { opacity: 0.3; }
|
|
5312
|
+
50% { opacity: 0.65; }
|
|
5313
|
+
}
|
|
5314
|
+
@keyframes save-shimmer-sweep {
|
|
5315
|
+
from { transform: translateX(-100%); }
|
|
5316
|
+
to { transform: translateX(250%); }
|
|
5317
|
+
}
|
|
5318
|
+
@keyframes save-text-in {
|
|
5319
|
+
from { opacity: 0; transform: translateX(-6px); }
|
|
5320
|
+
to { opacity: 1; transform: translateX(0); }
|
|
5321
|
+
}
|
|
5322
|
+
@keyframes save-node-glow {
|
|
5323
|
+
0%, 100% { box-shadow: 0 0 12px var(--glow-color, #60a5fa55); }
|
|
5324
|
+
50% { box-shadow: 0 0 28px var(--glow-color, #60a5fa88), 0 0 48px var(--glow-color, #60a5fa22); }
|
|
5325
|
+
}
|
|
5326
|
+
@keyframes save-burst-ray {
|
|
5327
|
+
0% { transform: rotate(var(--r, 0deg)) translateX(0); opacity: 1; }
|
|
5328
|
+
100% { transform: rotate(var(--r, 0deg)) translateX(56px); opacity: 0; }
|
|
5329
|
+
}
|
|
5330
|
+
|
|
5331
|
+
.save-drawer-up { animation: save-drawer-up 0.45s cubic-bezier(0.22, 1, 0.36, 1) forwards; }
|
|
5332
|
+
.save-fade-in { animation: save-fade-in 0.25s ease forwards; }
|
|
5333
|
+
.save-fade-up { animation: save-fade-up 0.35s ease forwards; }
|
|
5334
|
+
.save-success-pop { animation: save-success-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }
|
|
5335
|
+
.save-text-in { animation: save-text-in 0.3s ease forwards; }
|
|
5336
|
+
|
|
5337
|
+
.stroke-dash-30 { stroke-dasharray: 30; stroke-dashoffset: 30; }
|
|
5338
|
+
.save-check-draw { animation: save-check-draw 0.4s 0.05s ease forwards; }
|
|
5339
|
+
@keyframes save-check-draw {
|
|
5340
|
+
to { stroke-dashoffset: 0; }
|
|
5341
|
+
}
|
|
4801
5342
|
|
|
4802
5343
|
|
|
4803
5344
|
END_OF_FILE_CONTENT
|