@jsonpages/cli 3.0.49 → 3.0.50

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.
@@ -41,7 +41,6 @@ import { addSectionConfig } from '@/lib/addSectionConfig';
41
41
  import { getHydratedData } from '@/lib/draftStorage';
42
42
  import type { JsonPagesConfig, ProjectState } from '@jsonpages/core';
43
43
  import type { SiteConfig, ThemeConfig, MenuConfig } from '@/types';
44
- import { SaveDrawer } from '@/components/SaveDrawer';
45
44
 
46
45
  import siteData from '@/data/config/site.json';
47
46
  import themeData from '@/data/config/theme.json';
@@ -70,7 +69,6 @@ function getInitialData() {
70
69
  function App() {
71
70
  const [{ pages, siteConfig }] = useState(getInitialData);
72
71
  const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
73
- const [saveDrawer, setSaveDrawer] = useState<{ state: ProjectState; slug: string } | null>(null);
74
72
 
75
73
  useEffect(() => {
76
74
  // In Cloud mode, listing assets might be different or disabled for MVP
@@ -80,7 +78,11 @@ function App() {
80
78
  .then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
81
79
  .catch(() => setAssetsManifest([]));
82
80
  }, []);
83
-
81
+
82
+ console.log("🔍 DEBUG ENV:", {
83
+ URL: import.meta.env.VITE_JSONPAGES_CLOUD_URL,
84
+ KEY: import.meta.env.VITE_JSONPAGES_API_KEY ? "PRESENT" : "MISSING"
85
+ });
84
86
  const config: JsonPagesConfig = {
85
87
  tenantId: TENANT_ID,
86
88
  registry: ComponentRegistry as JsonPagesConfig['registry'],
@@ -94,9 +96,29 @@ function App() {
94
96
  persistence: {
95
97
  async saveToFile(state: ProjectState, slug: string): Promise<void> {
96
98
 
97
- // ☁️ SCENARIO A: CLOUD BRIDGE (Production) — open SaveDrawer, stream save-stream
99
+ // ☁️ SCENARIO A: CLOUD BRIDGE (Production)
98
100
  if (CLOUD_API_URL && CLOUD_API_KEY) {
99
- setSaveDrawer({ state, slug });
101
+ console.log(`☁️ Saving ${slug} via Cloud Bridge...`);
102
+
103
+ const res = await fetch(`${CLOUD_API_URL}/save`, {
104
+ method: 'POST',
105
+ headers: {
106
+ 'Content-Type': 'application/json',
107
+ 'Authorization': `Bearer ${CLOUD_API_KEY}`
108
+ },
109
+ body: JSON.stringify({
110
+ // Mapping logical slug to physical path in repo
111
+ path: `src/data/pages/${slug}.json`,
112
+ // We save the page config specifically
113
+ content: state.page,
114
+ message: `Content update for ${slug} via Visual Editor`
115
+ }),
116
+ });
117
+
118
+ if (!res.ok) {
119
+ const err = await res.json().catch(() => ({}));
120
+ throw new Error(err.error || `Cloud save failed: ${res.status}`);
121
+ }
100
122
  return;
101
123
  }
102
124
 
@@ -142,20 +164,7 @@ function App() {
142
164
  },
143
165
  };
144
166
 
145
- return (
146
- <>
147
- <JsonPagesEngine config={config} />
148
- {saveDrawer && CLOUD_API_URL && CLOUD_API_KEY && (
149
- <SaveDrawer
150
- cloudUrl={CLOUD_API_URL}
151
- apiKey={CLOUD_API_KEY}
152
- state={saveDrawer.state}
153
- slug={saveDrawer.slug}
154
- onClose={() => setSaveDrawer(null)}
155
- />
156
- )}
157
- </>
158
- );
167
+ return <JsonPagesEngine config={config} />;
159
168
  }
160
169
 
161
170
  export default App;
@@ -292,858 +301,6 @@ export const NotFound: React.FC = () => {
292
301
 
293
302
 
294
303
 
295
- END_OF_FILE_CONTENT
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
439
- echo "Creating src/components/SaveDrawer/SaveDrawer.tsx..."
440
- cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/SaveDrawer.tsx"
441
- /**
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.
445
- */
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';
455
- import type { ProjectState } from '@jsonpages/core';
456
-
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="h-9 rounded-md px-3 inline-flex items-center justify-center text-sm font-medium border border-white/20 bg-white/5 text-white/90 hover:bg-white/10 transition-colors"
671
- >
672
- Chiudi
673
- </button>
674
- <button
675
- type="button"
676
- onClick={() =>
677
- deployUrl ? window.open(deployUrl, '_blank') : window.location.reload()
678
- }
679
- className="h-9 rounded-md px-3 inline-flex items-center justify-center gap-1.5 text-sm font-semibold bg-emerald-400 text-zinc-900 hover:bg-emerald-300 transition-colors"
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="h-9 rounded-md px-3 inline-flex items-center justify-center text-sm font-medium text-white/80 hover:bg-white/10 transition-colors"
692
- >
693
- Annulla
694
- </button>
695
- <button
696
- type="button"
697
- onClick={retry}
698
- className="h-9 rounded-md px-3 inline-flex items-center justify-center text-sm font-medium bg-red-600 text-white hover:bg-red-500 transition-colors"
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';
930
-
931
- function parseSSE(buffer: string): { event: string; data: string }[] {
932
- const lines = buffer.split('\n');
933
- const out: { event: string; data: string }[] = [];
934
- let event = 'message';
935
- let data = '';
936
- for (const line of lines) {
937
- if (line.startsWith('event:')) event = line.slice(6).trim();
938
- else if (line.startsWith('data:')) data = line.slice(5).trim();
939
- else if (line === '' && data) {
940
- out.push({ event, data });
941
- data = '';
942
- event = 'message';
943
- }
944
- }
945
- if (data) out.push({ event, data });
946
- return out;
947
- }
948
-
949
- interface UseSaveStreamOptions {
950
- cloudUrl: string;
951
- apiKey: string;
952
- state: ProjectState;
953
- slug: string;
954
- onClose: () => void;
955
- }
956
-
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);
970
- const [logs, setLogs] = useState<Record<string, string[]>>({});
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);
976
-
977
- const addLog = useCallback((stepId: string, message: string) => {
978
- setLogs((prev) => ({ ...prev, [stepId]: [...(prev[stepId] || []), message] }));
979
- }, []);
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
-
994
- useEffect(() => {
995
- let cancelled = false;
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 base = cloudUrl.replace(/\/$/, '');
1014
- const url = base.includes('/api/v1')
1015
- ? `${base}/save-stream`
1016
- : `${base}/api/v1/save-stream`;
1017
-
1018
- const run = async () => {
1019
- try {
1020
- const payload = payloadRef.current;
1021
- if (!payload || cancelled) return;
1022
-
1023
- const res = await fetch(url, {
1024
- method: 'POST',
1025
- headers: {
1026
- 'Content-Type': 'application/json',
1027
- Authorization: `Bearer ${apiKey}`,
1028
- },
1029
- body: JSON.stringify(payload),
1030
- });
1031
-
1032
- if (!res.ok || !res.body) {
1033
- if (!cancelled) {
1034
- setPhase('error');
1035
- setErrorMessage(res.status ? `HTTP ${res.status}` : 'Request failed');
1036
- }
1037
- return;
1038
- }
1039
-
1040
- const reader = res.body.getReader();
1041
- const dec = new TextDecoder();
1042
- let buffer = '';
1043
-
1044
- while (true) {
1045
- const { value, done: streamDone } = await reader.read();
1046
- if (streamDone || cancelled) break;
1047
- buffer += dec.decode(value, { stream: true });
1048
- const parts = buffer.split('\n\n');
1049
- buffer = parts.pop() || '';
1050
-
1051
- for (const part of parts) {
1052
- const parsed = parseSSE(part + '\n\n');
1053
- for (const { event: ev, data: raw } of parsed) {
1054
- if (cancelled) return;
1055
- try {
1056
- const data = JSON.parse(raw) as Record<string, unknown>;
1057
- if (ev === 'step') {
1058
- const id = data.id as StepId | undefined;
1059
- const status = data.status as string | undefined;
1060
- if (id && status === 'running') {
1061
- setActiveStepId(id);
1062
- const idx = DEPLOY_STEPS.findIndex((s) => s.id === id);
1063
- if (idx >= 0) progressRef.current = Math.round((idx / DEPLOY_STEPS.length) * 100);
1064
- setProgress(progressRef.current);
1065
- }
1066
- if (id && status === 'done') {
1067
- setDoneSteps((prev) => [...prev, id].filter(Boolean));
1068
- setActiveStepId(null);
1069
- const idx = DEPLOY_STEPS.findIndex((s) => s.id === id);
1070
- progressRef.current = idx >= 0 ? Math.round(((idx + 1) / DEPLOY_STEPS.length) * 100) : 100;
1071
- setProgress(progressRef.current);
1072
- }
1073
- } else if (ev === 'log' && data.stepId && data.message) {
1074
- addLog(data.stepId as string, data.message as string);
1075
- } else if (ev === 'error') {
1076
- if (!cancelled) {
1077
- setErrorMessage((data.message as string) || 'Errore');
1078
- setPhase('error');
1079
- }
1080
- return;
1081
- } else if (ev === 'done') {
1082
- gotDone = true;
1083
- setDeployUrl((data.deployUrl as string) ?? null);
1084
- setProgress(100);
1085
- setDoneSteps(['commit', 'push', 'build', 'live']);
1086
- setActiveStepId(null);
1087
- setPhase('done');
1088
- return;
1089
- }
1090
- } catch {
1091
- // ignore parse errors
1092
- }
1093
- }
1094
- }
1095
- }
1096
-
1097
- if (!cancelled && !gotDone) {
1098
- setErrorMessage('Operazione interrotta o build non completato.');
1099
- setPhase('error');
1100
- }
1101
- } catch (e) {
1102
- if (!cancelled) {
1103
- setErrorMessage(e instanceof Error ? e.message : 'Errore');
1104
- setPhase('error');
1105
- }
1106
- }
1107
- };
1108
-
1109
- run();
1110
- return () => {
1111
- cancelled = true;
1112
- };
1113
- }, [cloudUrl, apiKey, slug, runId, addLog]);
1114
-
1115
- useEffect(() => {
1116
- if (phase !== 'done') return;
1117
- setCountdown(3);
1118
- const iv = setInterval(() => {
1119
- setCountdown((s) => {
1120
- if (s <= 1) {
1121
- clearInterval(iv);
1122
- onClose();
1123
- return 0;
1124
- }
1125
- return s - 1;
1126
- });
1127
- }, 1000);
1128
- return () => clearInterval(iv);
1129
- }, [phase, onClose]);
1130
-
1131
- const retry = useCallback(() => setRunId((r) => r + 1), []);
1132
-
1133
- return {
1134
- phase,
1135
- currentStep,
1136
- stepState,
1137
- doneSteps,
1138
- progress,
1139
- deployUrl,
1140
- countdown,
1141
- logs,
1142
- errorMessage,
1143
- retry,
1144
- };
1145
- }
1146
-
1147
304
  END_OF_FILE_CONTENT
1148
305
  mkdir -p "src/components/arch-layers"
1149
306
  echo "Creating src/components/arch-layers/View.tsx..."
@@ -3982,47 +3139,6 @@ export { Label }
3982
3139
 
3983
3140
 
3984
3141
 
3985
- END_OF_FILE_CONTENT
3986
- echo "Creating src/components/ui/progress.tsx..."
3987
- cat << 'END_OF_FILE_CONTENT' > "src/components/ui/progress.tsx"
3988
- import * as React from 'react';
3989
- import { cn } from '@/lib/utils';
3990
-
3991
- interface ProgressProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
3992
- value?: number | null;
3993
- indicatorClassName?: string;
3994
- indicatorStyle?: React.CSSProperties;
3995
- }
3996
-
3997
- const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
3998
- ({ className, value, indicatorClassName, indicatorStyle, ...props }, ref) => (
3999
- <div
4000
- ref={ref}
4001
- role="progressbar"
4002
- aria-valuenow={value ?? 0}
4003
- aria-valuemin={0}
4004
- aria-valuemax={100}
4005
- className={cn('relative h-2 w-full overflow-hidden rounded-full bg-secondary', className)}
4006
- {...props}
4007
- >
4008
- <div
4009
- className={cn(
4010
- 'h-full min-w-full bg-primary transition-all duration-700 ease-out',
4011
- indicatorClassName
4012
- )}
4013
- style={{
4014
- width: '100%',
4015
- transform: `translateX(-${100 - (value ?? 0)}%)`,
4016
- ...indicatorStyle,
4017
- }}
4018
- />
4019
- </div>
4020
- )
4021
- );
4022
- Progress.displayName = 'Progress';
4023
-
4024
- export { Progress };
4025
-
4026
3142
  END_OF_FILE_CONTENT
4027
3143
  echo "Creating src/components/ui/select.tsx..."
4028
3144
  cat << 'END_OF_FILE_CONTENT' > "src/components/ui/select.tsx"
@@ -5418,88 +4534,6 @@ html {
5418
4534
  opacity: 0;
5419
4535
  }
5420
4536
 
5421
- /* ==========================================================================
5422
- SAVE DRAWER (prototype-identical animations)
5423
- ========================================================================== */
5424
- @keyframes save-drawer-up {
5425
- from { transform: translateY(100%); opacity: 0; }
5426
- to { transform: translateY(0); opacity: 1; }
5427
- }
5428
- @keyframes save-fade-in {
5429
- from { opacity: 0; }
5430
- to { opacity: 1; }
5431
- }
5432
- @keyframes save-fade-up {
5433
- from { opacity: 0; transform: translateY(8px); }
5434
- to { opacity: 1; transform: translateY(0); }
5435
- }
5436
- @keyframes save-success-pop {
5437
- 0% { transform: scale(0.88); opacity: 0; }
5438
- 60% { transform: scale(1.04); }
5439
- 100% { transform: scale(1); opacity: 1; }
5440
- }
5441
- @keyframes save-ring-expand {
5442
- from { transform: scale(1); opacity: 0.7; }
5443
- to { transform: scale(2.1); opacity: 0; }
5444
- }
5445
- @keyframes save-orb-travel {
5446
- from { left: 0%; }
5447
- to { left: calc(100% - 10px); }
5448
- }
5449
- @keyframes save-glyph-rotate {
5450
- from { transform: rotate(0deg); }
5451
- to { transform: rotate(360deg); }
5452
- }
5453
- @keyframes save-particle-float {
5454
- 0% { transform: translateY(0) scale(1); opacity: 0; }
5455
- 15% { opacity: 1; }
5456
- 100% { transform: translateY(-90px) scale(0.3); opacity: 0; }
5457
- }
5458
- @keyframes save-bar-eq {
5459
- from { transform: scaleY(0.4); }
5460
- to { transform: scaleY(1); }
5461
- }
5462
- @keyframes save-ambient-pulse {
5463
- 0%, 100% { opacity: 0.3; }
5464
- 50% { opacity: 0.65; }
5465
- }
5466
- @keyframes save-shimmer-sweep {
5467
- from { transform: translateX(-100%); }
5468
- to { transform: translateX(250%); }
5469
- }
5470
- @keyframes save-text-in {
5471
- from { opacity: 0; transform: translateX(-6px); }
5472
- to { opacity: 1; transform: translateX(0); }
5473
- }
5474
- @keyframes save-node-glow {
5475
- 0%, 100% { box-shadow: 0 0 12px var(--glow-color, #60a5fa55); }
5476
- 50% { box-shadow: 0 0 28px var(--glow-color, #60a5fa88), 0 0 48px var(--glow-color, #60a5fa22); }
5477
- }
5478
- @keyframes save-burst-ray {
5479
- 0% { transform: rotate(var(--r, 0deg)) translateX(0); opacity: 1; }
5480
- 100% { transform: rotate(var(--r, 0deg)) translateX(56px); opacity: 0; }
5481
- }
5482
-
5483
- .save-drawer-up { animation: save-drawer-up 0.45s cubic-bezier(0.22, 1, 0.36, 1) forwards; }
5484
- .save-fade-in { animation: save-fade-in 0.25s ease forwards; }
5485
- .save-fade-up { animation: save-fade-up 0.35s ease forwards; }
5486
- .save-success-pop { animation: save-success-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }
5487
- .save-text-in { animation: save-text-in 0.3s ease forwards; }
5488
-
5489
- .stroke-dash-30 { stroke-dasharray: 30; stroke-dashoffset: 30; }
5490
- .save-check-draw { animation: save-check-draw 0.4s 0.05s ease forwards; }
5491
- @keyframes save-check-draw {
5492
- to { stroke-dashoffset: 0; }
5493
- }
5494
-
5495
- /* Countdown drain — 3s linear (dopa-save identical) */
5496
- .countdown-bar {
5497
- animation: countdown-drain 3s linear forwards;
5498
- }
5499
- @keyframes countdown-drain {
5500
- from { width: 100%; }
5501
- to { width: 0%; }
5502
- }
5503
4537
 
5504
4538
 
5505
4539
  END_OF_FILE_CONTENT
@@ -1 +1 @@
1
- {"rewrites":[{"source":"/(.*)","destination":"/index.html"}],"headers":[{"source":"/assets/(.*)","headers":[{"key":"Cache-Control","value":"public, max-age=31536000, immutable"}]}]}
1
+ {"rewrites":[{"source":"/(.*)","destination":"/index.html"}],"headers":[{"source":"/assets/(.*)","headers":[{"key":"Cache-Control","value":"public, max-age=31536000, immutable"}]}]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsonpages/cli",
3
- "version": "3.0.49",
3
+ "version": "3.0.50",
4
4
  "description": "The Sovereign CLI Engine for JsonPages.",
5
5
  "type": "module",
6
6
  "bin": {