@jsonpages/cli 3.0.47 → 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.
@@ -439,13 +439,14 @@ END_OF_FILE_CONTENT
439
439
  echo "Creating src/components/SaveDrawer/SaveDrawer.tsx..."
440
440
  cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/SaveDrawer.tsx"
441
441
  /**
442
- * SaveDrawer — Identical to prototype DopaDrawer.
442
+ * SaveDrawer — Identical to prototype DopaDrawer (dopa-save).
443
443
  * Bottom sheet for Cloud save; driven by real SSE (save-stream).
444
- * Done: "Refresh page" + "Chiudi". Error: "Annulla" + "Retry".
444
+ * Done: countdown 3s + "Chiusura in Xs" + Chiudi + Open site → auto-close. Error: Annulla + Retry.
445
445
  */
446
446
  import { useState } from 'react';
447
447
  import { cn } from '@/lib/utils';
448
448
  import { Separator } from '@/components/ui/separator';
449
+ import { Progress } from '@/components/ui/progress';
449
450
  import { DeployNode } from './DeployNode';
450
451
  import { DeployConnector } from './DeployConnector';
451
452
  import { Particles, BuildBars, SuccessBurst, ElapsedTimer } from './Visuals';
@@ -464,7 +465,18 @@ export interface SaveDrawerProps {
464
465
  export function SaveDrawer({ cloudUrl, apiKey, state, slug, onClose }: SaveDrawerProps) {
465
466
  const [burst, setBurst] = useState(false);
466
467
 
467
- const { phase, currentStep, stepState, doneSteps, progress, retry } = useSaveStream({
468
+ const {
469
+ phase,
470
+ currentStep,
471
+ stepState,
472
+ doneSteps,
473
+ progress,
474
+ deployUrl,
475
+ countdown,
476
+ logs,
477
+ errorMessage,
478
+ retry,
479
+ } = useSaveStream({
468
480
  cloudUrl,
469
481
  apiKey,
470
482
  state,
@@ -567,8 +579,12 @@ export function SaveDrawer({ cloudUrl, apiKey, state, slug, onClose }: SaveDrawe
567
579
  </div>
568
580
  ) : isError ? (
569
581
  <>
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>
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>
572
588
  </>
573
589
  ) : currentStep ? (
574
590
  <>
@@ -580,7 +596,24 @@ export function SaveDrawer({ cloudUrl, apiKey, state, slug, onClose }: SaveDrawe
580
596
  </div>
581
597
 
582
598
  <div className="flex flex-col items-end gap-1.5 shrink-0 ml-6">
583
- {!isDone && <ElapsedTimer running={isRunning} />}
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
+ )}
584
617
  </div>
585
618
  </div>
586
619
 
@@ -608,12 +641,21 @@ export function SaveDrawer({ cloudUrl, apiKey, state, slug, onClose }: SaveDrawe
608
641
 
609
642
  <Separator className="mb-5 bg-white/[0.06]" />
610
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
+
611
652
  <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}%`,
653
+ <div className="flex-1">
654
+ <Progress
655
+ value={progress}
656
+ className="h-[2px] bg-white/[0.06]"
657
+ indicatorClassName="transition-none"
658
+ indicatorStyle={{
617
659
  background: `linear-gradient(90deg, ${DEPLOY_STEPS[0].color}, ${activeColor})`,
618
660
  }}
619
661
  />
@@ -624,17 +666,19 @@ export function SaveDrawer({ cloudUrl, apiKey, state, slug, onClose }: SaveDrawe
624
666
  <div className="flex gap-2 save-fade-up">
625
667
  <button
626
668
  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"
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"
629
671
  >
630
- Refresh page
672
+ Chiudi
631
673
  </button>
632
674
  <button
633
675
  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"
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"
636
680
  >
637
- Chiudi
681
+ <span aria-hidden>↗</span> Open site
638
682
  </button>
639
683
  </div>
640
684
  )}
@@ -875,7 +919,9 @@ END_OF_FILE_CONTENT
875
919
  echo "Creating src/components/SaveDrawer/useSaveStream.ts..."
876
920
  cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/useSaveStream.ts"
877
921
  /**
878
- * useSaveStream — Same API as prototype useSaveOrchestrator, but driven by real SSE from save-stream.
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).
879
925
  */
880
926
  import { useState, useCallback, useEffect, useRef } from 'react';
881
927
  import type { ProjectState } from '@jsonpages/core';
@@ -913,15 +959,25 @@ export function useSaveStream({
913
959
  apiKey,
914
960
  state,
915
961
  slug,
916
- onClose: _onClose,
962
+ onClose,
917
963
  }: UseSaveStreamOptions) {
918
964
  const [phase, setPhase] = useState<DeployPhase>('running');
919
965
  const [doneSteps, setDoneSteps] = useState<StepId[]>([]);
920
966
  const [activeStepId, setActiveStepId] = useState<StepId | null>('commit');
921
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);
922
972
  const progressRef = useRef(0);
923
973
  const [runId, setRunId] = useState(0);
924
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
+
925
981
  const stepState = useCallback(
926
982
  (i: number): StepState => {
927
983
  const step = DEPLOY_STEPS[i];
@@ -937,81 +993,113 @@ export function useSaveStream({
937
993
 
938
994
  useEffect(() => {
939
995
  let cancelled = false;
996
+ let gotDone = false;
940
997
  setPhase('running');
941
998
  setDoneSteps([]);
942
999
  setActiveStepId('commit');
943
1000
  setProgress(0);
1001
+ setDeployUrl(null);
1002
+ setCountdown(3);
1003
+ setLogs({});
1004
+ setErrorMessage(null);
944
1005
  progressRef.current = 0;
945
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
+
946
1013
  const url = `${cloudUrl.replace(/\/$/, '')}/save-stream`;
947
1014
 
948
1015
  const run = async () => {
949
- const res = await fetch(url, {
950
- method: 'POST',
951
- headers: {
952
- 'Content-Type': 'application/json',
953
- Authorization: `Bearer ${apiKey}`,
954
- },
955
- body: JSON.stringify({
956
- path: `src/data/pages/${slug}.json`,
957
- content: state.page,
958
- message: `Content update for ${slug} via Visual Editor`,
959
- }),
960
- });
1016
+ try {
1017
+ const payload = payloadRef.current;
1018
+ if (!payload || cancelled) return;
961
1019
 
962
- if (!res.ok || !res.body) {
963
- if (!cancelled) setPhase('error');
964
- return;
965
- }
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
+ }
966
1036
 
967
- const reader = res.body.getReader();
968
- const dec = new TextDecoder();
969
- let buffer = '';
1037
+ const reader = res.body.getReader();
1038
+ const dec = new TextDecoder();
1039
+ let buffer = '';
970
1040
 
971
- while (true) {
972
- const { value, done: streamDone } = await reader.read();
973
- if (streamDone || cancelled) break;
974
- buffer += dec.decode(value, { stream: true });
975
- const parts = buffer.split('\n\n');
976
- buffer = parts.pop() || '';
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() || '';
977
1047
 
978
- for (const part of parts) {
979
- const parsed = parseSSE(part + '\n\n');
980
- for (const { event: ev, data: raw } of parsed) {
981
- if (cancelled) return;
982
- try {
983
- const data = JSON.parse(raw) as Record<string, unknown>;
984
- if (ev === 'step') {
985
- const id = data.id as StepId | undefined;
986
- const status = data.status as string | undefined;
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
- }
993
- if (id && status === 'done') {
994
- setDoneSteps((prev) => [...prev, id].filter(Boolean));
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']);
995
1083
  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);
1084
+ setPhase('done');
1085
+ return;
999
1086
  }
1000
- } else if (ev === 'error') {
1001
- setPhase('error');
1002
- return;
1003
- } else if (ev === 'done') {
1004
- setProgress(100);
1005
- setDoneSteps(['commit', 'push', 'build', 'live']);
1006
- setActiveStepId(null);
1007
- setPhase('done');
1008
- return;
1087
+ } catch {
1088
+ // ignore parse errors
1009
1089
  }
1010
- } catch {
1011
- // ignore
1012
1090
  }
1013
1091
  }
1014
1092
  }
1093
+
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
+ }
1015
1103
  }
1016
1104
  };
1017
1105
 
@@ -1019,7 +1107,23 @@ export function useSaveStream({
1019
1107
  return () => {
1020
1108
  cancelled = true;
1021
1109
  };
1022
- }, [cloudUrl, apiKey, slug, state.page, runId]);
1110
+ }, [cloudUrl, apiKey, slug, runId, addLog]);
1111
+
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]);
1023
1127
 
1024
1128
  const retry = useCallback(() => setRunId((r) => r + 1), []);
1025
1129
 
@@ -1029,6 +1133,10 @@ export function useSaveStream({
1029
1133
  stepState,
1030
1134
  doneSteps,
1031
1135
  progress,
1136
+ deployUrl,
1137
+ countdown,
1138
+ logs,
1139
+ errorMessage,
1032
1140
  retry,
1033
1141
  };
1034
1142
  }
@@ -3871,6 +3979,46 @@ export { Label }
3871
3979
 
3872
3980
 
3873
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
+
3874
4022
  END_OF_FILE_CONTENT
3875
4023
  echo "Creating src/components/ui/select.tsx..."
3876
4024
  cat << 'END_OF_FILE_CONTENT' > "src/components/ui/select.tsx"
@@ -5340,6 +5488,15 @@ html {
5340
5488
  to { stroke-dashoffset: 0; }
5341
5489
  }
5342
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
+ }
5499
+
5343
5500
 
5344
5501
  END_OF_FILE_CONTENT
5345
5502
  mkdir -p "src/lib"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsonpages/cli",
3
- "version": "3.0.47",
3
+ "version": "3.0.48",
4
4
  "description": "The Sovereign CLI Engine for JsonPages.",
5
5
  "type": "module",
6
6
  "bin": {