@jsonpages/cli 3.0.71 → 3.0.73

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.
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
- set -e # Termina se c'è un errore
2
+ set -e
3
3
 
4
- echo "Inizio ricostruzione progetto..."
4
+ echo "Starting project reconstruction..."
5
5
 
6
6
  mkdir -p "docs"
7
7
  echo "Creating docs/01-Onboarding_Client_completo_aggiornato.md..."
@@ -585,17 +585,18 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
585
585
  "dev:clean": "vite --force",
586
586
  "prebuild": "node scripts/sync-pages-to-public.mjs",
587
587
  "build": "tsc && vite build",
588
- "dist": "bash ./src2Code.sh src vercel.json index.html vite.config.ts scripts docs package.json",
588
+ "dist": "bash ./src2Code.sh --template alpha src vercel.json index.html vite.config.ts scripts docs package.json",
589
589
  "preview": "vite preview",
590
590
  "bake:email": "tsx scripts/bake-email.tsx",
591
- "bakemail": "npm run bake:email --"
591
+ "bakemail": "npm run bake:email --",
592
+ "dist:dna": "npm run dist"
592
593
  },
593
594
  "dependencies": {
594
595
  "@tiptap/extension-image": "^2.11.5",
595
596
  "@tiptap/extension-link": "^2.11.5",
596
597
  "@tiptap/react": "^2.11.5",
597
598
  "@tiptap/starter-kit": "^2.11.5",
598
- "@jsonpages/core": "^1.0.58",
599
+ "@jsonpages/core": "^1.0.60",
599
600
  "clsx": "^2.1.1",
600
601
  "lucide-react": "^0.474.0",
601
602
  "react": "^19.0.0",
@@ -924,20 +925,22 @@ cat << 'END_OF_FILE_CONTENT' > "src/App.tsx"
924
925
  * Data from getHydratedData (file-backed or draft); assets from public/assets/images.
925
926
  * Supports Hybrid Persistence: Local Filesystem (Dev) or Cloud Bridge (Prod).
926
927
  */
927
- import { useState, useEffect } from 'react';
928
+ import { useCallback, useEffect, useRef, useState } from 'react';
928
929
  import { JsonPagesEngine } from '@jsonpages/core';
929
- import type { LibraryImageEntry } from '@jsonpages/core';
930
+ import type { JsonPagesConfig, LibraryImageEntry, ProjectState } from '@jsonpages/core';
930
931
  import { ComponentRegistry } from '@/lib/ComponentRegistry';
931
932
  import { SECTION_SCHEMAS } from '@/lib/schemas';
932
933
  import { addSectionConfig } from '@/lib/addSectionConfig';
933
934
  import { getHydratedData } from '@/lib/draftStorage';
934
- import type { JsonPagesConfig, ProjectState } from '@jsonpages/core';
935
935
  import type { SiteConfig, ThemeConfig, MenuConfig } from '@/types';
936
-
936
+ import type { DeployPhase, StepId } from '@/types/deploy';
937
+ import { DEPLOY_STEPS } from '@/lib/deploySteps';
938
+ import { startCloudSaveStream } from '@/lib/cloudSaveStream';
937
939
  import siteData from '@/data/config/site.json';
938
940
  import themeData from '@/data/config/theme.json';
939
941
  import menuData from '@/data/config/menu.json';
940
942
  import { getFilePages } from '@/lib/getFilePages';
943
+ import { DopaDrawer } from '@/components/save-drawer/DopaDrawer';
941
944
 
942
945
  import fontsCss from './fonts.css?inline';
943
946
  import tenantCss from './index.css?inline';
@@ -954,13 +957,41 @@ const filePages = getFilePages();
954
957
  const fileSiteConfig = siteData as unknown as SiteConfig;
955
958
  const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024;
956
959
 
960
+ interface CloudSaveUiState {
961
+ isOpen: boolean;
962
+ phase: DeployPhase;
963
+ currentStepId: StepId | null;
964
+ doneSteps: StepId[];
965
+ progress: number;
966
+ errorMessage?: string;
967
+ deployUrl?: string;
968
+ }
969
+
957
970
  function getInitialData() {
958
971
  return getHydratedData(TENANT_ID, filePages, fileSiteConfig);
959
972
  }
960
973
 
974
+ function getInitialCloudSaveUiState(): CloudSaveUiState {
975
+ return {
976
+ isOpen: false,
977
+ phase: 'idle',
978
+ currentStepId: null,
979
+ doneSteps: [],
980
+ progress: 0,
981
+ };
982
+ }
983
+
984
+ function stepProgress(doneSteps: StepId[]): number {
985
+ return Math.round((doneSteps.length / DEPLOY_STEPS.length) * 100);
986
+ }
987
+
961
988
  function App() {
962
989
  const [{ pages, siteConfig }] = useState(getInitialData);
963
990
  const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
991
+ const [cloudSaveUi, setCloudSaveUi] = useState<CloudSaveUiState>(getInitialCloudSaveUiState);
992
+ const activeCloudSaveController = useRef<AbortController | null>(null);
993
+ const pendingCloudSave = useRef<{ state: ProjectState; slug: string } | null>(null);
994
+ const isCloudMode = Boolean(CLOUD_API_URL && CLOUD_API_KEY);
964
995
 
965
996
  useEffect(() => {
966
997
  // In Cloud mode, listing assets might be different or disabled for MVP
@@ -970,11 +1001,111 @@ function App() {
970
1001
  .then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
971
1002
  .catch(() => setAssetsManifest([]));
972
1003
  }, []);
973
-
974
- console.log("🔍 DEBUG ENV:", {
975
- URL: import.meta.env.VITE_JSONPAGES_CLOUD_URL,
976
- KEY: import.meta.env.VITE_JSONPAGES_API_KEY ? "PRESENT" : "MISSING"
977
- });
1004
+
1005
+ useEffect(() => {
1006
+ return () => {
1007
+ activeCloudSaveController.current?.abort();
1008
+ };
1009
+ }, []);
1010
+
1011
+ const runCloudSave = useCallback(
1012
+ async (
1013
+ payload: { state: ProjectState; slug: string },
1014
+ rejectOnError: boolean
1015
+ ): Promise<void> => {
1016
+ if (!CLOUD_API_URL || !CLOUD_API_KEY) {
1017
+ const noCloudError = new Error('Cloud mode is not configured.');
1018
+ if (rejectOnError) throw noCloudError;
1019
+ return;
1020
+ }
1021
+
1022
+ pendingCloudSave.current = payload;
1023
+ activeCloudSaveController.current?.abort();
1024
+ const controller = new AbortController();
1025
+ activeCloudSaveController.current = controller;
1026
+
1027
+ setCloudSaveUi({
1028
+ isOpen: true,
1029
+ phase: 'running',
1030
+ currentStepId: null,
1031
+ doneSteps: [],
1032
+ progress: 0,
1033
+ });
1034
+
1035
+ try {
1036
+ await startCloudSaveStream({
1037
+ apiBaseUrl: CLOUD_API_URL,
1038
+ apiKey: CLOUD_API_KEY,
1039
+ path: `src/data/pages/${payload.slug}.json`,
1040
+ content: payload.state.page,
1041
+ message: `Content update for ${payload.slug} via Visual Editor`,
1042
+ signal: controller.signal,
1043
+ onStep: (event) => {
1044
+ setCloudSaveUi((prev) => {
1045
+ if (event.status === 'running') {
1046
+ return {
1047
+ ...prev,
1048
+ isOpen: true,
1049
+ phase: 'running',
1050
+ currentStepId: event.id,
1051
+ errorMessage: undefined,
1052
+ };
1053
+ }
1054
+
1055
+ if (prev.doneSteps.includes(event.id)) {
1056
+ return prev;
1057
+ }
1058
+
1059
+ const nextDone = [...prev.doneSteps, event.id];
1060
+ return {
1061
+ ...prev,
1062
+ isOpen: true,
1063
+ phase: 'running',
1064
+ currentStepId: event.id,
1065
+ doneSteps: nextDone,
1066
+ progress: stepProgress(nextDone),
1067
+ };
1068
+ });
1069
+ },
1070
+ onDone: (event) => {
1071
+ const completed = DEPLOY_STEPS.map((step) => step.id);
1072
+ setCloudSaveUi({
1073
+ isOpen: true,
1074
+ phase: 'done',
1075
+ currentStepId: 'live',
1076
+ doneSteps: completed,
1077
+ progress: 100,
1078
+ deployUrl: event.deployUrl,
1079
+ });
1080
+ },
1081
+ });
1082
+ } catch (error: unknown) {
1083
+ const message = error instanceof Error ? error.message : 'Cloud save failed.';
1084
+ setCloudSaveUi((prev) => ({
1085
+ ...prev,
1086
+ isOpen: true,
1087
+ phase: 'error',
1088
+ errorMessage: message,
1089
+ }));
1090
+ if (rejectOnError) throw new Error(message);
1091
+ } finally {
1092
+ if (activeCloudSaveController.current === controller) {
1093
+ activeCloudSaveController.current = null;
1094
+ }
1095
+ }
1096
+ },
1097
+ []
1098
+ );
1099
+
1100
+ const closeCloudDrawer = useCallback(() => {
1101
+ setCloudSaveUi(getInitialCloudSaveUiState());
1102
+ }, []);
1103
+
1104
+ const retryCloudSave = useCallback(() => {
1105
+ if (!pendingCloudSave.current) return;
1106
+ void runCloudSave(pendingCloudSave.current, false);
1107
+ }, [runCloudSave]);
1108
+
978
1109
  const config: JsonPagesConfig = {
979
1110
  tenantId: TENANT_ID,
980
1111
  registry: ComponentRegistry as JsonPagesConfig['registry'],
@@ -987,30 +1118,9 @@ console.log("🔍 DEBUG ENV:", {
987
1118
  addSection: addSectionConfig,
988
1119
  persistence: {
989
1120
  async saveToFile(state: ProjectState, slug: string): Promise<void> {
990
-
991
1121
  // ☁️ SCENARIO A: CLOUD BRIDGE (Production)
992
- if (CLOUD_API_URL && CLOUD_API_KEY) {
993
- console.log(`☁️ Saving ${slug} via Cloud Bridge...`);
994
-
995
- const res = await fetch(`${CLOUD_API_URL}/save`, {
996
- method: 'POST',
997
- headers: {
998
- 'Content-Type': 'application/json',
999
- 'Authorization': `Bearer ${CLOUD_API_KEY}`
1000
- },
1001
- body: JSON.stringify({
1002
- // Mapping logical slug to physical path in repo
1003
- path: `src/data/pages/${slug}.json`,
1004
- // We save the page config specifically
1005
- content: state.page,
1006
- message: `Content update for ${slug} via Visual Editor`
1007
- }),
1008
- });
1009
-
1010
- if (!res.ok) {
1011
- const err = await res.json().catch(() => ({}));
1012
- throw new Error(err.error || `Cloud save failed: ${res.status}`);
1013
- }
1122
+ if (isCloudMode) {
1123
+ await runCloudSave({ state, slug }, true);
1014
1124
  return;
1015
1125
  }
1016
1126
 
@@ -1056,11 +1166,27 @@ console.log("🔍 DEBUG ENV:", {
1056
1166
  },
1057
1167
  };
1058
1168
 
1059
- return <JsonPagesEngine config={config} />;
1169
+ return (
1170
+ <>
1171
+ <JsonPagesEngine config={config} />
1172
+ <DopaDrawer
1173
+ isOpen={cloudSaveUi.isOpen}
1174
+ phase={cloudSaveUi.phase}
1175
+ currentStepId={cloudSaveUi.currentStepId}
1176
+ doneSteps={cloudSaveUi.doneSteps}
1177
+ progress={cloudSaveUi.progress}
1178
+ errorMessage={cloudSaveUi.errorMessage}
1179
+ deployUrl={cloudSaveUi.deployUrl}
1180
+ onClose={closeCloudDrawer}
1181
+ onRetry={retryCloudSave}
1182
+ />
1183
+ </>
1184
+ );
1060
1185
  }
1061
1186
 
1062
1187
  export default App;
1063
1188
 
1189
+
1064
1190
  END_OF_FILE_CONTENT
1065
1191
  echo "Creating src/App_.tsx..."
1066
1192
  cat << 'END_OF_FILE_CONTENT' > "src/App_.tsx"
@@ -3933,6 +4059,1089 @@ export type ProductTriadSettings = z.infer<typeof BaseSectionSettingsSchema>;
3933
4059
 
3934
4060
  END_OF_FILE_CONTENT
3935
4061
  mkdir -p "src/components/save-drawer"
4062
+ echo "Creating src/components/save-drawer/DeployConnector.tsx..."
4063
+ cat << 'END_OF_FILE_CONTENT' > "src/components/save-drawer/DeployConnector.tsx"
4064
+ import type { StepState } from '@/types/deploy';
4065
+
4066
+ interface DeployConnectorProps {
4067
+ fromState: StepState;
4068
+ toState: StepState;
4069
+ color: string;
4070
+ }
4071
+
4072
+ export function DeployConnector({ fromState, toState, color }: DeployConnectorProps) {
4073
+ const filled = fromState === 'done' && toState === 'done';
4074
+ const filling = fromState === 'done' && toState === 'active';
4075
+ const lit = filled || filling;
4076
+
4077
+ return (
4078
+ <div className="jp-drawer-connector">
4079
+ <div className="jp-drawer-connector-base" />
4080
+
4081
+ <div
4082
+ className="jp-drawer-connector-fill"
4083
+ style={{
4084
+ background: `linear-gradient(90deg, ${color}cc, ${color}66)`,
4085
+ width: filled ? '100%' : filling ? '100%' : '0%',
4086
+ transition: filling ? 'width 2s cubic-bezier(0.4,0,0.2,1)' : 'none',
4087
+ boxShadow: lit ? `0 0 8px ${color}77` : 'none',
4088
+ }}
4089
+ />
4090
+
4091
+ {filling && (
4092
+ <div
4093
+ className="jp-drawer-connector-orb"
4094
+ style={{
4095
+ background: color,
4096
+ boxShadow: `0 0 14px ${color}, 0 0 28px ${color}88`,
4097
+ animation: 'orb-travel 2s cubic-bezier(0.4,0,0.6,1) forwards',
4098
+ }}
4099
+ />
4100
+ )}
4101
+ </div>
4102
+ );
4103
+ }
4104
+
4105
+
4106
+ END_OF_FILE_CONTENT
4107
+ echo "Creating src/components/save-drawer/DeployNode.tsx..."
4108
+ cat << 'END_OF_FILE_CONTENT' > "src/components/save-drawer/DeployNode.tsx"
4109
+ import type { CSSProperties } from 'react';
4110
+ import type { DeployStep, StepState } from '@/types/deploy';
4111
+
4112
+ interface DeployNodeProps {
4113
+ step: DeployStep;
4114
+ state: StepState;
4115
+ }
4116
+
4117
+ export function DeployNode({ step, state }: DeployNodeProps) {
4118
+ const isActive = state === 'active';
4119
+ const isDone = state === 'done';
4120
+ const isPending = state === 'pending';
4121
+
4122
+ return (
4123
+ <div className="jp-drawer-node-wrap">
4124
+ <div
4125
+ className={`jp-drawer-node ${isPending ? 'jp-drawer-node-pending' : ''}`}
4126
+ style={
4127
+ {
4128
+ background: isDone ? step.color : isActive ? 'rgba(0,0,0,0.5)' : undefined,
4129
+ borderWidth: isDone ? 0 : 1,
4130
+ borderColor: isActive ? `${step.color}80` : undefined,
4131
+ boxShadow: isDone
4132
+ ? `0 0 20px ${step.color}55, 0 0 40px ${step.color}22`
4133
+ : isActive
4134
+ ? `0 0 14px ${step.color}33`
4135
+ : undefined,
4136
+ animation: isActive ? 'node-glow 2s ease infinite' : undefined,
4137
+ ['--glow-color' as string]: step.color,
4138
+ } as CSSProperties
4139
+ }
4140
+ >
4141
+ {isDone && (
4142
+ <svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" aria-label="Done">
4143
+ <path
4144
+ className="stroke-dash-30 animate-check-draw"
4145
+ d="M5 13l4 4L19 7"
4146
+ stroke="#0a0f1a"
4147
+ strokeWidth="2.5"
4148
+ strokeLinecap="round"
4149
+ strokeLinejoin="round"
4150
+ />
4151
+ </svg>
4152
+ )}
4153
+
4154
+ {isActive && (
4155
+ <span
4156
+ className="jp-drawer-node-glyph jp-drawer-node-glyph-active"
4157
+ style={{ color: step.color, animation: 'glyph-rotate 9s linear infinite' }}
4158
+ aria-hidden
4159
+ >
4160
+ {step.glyph}
4161
+ </span>
4162
+ )}
4163
+
4164
+ {isPending && (
4165
+ <span className="jp-drawer-node-glyph jp-drawer-node-glyph-pending" aria-hidden>
4166
+ {step.glyph}
4167
+ </span>
4168
+ )}
4169
+
4170
+ {isActive && (
4171
+ <span
4172
+ className="jp-drawer-node-ring"
4173
+ style={{
4174
+ inset: -7,
4175
+ borderColor: `${step.color}50`,
4176
+ animation: 'ring-expand 2s ease-out infinite',
4177
+ }}
4178
+ />
4179
+ )}
4180
+ </div>
4181
+
4182
+ <span
4183
+ className="jp-drawer-node-label"
4184
+ style={{ color: isDone ? step.color : isActive ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.18)' }}
4185
+ >
4186
+ {step.label}
4187
+ </span>
4188
+ </div>
4189
+ );
4190
+ }
4191
+
4192
+
4193
+ END_OF_FILE_CONTENT
4194
+ echo "Creating src/components/save-drawer/DopaDrawer.tsx..."
4195
+ cat << 'END_OF_FILE_CONTENT' > "src/components/save-drawer/DopaDrawer.tsx"
4196
+ import { useEffect, useMemo, useState } from 'react';
4197
+ import { createPortal } from 'react-dom';
4198
+ import type { StepId, StepState } from '@/types/deploy';
4199
+ import { DEPLOY_STEPS } from '@/lib/deploySteps';
4200
+ import fontsCss from '@/fonts.css?inline';
4201
+ import saverStyleCss from './saverStyle.css?inline';
4202
+ import { DeployNode } from './DeployNode';
4203
+ import { DeployConnector } from './DeployConnector';
4204
+ import { BuildBars, ElapsedTimer, Particles, SuccessBurst } from './Visuals';
4205
+
4206
+ interface DopaDrawerProps {
4207
+ isOpen: boolean;
4208
+ phase: 'idle' | 'running' | 'done' | 'error';
4209
+ currentStepId: StepId | null;
4210
+ doneSteps: StepId[];
4211
+ progress: number;
4212
+ errorMessage?: string;
4213
+ deployUrl?: string;
4214
+ onClose: () => void;
4215
+ onRetry: () => void;
4216
+ }
4217
+
4218
+ export function DopaDrawer({
4219
+ isOpen,
4220
+ phase,
4221
+ currentStepId,
4222
+ doneSteps,
4223
+ progress,
4224
+ errorMessage,
4225
+ deployUrl,
4226
+ onClose,
4227
+ onRetry,
4228
+ }: DopaDrawerProps) {
4229
+ const [shadowMount, setShadowMount] = useState<HTMLElement | null>(null);
4230
+ const [burst, setBurst] = useState(false);
4231
+ const [countdown, setCountdown] = useState(3);
4232
+
4233
+ const isRunning = phase === 'running';
4234
+ const isDone = phase === 'done';
4235
+ const isError = phase === 'error';
4236
+
4237
+ useEffect(() => {
4238
+ const host = document.createElement('div');
4239
+ host.setAttribute('data-jp-drawer-shadow-host', '');
4240
+
4241
+ const shadowRoot = host.attachShadow({ mode: 'open' });
4242
+ const style = document.createElement('style');
4243
+ style.textContent = `${fontsCss}\n${saverStyleCss}`;
4244
+
4245
+ const mount = document.createElement('div');
4246
+ shadowRoot.append(style, mount);
4247
+
4248
+ document.body.appendChild(host);
4249
+ setShadowMount(mount);
4250
+
4251
+ return () => {
4252
+ setShadowMount(null);
4253
+ host.remove();
4254
+ };
4255
+ }, []);
4256
+
4257
+ useEffect(() => {
4258
+ if (!isOpen) {
4259
+ setBurst(false);
4260
+ setCountdown(3);
4261
+ return;
4262
+ }
4263
+ if (isDone) setBurst(true);
4264
+ }, [isDone, isOpen]);
4265
+
4266
+ useEffect(() => {
4267
+ if (!isOpen || !isDone) return;
4268
+ setCountdown(3);
4269
+ const interval = window.setInterval(() => {
4270
+ setCountdown((prev) => {
4271
+ if (prev <= 1) {
4272
+ window.clearInterval(interval);
4273
+ onClose();
4274
+ return 0;
4275
+ }
4276
+ return prev - 1;
4277
+ });
4278
+ }, 1000);
4279
+ return () => window.clearInterval(interval);
4280
+ }, [isDone, isOpen, onClose]);
4281
+
4282
+ const currentStep = useMemo(
4283
+ () => DEPLOY_STEPS.find((step) => step.id === currentStepId) ?? null,
4284
+ [currentStepId]
4285
+ );
4286
+
4287
+ const activeColor = isDone ? '#34d399' : isError ? '#f87171' : (currentStep?.color ?? '#60a5fa');
4288
+ const particleCount = isDone ? 40 : doneSteps.length === 3 ? 28 : doneSteps.length === 2 ? 16 : doneSteps.length === 1 ? 8 : 4;
4289
+
4290
+ const stepState = (index: number): StepState => {
4291
+ const step = DEPLOY_STEPS[index];
4292
+ if (doneSteps.includes(step.id)) return 'done';
4293
+ if (phase === 'running' && currentStepId === step.id) return 'active';
4294
+ return 'pending';
4295
+ };
4296
+
4297
+ if (!shadowMount || !isOpen || phase === 'idle') return null;
4298
+
4299
+ return createPortal(
4300
+ <div className="jp-drawer-root">
4301
+ <div
4302
+ className="jp-drawer-overlay animate-fade-in"
4303
+ onClick={isDone || isError ? onClose : undefined}
4304
+ aria-hidden
4305
+ />
4306
+
4307
+ <div
4308
+ role="status"
4309
+ aria-live="polite"
4310
+ aria-label={isDone ? 'Deploy completed' : isError ? 'Deploy failed' : 'Deploying'}
4311
+ className="jp-drawer-shell animate-drawer-up"
4312
+ style={{ bottom: 'max(2.25rem, env(safe-area-inset-bottom))' }}
4313
+ >
4314
+ <div
4315
+ className="jp-drawer-card"
4316
+ style={{
4317
+ backgroundColor: 'hsl(222 18% 7%)',
4318
+ 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`,
4319
+ transition: 'box-shadow 1.2s ease',
4320
+ }}
4321
+ >
4322
+ <div
4323
+ className="jp-drawer-ambient"
4324
+ style={{
4325
+ background: `radial-gradient(ellipse 70% 60% at 50% 110%, ${activeColor}12 0%, transparent 65%)`,
4326
+ transition: 'background 1.5s ease',
4327
+ animation: 'ambient-pulse 3.5s ease infinite',
4328
+ }}
4329
+ aria-hidden
4330
+ />
4331
+
4332
+ {isDone && (
4333
+ <div className="jp-drawer-shimmer" aria-hidden>
4334
+ <div
4335
+ className="jp-drawer-shimmer-bar"
4336
+ style={{
4337
+ background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.04), transparent)',
4338
+ animation: 'shimmer-sweep 1.4s 0.1s ease forwards',
4339
+ }}
4340
+ />
4341
+ </div>
4342
+ )}
4343
+
4344
+ <Particles count={particleCount} color={activeColor} />
4345
+ {burst && <SuccessBurst />}
4346
+
4347
+ <div className="jp-drawer-content">
4348
+ <div className="jp-drawer-header">
4349
+ <div className="jp-drawer-header-left">
4350
+ <div className="jp-drawer-status" style={{ color: activeColor }}>
4351
+ <span
4352
+ className="jp-drawer-status-dot"
4353
+ style={{
4354
+ background: activeColor,
4355
+ boxShadow: `0 0 6px ${activeColor}`,
4356
+ animation: isRunning ? 'ambient-pulse 1.5s ease infinite' : 'none',
4357
+ }}
4358
+ aria-hidden
4359
+ />
4360
+ {isDone ? 'Live' : isError ? 'Build failed' : currentStep?.verb ?? 'Saving'}
4361
+ </div>
4362
+
4363
+ <div key={currentStep?.id ?? phase} className="jp-drawer-copy animate-text-in">
4364
+ {isDone ? (
4365
+ <div className="animate-success-pop">
4366
+ <p className="jp-drawer-copy-title jp-drawer-copy-title-lg">Your content is live.</p>
4367
+ <p className="jp-drawer-copy-sub">Deployed to production successfully</p>
4368
+ </div>
4369
+ ) : isError ? (
4370
+ <>
4371
+ <p className="jp-drawer-copy-title jp-drawer-copy-title-md">Deploy failed at build.</p>
4372
+ <p className="jp-drawer-copy-sub jp-drawer-copy-sub-error">{errorMessage ?? 'Check your Vercel logs or retry below'}</p>
4373
+ </>
4374
+ ) : currentStep ? (
4375
+ <>
4376
+ <p className="jp-drawer-poem-line jp-drawer-poem-line-1">{currentStep.poem[0]}</p>
4377
+ <p className="jp-drawer-poem-line jp-drawer-poem-line-2">{currentStep.poem[1]}</p>
4378
+ </>
4379
+ ) : null}
4380
+ </div>
4381
+ </div>
4382
+
4383
+ <div className="jp-drawer-right">
4384
+ {isDone ? (
4385
+ <div className="jp-drawer-countdown-wrap animate-fade-up">
4386
+ <span className="jp-drawer-countdown-text" aria-live="polite">
4387
+ Chiusura in {countdown}s
4388
+ </span>
4389
+ <div className="jp-drawer-countdown-track">
4390
+ <div className="jp-drawer-countdown-bar countdown-bar" style={{ boxShadow: '0 0 6px #34d39988' }} />
4391
+ </div>
4392
+ </div>
4393
+ ) : (
4394
+ <ElapsedTimer running={isRunning} />
4395
+ )}
4396
+ </div>
4397
+ </div>
4398
+
4399
+ <div className="jp-drawer-track-row">
4400
+ {DEPLOY_STEPS.map((step, i) => (
4401
+ <div key={step.id} style={{ display: 'flex', alignItems: 'center', flex: i < DEPLOY_STEPS.length - 1 ? 1 : 'none' }}>
4402
+ <DeployNode step={step} state={stepState(i)} />
4403
+ {i < DEPLOY_STEPS.length - 1 && (
4404
+ <DeployConnector fromState={stepState(i)} toState={stepState(i + 1)} color={DEPLOY_STEPS[i + 1].color} />
4405
+ )}
4406
+ </div>
4407
+ ))}
4408
+ </div>
4409
+
4410
+ <div className="jp-drawer-bars-wrap">
4411
+ <BuildBars active={stepState(2) === 'active'} />
4412
+ </div>
4413
+
4414
+ <div className="jp-drawer-separator" />
4415
+
4416
+ <div className="jp-drawer-footer">
4417
+ <div className="jp-drawer-progress">
4418
+ <div
4419
+ className="jp-drawer-progress-indicator"
4420
+ style={{
4421
+ width: `${Math.max(0, Math.min(100, progress))}%`,
4422
+ background: `linear-gradient(90deg, ${DEPLOY_STEPS[0].color}, ${activeColor})`,
4423
+ }}
4424
+ />
4425
+ </div>
4426
+
4427
+ <div className="jp-drawer-cta">
4428
+ {isDone && (
4429
+ <div className="jp-drawer-btn-row animate-fade-up">
4430
+ <button type="button" className="jp-drawer-btn jp-drawer-btn-secondary" onClick={onClose}>
4431
+ Chiudi
4432
+ </button>
4433
+ <button
4434
+ type="button"
4435
+ className="jp-drawer-btn jp-drawer-btn-emerald"
4436
+ onClick={() => {
4437
+ if (deployUrl) window.open(deployUrl, '_blank', 'noopener,noreferrer');
4438
+ }}
4439
+ disabled={!deployUrl}
4440
+ >
4441
+ <span aria-hidden>↗</span> Open site
4442
+ </button>
4443
+ </div>
4444
+ )}
4445
+
4446
+ {isError && (
4447
+ <div className="jp-drawer-btn-row animate-fade-up">
4448
+ <button type="button" className="jp-drawer-btn jp-drawer-btn-ghost" onClick={onClose}>
4449
+ Annulla
4450
+ </button>
4451
+ <button type="button" className="jp-drawer-btn jp-drawer-btn-destructive" onClick={onRetry}>
4452
+ Retry
4453
+ </button>
4454
+ </div>
4455
+ )}
4456
+
4457
+ {isRunning && (
4458
+ <span className="jp-drawer-running-step" aria-hidden>
4459
+ {doneSteps.length + 1} / {DEPLOY_STEPS.length}
4460
+ </span>
4461
+ )}
4462
+ </div>
4463
+ </div>
4464
+ </div>
4465
+ </div>
4466
+ </div>
4467
+ </div>,
4468
+ shadowMount
4469
+ );
4470
+ }
4471
+
4472
+
4473
+ END_OF_FILE_CONTENT
4474
+ echo "Creating src/components/save-drawer/Visuals.tsx..."
4475
+ cat << 'END_OF_FILE_CONTENT' > "src/components/save-drawer/Visuals.tsx"
4476
+ import { useEffect, useRef, useState } from 'react';
4477
+ import type { CSSProperties } from 'react';
4478
+
4479
+ interface Particle {
4480
+ id: number;
4481
+ x: number;
4482
+ y: number;
4483
+ size: number;
4484
+ dur: number;
4485
+ delay: number;
4486
+ }
4487
+
4488
+ const PARTICLE_POOL: Particle[] = Array.from({ length: 44 }, (_, i) => ({
4489
+ id: i,
4490
+ x: 5 + Math.random() * 90,
4491
+ y: 15 + Math.random() * 70,
4492
+ size: 1.5 + Math.random() * 2.5,
4493
+ dur: 2.8 + Math.random() * 3.5,
4494
+ delay: Math.random() * 4,
4495
+ }));
4496
+
4497
+ interface ParticlesProps {
4498
+ count: number;
4499
+ color: string;
4500
+ }
4501
+
4502
+ export function Particles({ count, color }: ParticlesProps) {
4503
+ return (
4504
+ <div className="jp-drawer-particles" aria-hidden>
4505
+ {PARTICLE_POOL.slice(0, count).map((particle) => (
4506
+ <div
4507
+ key={particle.id}
4508
+ className="jp-drawer-particle"
4509
+ style={{
4510
+ left: `${particle.x}%`,
4511
+ bottom: `${particle.y}%`,
4512
+ width: particle.size,
4513
+ height: particle.size,
4514
+ background: color,
4515
+ boxShadow: `0 0 ${particle.size * 3}px ${color}`,
4516
+ opacity: 0,
4517
+ animation: `particle-float ${particle.dur}s ${particle.delay}s ease-out infinite`,
4518
+ }}
4519
+ />
4520
+ ))}
4521
+ </div>
4522
+ );
4523
+ }
4524
+
4525
+ 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];
4526
+
4527
+ interface BuildBarsProps {
4528
+ active: boolean;
4529
+ }
4530
+
4531
+ export function BuildBars({ active }: BuildBarsProps) {
4532
+ if (!active) return <div className="jp-drawer-bars-placeholder" />;
4533
+
4534
+ return (
4535
+ <div className="jp-drawer-bars" aria-hidden>
4536
+ {BAR_H.map((height, i) => (
4537
+ <div
4538
+ key={i}
4539
+ className="jp-drawer-bar"
4540
+ style={{
4541
+ height: `${height * 100}%`,
4542
+ animation: `bar-eq ${0.42 + i * 0.06}s ${i * 0.04}s ease-in-out infinite alternate`,
4543
+ }}
4544
+ />
4545
+ ))}
4546
+ </div>
4547
+ );
4548
+ }
4549
+
4550
+ const BURST_COLORS = ['#34d399', '#60a5fa', '#a78bfa', '#f59e0b', '#f472b6'];
4551
+
4552
+ export function SuccessBurst() {
4553
+ return (
4554
+ <div className="jp-drawer-burst" aria-hidden>
4555
+ {Array.from({ length: 16 }).map((_, i) => (
4556
+ <div
4557
+ key={i}
4558
+ className="jp-drawer-burst-dot"
4559
+ style={
4560
+ {
4561
+ background: BURST_COLORS[i % BURST_COLORS.length],
4562
+ ['--r' as string]: `${i * 22.5}deg`,
4563
+ animation: `burst-ray 0.85s ${i * 0.03}s cubic-bezier(0,0.6,0.5,1) forwards`,
4564
+ transform: `rotate(${i * 22.5}deg)`,
4565
+ transformOrigin: '50% 50%',
4566
+ opacity: 0,
4567
+ } as CSSProperties
4568
+ }
4569
+ />
4570
+ ))}
4571
+ </div>
4572
+ );
4573
+ }
4574
+
4575
+ interface ElapsedTimerProps {
4576
+ running: boolean;
4577
+ }
4578
+
4579
+ export function ElapsedTimer({ running }: ElapsedTimerProps) {
4580
+ const [elapsed, setElapsed] = useState(0);
4581
+ const startRef = useRef<number | null>(null);
4582
+ const rafRef = useRef<number | null>(null);
4583
+
4584
+ useEffect(() => {
4585
+ if (!running) return;
4586
+ if (!startRef.current) startRef.current = performance.now();
4587
+
4588
+ const tick = () => {
4589
+ if (!startRef.current) return;
4590
+ setElapsed(Math.floor((performance.now() - startRef.current) / 1000));
4591
+ rafRef.current = requestAnimationFrame(tick);
4592
+ };
4593
+
4594
+ rafRef.current = requestAnimationFrame(tick);
4595
+ return () => {
4596
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
4597
+ };
4598
+ }, [running]);
4599
+
4600
+ const sec = String(elapsed % 60).padStart(2, '0');
4601
+ const min = String(Math.floor(elapsed / 60)).padStart(2, '0');
4602
+ return <span className="jp-drawer-elapsed" aria-live="off">{min}:{sec}</span>;
4603
+ }
4604
+
4605
+
4606
+ END_OF_FILE_CONTENT
4607
+ echo "Creating src/components/save-drawer/saverStyle.css..."
4608
+ cat << 'END_OF_FILE_CONTENT' > "src/components/save-drawer/saverStyle.css"
4609
+ /* Save Drawer strict_full isolated stylesheet */
4610
+
4611
+ .jp-drawer-root {
4612
+ --background: 222 18% 6%;
4613
+ --foreground: 210 20% 96%;
4614
+ --card: 222 16% 8%;
4615
+ --card-foreground: 210 20% 96%;
4616
+ --primary: 0 0% 95%;
4617
+ --primary-foreground: 222 18% 6%;
4618
+ --secondary: 220 14% 13%;
4619
+ --secondary-foreground: 210 20% 96%;
4620
+ --destructive: 0 72% 51%;
4621
+ --destructive-foreground: 0 0% 98%;
4622
+ --border: 220 14% 13%;
4623
+ --radius: 0.6rem;
4624
+ font-family: 'Geist', system-ui, sans-serif;
4625
+ }
4626
+
4627
+ .jp-drawer-overlay {
4628
+ position: fixed;
4629
+ inset: 0;
4630
+ z-index: 2147483600;
4631
+ background: rgb(0 0 0 / 0.4);
4632
+ backdrop-filter: blur(2px);
4633
+ }
4634
+
4635
+ .jp-drawer-shell {
4636
+ position: fixed;
4637
+ left: 0;
4638
+ right: 0;
4639
+ z-index: 2147483601;
4640
+ display: flex;
4641
+ justify-content: center;
4642
+ padding: 0 1rem;
4643
+ }
4644
+
4645
+ .jp-drawer-card {
4646
+ position: relative;
4647
+ width: 100%;
4648
+ max-width: 31rem;
4649
+ overflow: hidden;
4650
+ border-radius: 1rem;
4651
+ border: 1px solid rgb(255 255 255 / 0.07);
4652
+ }
4653
+
4654
+ .jp-drawer-ambient {
4655
+ position: absolute;
4656
+ inset: 0;
4657
+ pointer-events: none;
4658
+ }
4659
+
4660
+ .jp-drawer-shimmer {
4661
+ position: absolute;
4662
+ inset: 0;
4663
+ overflow: hidden;
4664
+ pointer-events: none;
4665
+ }
4666
+
4667
+ .jp-drawer-shimmer-bar {
4668
+ position: absolute;
4669
+ inset-block: 0;
4670
+ width: 35%;
4671
+ }
4672
+
4673
+ .jp-drawer-content {
4674
+ position: relative;
4675
+ z-index: 10;
4676
+ padding: 2rem 2rem 1.75rem;
4677
+ }
4678
+
4679
+ .jp-drawer-header {
4680
+ margin-bottom: 1.5rem;
4681
+ display: flex;
4682
+ align-items: flex-start;
4683
+ justify-content: space-between;
4684
+ }
4685
+
4686
+ .jp-drawer-header-left {
4687
+ display: flex;
4688
+ flex-direction: column;
4689
+ gap: 0.625rem;
4690
+ }
4691
+
4692
+ .jp-drawer-status {
4693
+ display: flex;
4694
+ align-items: center;
4695
+ gap: 0.5rem;
4696
+ font-size: 0.75rem;
4697
+ font-weight: 700;
4698
+ text-transform: uppercase;
4699
+ letter-spacing: 0.1em;
4700
+ transition: color 0.5s;
4701
+ }
4702
+
4703
+ .jp-drawer-status-dot {
4704
+ width: 0.375rem;
4705
+ height: 0.375rem;
4706
+ border-radius: 9999px;
4707
+ display: inline-block;
4708
+ }
4709
+
4710
+ .jp-drawer-copy {
4711
+ min-height: 52px;
4712
+ }
4713
+
4714
+ .jp-drawer-copy-title {
4715
+ margin: 0;
4716
+ color: white;
4717
+ line-height: 1.25;
4718
+ font-weight: 600;
4719
+ }
4720
+
4721
+ .jp-drawer-copy-title-lg {
4722
+ font-size: 1.125rem;
4723
+ }
4724
+
4725
+ .jp-drawer-copy-title-md {
4726
+ font-size: 1rem;
4727
+ }
4728
+
4729
+ .jp-drawer-copy-sub {
4730
+ margin: 0.125rem 0 0;
4731
+ color: rgb(255 255 255 / 0.4);
4732
+ font-size: 0.875rem;
4733
+ }
4734
+
4735
+ .jp-drawer-copy-sub-error {
4736
+ color: rgb(255 255 255 / 0.35);
4737
+ }
4738
+
4739
+ .jp-drawer-poem-line {
4740
+ margin: 0;
4741
+ font-size: 0.875rem;
4742
+ font-weight: 300;
4743
+ line-height: 1.5;
4744
+ }
4745
+
4746
+ .jp-drawer-poem-line-1 {
4747
+ color: rgb(255 255 255 / 0.55);
4748
+ }
4749
+
4750
+ .jp-drawer-poem-line-2 {
4751
+ color: rgb(255 255 255 / 0.3);
4752
+ }
4753
+
4754
+ .jp-drawer-right {
4755
+ margin-left: 1.5rem;
4756
+ display: flex;
4757
+ flex-direction: column;
4758
+ align-items: flex-end;
4759
+ gap: 0.5rem;
4760
+ flex-shrink: 0;
4761
+ }
4762
+
4763
+ .jp-drawer-countdown-wrap {
4764
+ display: flex;
4765
+ flex-direction: column;
4766
+ align-items: flex-end;
4767
+ gap: 0.5rem;
4768
+ }
4769
+
4770
+ .jp-drawer-countdown-text {
4771
+ font-family: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
4772
+ font-size: 0.75rem;
4773
+ font-weight: 600;
4774
+ color: #34d399;
4775
+ }
4776
+
4777
+ .jp-drawer-countdown-track {
4778
+ width: 6rem;
4779
+ height: 0.125rem;
4780
+ border-radius: 9999px;
4781
+ overflow: hidden;
4782
+ background: rgb(255 255 255 / 0.1);
4783
+ }
4784
+
4785
+ .jp-drawer-countdown-bar {
4786
+ width: 100%;
4787
+ height: 100%;
4788
+ border-radius: 9999px;
4789
+ background: #34d399;
4790
+ }
4791
+
4792
+ .jp-drawer-track-row {
4793
+ margin-bottom: 1rem;
4794
+ display: flex;
4795
+ align-items: center;
4796
+ }
4797
+
4798
+ .jp-drawer-bars-wrap {
4799
+ margin-bottom: 1rem;
4800
+ display: flex;
4801
+ justify-content: center;
4802
+ }
4803
+
4804
+ .jp-drawer-separator {
4805
+ margin-bottom: 1rem;
4806
+ height: 1px;
4807
+ width: 100%;
4808
+ border: 0;
4809
+ background: rgb(255 255 255 / 0.06);
4810
+ }
4811
+
4812
+ .jp-drawer-footer {
4813
+ display: flex;
4814
+ align-items: center;
4815
+ gap: 1rem;
4816
+ }
4817
+
4818
+ .jp-drawer-progress {
4819
+ flex: 1;
4820
+ height: 2px;
4821
+ border-radius: 9999px;
4822
+ overflow: hidden;
4823
+ background: rgb(255 255 255 / 0.06);
4824
+ }
4825
+
4826
+ .jp-drawer-progress-indicator {
4827
+ height: 100%;
4828
+ }
4829
+
4830
+ .jp-drawer-cta {
4831
+ display: flex;
4832
+ align-items: center;
4833
+ gap: 0.5rem;
4834
+ flex-shrink: 0;
4835
+ }
4836
+
4837
+ .jp-drawer-running-step {
4838
+ font-family: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
4839
+ font-size: 0.75rem;
4840
+ color: rgb(255 255 255 / 0.2);
4841
+ }
4842
+
4843
+ .jp-drawer-btn-row {
4844
+ display: flex;
4845
+ gap: 0.5rem;
4846
+ }
4847
+
4848
+ .jp-drawer-btn {
4849
+ border: 1px solid transparent;
4850
+ border-radius: 0.375rem;
4851
+ font-size: 0.8125rem;
4852
+ font-weight: 500;
4853
+ line-height: 1;
4854
+ height: 2.25rem;
4855
+ padding: 0 0.75rem;
4856
+ cursor: pointer;
4857
+ transition: all 0.2s ease;
4858
+ display: inline-flex;
4859
+ align-items: center;
4860
+ justify-content: center;
4861
+ gap: 0.375rem;
4862
+ }
4863
+
4864
+ .jp-drawer-btn:disabled {
4865
+ opacity: 0.5;
4866
+ cursor: not-allowed;
4867
+ }
4868
+
4869
+ .jp-drawer-btn-secondary {
4870
+ background: hsl(var(--secondary));
4871
+ color: hsl(var(--secondary-foreground));
4872
+ }
4873
+
4874
+ .jp-drawer-btn-secondary:hover {
4875
+ filter: brightness(1.08);
4876
+ }
4877
+
4878
+ .jp-drawer-btn-emerald {
4879
+ background: #34d399;
4880
+ color: #18181b;
4881
+ font-weight: 600;
4882
+ }
4883
+
4884
+ .jp-drawer-btn-emerald:hover {
4885
+ background: #6ee7b7;
4886
+ }
4887
+
4888
+ .jp-drawer-btn-ghost {
4889
+ background: transparent;
4890
+ color: rgb(255 255 255 / 0.9);
4891
+ }
4892
+
4893
+ .jp-drawer-btn-ghost:hover {
4894
+ background: rgb(255 255 255 / 0.08);
4895
+ }
4896
+
4897
+ .jp-drawer-btn-destructive {
4898
+ background: hsl(var(--destructive));
4899
+ color: hsl(var(--destructive-foreground));
4900
+ }
4901
+
4902
+ .jp-drawer-btn-destructive:hover {
4903
+ filter: brightness(1.06);
4904
+ }
4905
+
4906
+ .jp-drawer-node-wrap {
4907
+ position: relative;
4908
+ z-index: 10;
4909
+ display: flex;
4910
+ flex-direction: column;
4911
+ align-items: center;
4912
+ gap: 0.625rem;
4913
+ }
4914
+
4915
+ .jp-drawer-node {
4916
+ position: relative;
4917
+ width: 3rem;
4918
+ height: 3rem;
4919
+ border-radius: 9999px;
4920
+ border: 1px solid transparent;
4921
+ display: flex;
4922
+ align-items: center;
4923
+ justify-content: center;
4924
+ transition: all 0.5s;
4925
+ }
4926
+
4927
+ .jp-drawer-node-pending {
4928
+ border-color: rgb(255 255 255 / 0.08);
4929
+ background: rgb(255 255 255 / 0.02);
4930
+ }
4931
+
4932
+ .jp-drawer-node-glyph {
4933
+ font-size: 1.125rem;
4934
+ line-height: 1;
4935
+ }
4936
+
4937
+ .jp-drawer-node-glyph-active {
4938
+ display: inline-block;
4939
+ }
4940
+
4941
+ .jp-drawer-node-glyph-pending {
4942
+ color: rgb(255 255 255 / 0.15);
4943
+ }
4944
+
4945
+ .jp-drawer-node-ring {
4946
+ position: absolute;
4947
+ border-radius: 9999px;
4948
+ border: 1px solid transparent;
4949
+ }
4950
+
4951
+ .jp-drawer-node-label {
4952
+ font-size: 10px;
4953
+ font-weight: 600;
4954
+ text-transform: uppercase;
4955
+ letter-spacing: 0.1em;
4956
+ transition: color 0.5s;
4957
+ }
4958
+
4959
+ .jp-drawer-connector {
4960
+ position: relative;
4961
+ z-index: 0;
4962
+ flex: 1;
4963
+ height: 2px;
4964
+ margin-top: -24px;
4965
+ }
4966
+
4967
+ .jp-drawer-connector-base {
4968
+ position: absolute;
4969
+ inset: 0;
4970
+ border-radius: 9999px;
4971
+ background: rgb(255 255 255 / 0.08);
4972
+ }
4973
+
4974
+ .jp-drawer-connector-fill {
4975
+ position: absolute;
4976
+ left: 0;
4977
+ right: auto;
4978
+ top: 0;
4979
+ bottom: 0;
4980
+ border-radius: 9999px;
4981
+ }
4982
+
4983
+ .jp-drawer-connector-orb {
4984
+ position: absolute;
4985
+ top: 50%;
4986
+ transform: translateY(-50%);
4987
+ width: 10px;
4988
+ height: 10px;
4989
+ border-radius: 9999px;
4990
+ }
4991
+
4992
+ .jp-drawer-particles {
4993
+ position: absolute;
4994
+ inset: 0;
4995
+ overflow: hidden;
4996
+ pointer-events: none;
4997
+ }
4998
+
4999
+ .jp-drawer-particle {
5000
+ position: absolute;
5001
+ border-radius: 9999px;
5002
+ }
5003
+
5004
+ .jp-drawer-bars {
5005
+ height: 1.75rem;
5006
+ display: flex;
5007
+ align-items: flex-end;
5008
+ gap: 3px;
5009
+ }
5010
+
5011
+ .jp-drawer-bars-placeholder {
5012
+ height: 1.75rem;
5013
+ }
5014
+
5015
+ .jp-drawer-bar {
5016
+ width: 3px;
5017
+ border-radius: 2px;
5018
+ background: #f59e0b;
5019
+ transform-origin: bottom;
5020
+ }
5021
+
5022
+ .jp-drawer-burst {
5023
+ position: absolute;
5024
+ inset: 0;
5025
+ display: flex;
5026
+ align-items: center;
5027
+ justify-content: center;
5028
+ pointer-events: none;
5029
+ }
5030
+
5031
+ .jp-drawer-burst-dot {
5032
+ position: absolute;
5033
+ width: 5px;
5034
+ height: 5px;
5035
+ border-radius: 9999px;
5036
+ }
5037
+
5038
+ .jp-drawer-elapsed {
5039
+ font-family: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
5040
+ font-size: 0.75rem;
5041
+ letter-spacing: 0.1em;
5042
+ color: rgb(255 255 255 / 0.25);
5043
+ }
5044
+
5045
+ /* Animation helper classes */
5046
+ .animate-drawer-up { animation: drawer-up 0.45s cubic-bezier(0.22, 1, 0.36, 1) forwards; }
5047
+ .animate-fade-in { animation: fade-in 0.25s ease forwards; }
5048
+ .animate-fade-up { animation: fade-up 0.35s ease forwards; }
5049
+ .animate-text-in { animation: text-in 0.3s ease forwards; }
5050
+ .animate-success-pop { animation: success-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }
5051
+ .countdown-bar { animation: countdown-drain 3s linear forwards; }
5052
+
5053
+ .stroke-dash-30 {
5054
+ stroke-dasharray: 30;
5055
+ stroke-dashoffset: 30;
5056
+ }
5057
+
5058
+ .animate-check-draw {
5059
+ animation: check-draw 0.4s 0.05s ease forwards;
5060
+ }
5061
+
5062
+ @keyframes check-draw {
5063
+ to { stroke-dashoffset: 0; }
5064
+ }
5065
+
5066
+ @keyframes drawer-up {
5067
+ from { transform: translateY(100%); opacity: 0; }
5068
+ to { transform: translateY(0); opacity: 1; }
5069
+ }
5070
+
5071
+ @keyframes fade-in {
5072
+ from { opacity: 0; }
5073
+ to { opacity: 1; }
5074
+ }
5075
+
5076
+ @keyframes fade-up {
5077
+ from { opacity: 0; transform: translateY(8px); }
5078
+ to { transform: translateY(0); opacity: 1; }
5079
+ }
5080
+
5081
+ @keyframes text-in {
5082
+ from { opacity: 0; transform: translateX(-6px); }
5083
+ to { opacity: 1; transform: translateX(0); }
5084
+ }
5085
+
5086
+ @keyframes success-pop {
5087
+ 0% { transform: scale(0.88); opacity: 0; }
5088
+ 60% { transform: scale(1.04); }
5089
+ 100% { transform: scale(1); opacity: 1; }
5090
+ }
5091
+
5092
+ @keyframes ambient-pulse {
5093
+ 0%, 100% { opacity: 0.3; }
5094
+ 50% { opacity: 0.65; }
5095
+ }
5096
+
5097
+ @keyframes shimmer-sweep {
5098
+ from { transform: translateX(-100%); }
5099
+ to { transform: translateX(250%); }
5100
+ }
5101
+
5102
+ @keyframes node-glow {
5103
+ 0%, 100% { box-shadow: 0 0 12px var(--glow-color,#60a5fa55); }
5104
+ 50% { box-shadow: 0 0 28px var(--glow-color,#60a5fa88), 0 0 48px var(--glow-color,#60a5fa22); }
5105
+ }
5106
+
5107
+ @keyframes glyph-rotate {
5108
+ from { transform: rotate(0deg); }
5109
+ to { transform: rotate(360deg); }
5110
+ }
5111
+
5112
+ @keyframes ring-expand {
5113
+ from { transform: scale(1); opacity: 0.7; }
5114
+ to { transform: scale(2.1); opacity: 0; }
5115
+ }
5116
+
5117
+ @keyframes orb-travel {
5118
+ from { left: 0%; }
5119
+ to { left: calc(100% - 10px); }
5120
+ }
5121
+
5122
+ @keyframes particle-float {
5123
+ 0% { transform: translateY(0) scale(1); opacity: 0; }
5124
+ 15% { opacity: 1; }
5125
+ 100% { transform: translateY(-90px) scale(0.3); opacity: 0; }
5126
+ }
5127
+
5128
+ @keyframes bar-eq {
5129
+ from { transform: scaleY(0.4); }
5130
+ to { transform: scaleY(1); }
5131
+ }
5132
+
5133
+ @keyframes burst-ray {
5134
+ 0% { transform: rotate(var(--r, 0deg)) translateX(0); opacity: 1; }
5135
+ 100% { transform: rotate(var(--r, 0deg)) translateX(56px); opacity: 0; }
5136
+ }
5137
+
5138
+ @keyframes countdown-drain {
5139
+ from { width: 100%; }
5140
+ to { width: 0%; }
5141
+ }
5142
+
5143
+
5144
+ END_OF_FILE_CONTENT
3936
5145
  mkdir -p "src/components/tiptap"
3937
5146
  echo "Creating src/components/tiptap/INTEGRATION.md..."
3938
5147
  cat << 'END_OF_FILE_CONTENT' > "src/components/tiptap/INTEGRATION.md"
@@ -7159,6 +8368,176 @@ export const CtaSchema = z.object({
7159
8368
  variant: z.enum(['primary', 'secondary']).default('primary').describe('ui:select'),
7160
8369
  });
7161
8370
 
8371
+ END_OF_FILE_CONTENT
8372
+ echo "Creating src/lib/cloudSaveStream.ts..."
8373
+ cat << 'END_OF_FILE_CONTENT' > "src/lib/cloudSaveStream.ts"
8374
+ import type { StepId } from '@/types/deploy';
8375
+
8376
+ interface SaveStreamStepEvent {
8377
+ id: StepId;
8378
+ status: 'running' | 'done';
8379
+ label?: string;
8380
+ }
8381
+
8382
+ interface SaveStreamLogEvent {
8383
+ stepId: StepId;
8384
+ message: string;
8385
+ }
8386
+
8387
+ interface SaveStreamDoneEvent {
8388
+ deployUrl?: string;
8389
+ commitSha?: string;
8390
+ }
8391
+
8392
+ interface SaveStreamErrorEvent {
8393
+ message?: string;
8394
+ }
8395
+
8396
+ interface StartCloudSaveStreamInput {
8397
+ apiBaseUrl: string;
8398
+ apiKey: string;
8399
+ path: string;
8400
+ content: unknown;
8401
+ message?: string;
8402
+ signal?: AbortSignal;
8403
+ onStep: (event: SaveStreamStepEvent) => void;
8404
+ onLog?: (event: SaveStreamLogEvent) => void;
8405
+ onDone: (event: SaveStreamDoneEvent) => void;
8406
+ }
8407
+
8408
+ function parseSseEventBlock(rawBlock: string): { event: string; data: string } | null {
8409
+ const lines = rawBlock
8410
+ .split('\n')
8411
+ .map((line) => line.trimEnd())
8412
+ .filter((line) => line.length > 0);
8413
+
8414
+ if (lines.length === 0) return null;
8415
+
8416
+ let eventName = 'message';
8417
+ const dataLines: string[] = [];
8418
+ for (const line of lines) {
8419
+ if (line.startsWith('event:')) {
8420
+ eventName = line.slice(6).trim();
8421
+ continue;
8422
+ }
8423
+ if (line.startsWith('data:')) {
8424
+ dataLines.push(line.slice(5).trim());
8425
+ }
8426
+ }
8427
+ return { event: eventName, data: dataLines.join('\n') };
8428
+ }
8429
+
8430
+ export async function startCloudSaveStream(input: StartCloudSaveStreamInput): Promise<void> {
8431
+ const response = await fetch(`${input.apiBaseUrl}/save-stream`, {
8432
+ method: 'POST',
8433
+ headers: {
8434
+ 'Content-Type': 'application/json',
8435
+ Authorization: `Bearer ${input.apiKey}`,
8436
+ },
8437
+ body: JSON.stringify({
8438
+ path: input.path,
8439
+ content: input.content,
8440
+ message: input.message,
8441
+ }),
8442
+ signal: input.signal,
8443
+ });
8444
+
8445
+ if (!response.ok || !response.body) {
8446
+ const body = (await response.json().catch(() => ({}))) as SaveStreamErrorEvent;
8447
+ throw new Error(body.message ?? `Cloud save stream failed: ${response.status}`);
8448
+ }
8449
+
8450
+ const reader = response.body.getReader();
8451
+ const decoder = new TextDecoder();
8452
+ let buffer = '';
8453
+ let receivedDone = false;
8454
+
8455
+ while (true) {
8456
+ const { value, done } = await reader.read();
8457
+ if (!done) {
8458
+ buffer += decoder.decode(value, { stream: true });
8459
+ } else {
8460
+ buffer += decoder.decode();
8461
+ }
8462
+
8463
+ const chunks = buffer.split('\n\n');
8464
+ buffer = done ? '' : (chunks.pop() ?? '');
8465
+
8466
+ for (const chunk of chunks) {
8467
+ const parsed = parseSseEventBlock(chunk);
8468
+ if (!parsed) continue;
8469
+ if (!parsed.data) continue;
8470
+
8471
+ if (parsed.event === 'step') {
8472
+ const payload = JSON.parse(parsed.data) as SaveStreamStepEvent;
8473
+ input.onStep(payload);
8474
+ } else if (parsed.event === 'log') {
8475
+ const payload = JSON.parse(parsed.data) as SaveStreamLogEvent;
8476
+ input.onLog?.(payload);
8477
+ } else if (parsed.event === 'error') {
8478
+ const payload = JSON.parse(parsed.data) as SaveStreamErrorEvent;
8479
+ throw new Error(payload.message ?? 'Cloud save failed.');
8480
+ } else if (parsed.event === 'done') {
8481
+ const payload = JSON.parse(parsed.data) as SaveStreamDoneEvent;
8482
+ input.onDone(payload);
8483
+ receivedDone = true;
8484
+ }
8485
+ }
8486
+
8487
+ if (done) break;
8488
+ }
8489
+
8490
+ if (!receivedDone) {
8491
+ throw new Error('Cloud save stream ended before completion.');
8492
+ }
8493
+ }
8494
+
8495
+
8496
+ END_OF_FILE_CONTENT
8497
+ echo "Creating src/lib/deploySteps.ts..."
8498
+ cat << 'END_OF_FILE_CONTENT' > "src/lib/deploySteps.ts"
8499
+ import type { DeployStep } from '@/types/deploy';
8500
+
8501
+ export const DEPLOY_STEPS: readonly DeployStep[] = [
8502
+ {
8503
+ id: 'commit',
8504
+ label: 'Commit',
8505
+ verb: 'Committing',
8506
+ poem: ['Crystallizing your edit', 'into permanent history.'],
8507
+ color: '#60a5fa',
8508
+ glyph: '◈',
8509
+ duration: 2200,
8510
+ },
8511
+ {
8512
+ id: 'push',
8513
+ label: 'Push',
8514
+ verb: 'Pushing',
8515
+ poem: ['Sending your vision', 'across the wire.'],
8516
+ color: '#a78bfa',
8517
+ glyph: '◎',
8518
+ duration: 2800,
8519
+ },
8520
+ {
8521
+ id: 'build',
8522
+ label: 'Build',
8523
+ verb: 'Building',
8524
+ poem: ['Assembling the pieces,', 'brick by digital brick.'],
8525
+ color: '#f59e0b',
8526
+ glyph: '⬡',
8527
+ duration: 7500,
8528
+ },
8529
+ {
8530
+ id: 'live',
8531
+ label: 'Live',
8532
+ verb: 'Going live',
8533
+ poem: ['Your content', 'is now breathing.'],
8534
+ color: '#34d399',
8535
+ glyph: '✦',
8536
+ duration: 1600,
8537
+ },
8538
+ ] as const;
8539
+
8540
+
7162
8541
  END_OF_FILE_CONTENT
7163
8542
  echo "Creating src/lib/draftStorage.ts..."
7164
8543
  cat << 'END_OF_FILE_CONTENT' > "src/lib/draftStorage.ts"
@@ -7420,7 +8799,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
7420
8799
 
7421
8800
 
7422
8801
  END_OF_FILE_CONTENT
7423
- # SKIP: src/registry-types.ts è un file binario e non può essere convertito in testo.
8802
+ # SKIP: src/registry-types.ts is binary and cannot be embedded as text.
7424
8803
  mkdir -p "src/server"
7425
8804
  mkdir -p "src/types"
7426
8805
  echo "Creating src/types.ts..."
@@ -7515,6 +8894,26 @@ declare module '@jsonpages/core' {
7515
8894
 
7516
8895
  export * from '@jsonpages/core';
7517
8896
 
8897
+ END_OF_FILE_CONTENT
8898
+ echo "Creating src/types/deploy.ts..."
8899
+ cat << 'END_OF_FILE_CONTENT' > "src/types/deploy.ts"
8900
+ export type StepId = 'commit' | 'push' | 'build' | 'live';
8901
+
8902
+ export type StepState = 'pending' | 'active' | 'done';
8903
+
8904
+ export type DeployPhase = 'idle' | 'running' | 'done' | 'error';
8905
+
8906
+ export interface DeployStep {
8907
+ id: StepId;
8908
+ label: string;
8909
+ verb: string;
8910
+ poem: [string, string];
8911
+ color: string;
8912
+ glyph: string;
8913
+ duration: number;
8914
+ }
8915
+
8916
+
7518
8917
  END_OF_FILE_CONTENT
7519
8918
  echo "Creating src/vite-env.d.ts..."
7520
8919
  cat << 'END_OF_FILE_CONTENT' > "src/vite-env.d.ts"