@jsonpages/cli 3.0.56 → 3.0.58

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.
@@ -32,20 +32,22 @@ cat << 'END_OF_FILE_CONTENT' > "src/App.tsx"
32
32
  * Data from getHydratedData (file-backed or draft); assets from public/assets/images.
33
33
  * Supports Hybrid Persistence: Local Filesystem (Dev) or Cloud Bridge (Prod).
34
34
  */
35
- import { useState, useEffect } from 'react';
35
+ import { useCallback, useEffect, useRef, useState } from 'react';
36
36
  import { JsonPagesEngine } from '@jsonpages/core';
37
- import type { LibraryImageEntry } from '@jsonpages/core';
37
+ import type { JsonPagesConfig, LibraryImageEntry, ProjectState } from '@jsonpages/core';
38
38
  import { ComponentRegistry } from '@/lib/ComponentRegistry';
39
39
  import { SECTION_SCHEMAS } from '@/lib/schemas';
40
40
  import { addSectionConfig } from '@/lib/addSectionConfig';
41
41
  import { getHydratedData } from '@/lib/draftStorage';
42
- import type { JsonPagesConfig, ProjectState } from '@jsonpages/core';
43
42
  import type { SiteConfig, ThemeConfig, MenuConfig } from '@/types';
44
-
43
+ import type { DeployPhase, StepId } from '@/types/deploy';
44
+ import { DEPLOY_STEPS } from '@/lib/deploySteps';
45
+ import { startCloudSaveStream } from '@/lib/cloudSaveStream';
45
46
  import siteData from '@/data/config/site.json';
46
47
  import themeData from '@/data/config/theme.json';
47
48
  import menuData from '@/data/config/menu.json';
48
49
  import { getFilePages } from '@/lib/getFilePages';
50
+ import { DopaDrawer } from '@/components/save-drawer/DopaDrawer';
49
51
 
50
52
  import fontsCss from './fonts.css?inline';
51
53
  import tenantCss from './index.css?inline';
@@ -62,13 +64,41 @@ const filePages = getFilePages();
62
64
  const fileSiteConfig = siteData as unknown as SiteConfig;
63
65
  const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024;
64
66
 
67
+ interface CloudSaveUiState {
68
+ isOpen: boolean;
69
+ phase: DeployPhase;
70
+ currentStepId: StepId | null;
71
+ doneSteps: StepId[];
72
+ progress: number;
73
+ errorMessage?: string;
74
+ deployUrl?: string;
75
+ }
76
+
65
77
  function getInitialData() {
66
78
  return getHydratedData(TENANT_ID, filePages, fileSiteConfig);
67
79
  }
68
80
 
81
+ function getInitialCloudSaveUiState(): CloudSaveUiState {
82
+ return {
83
+ isOpen: false,
84
+ phase: 'idle',
85
+ currentStepId: null,
86
+ doneSteps: [],
87
+ progress: 0,
88
+ };
89
+ }
90
+
91
+ function stepProgress(doneSteps: StepId[]): number {
92
+ return Math.round((doneSteps.length / DEPLOY_STEPS.length) * 100);
93
+ }
94
+
69
95
  function App() {
70
96
  const [{ pages, siteConfig }] = useState(getInitialData);
71
97
  const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
98
+ const [cloudSaveUi, setCloudSaveUi] = useState<CloudSaveUiState>(getInitialCloudSaveUiState);
99
+ const activeCloudSaveController = useRef<AbortController | null>(null);
100
+ const pendingCloudSave = useRef<{ state: ProjectState; slug: string } | null>(null);
101
+ const isCloudMode = Boolean(CLOUD_API_URL && CLOUD_API_KEY);
72
102
 
73
103
  useEffect(() => {
74
104
  // In Cloud mode, listing assets might be different or disabled for MVP
@@ -78,11 +108,111 @@ function App() {
78
108
  .then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
79
109
  .catch(() => setAssetsManifest([]));
80
110
  }, []);
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
- });
111
+
112
+ useEffect(() => {
113
+ return () => {
114
+ activeCloudSaveController.current?.abort();
115
+ };
116
+ }, []);
117
+
118
+ const runCloudSave = useCallback(
119
+ async (
120
+ payload: { state: ProjectState; slug: string },
121
+ rejectOnError: boolean
122
+ ): Promise<void> => {
123
+ if (!CLOUD_API_URL || !CLOUD_API_KEY) {
124
+ const noCloudError = new Error('Cloud mode is not configured.');
125
+ if (rejectOnError) throw noCloudError;
126
+ return;
127
+ }
128
+
129
+ pendingCloudSave.current = payload;
130
+ activeCloudSaveController.current?.abort();
131
+ const controller = new AbortController();
132
+ activeCloudSaveController.current = controller;
133
+
134
+ setCloudSaveUi({
135
+ isOpen: true,
136
+ phase: 'running',
137
+ currentStepId: null,
138
+ doneSteps: [],
139
+ progress: 0,
140
+ });
141
+
142
+ try {
143
+ await startCloudSaveStream({
144
+ apiBaseUrl: CLOUD_API_URL,
145
+ apiKey: CLOUD_API_KEY,
146
+ path: `src/data/pages/${payload.slug}.json`,
147
+ content: payload.state.page,
148
+ message: `Content update for ${payload.slug} via Visual Editor`,
149
+ signal: controller.signal,
150
+ onStep: (event) => {
151
+ setCloudSaveUi((prev) => {
152
+ if (event.status === 'running') {
153
+ return {
154
+ ...prev,
155
+ isOpen: true,
156
+ phase: 'running',
157
+ currentStepId: event.id,
158
+ errorMessage: undefined,
159
+ };
160
+ }
161
+
162
+ if (prev.doneSteps.includes(event.id)) {
163
+ return prev;
164
+ }
165
+
166
+ const nextDone = [...prev.doneSteps, event.id];
167
+ return {
168
+ ...prev,
169
+ isOpen: true,
170
+ phase: 'running',
171
+ currentStepId: event.id,
172
+ doneSteps: nextDone,
173
+ progress: stepProgress(nextDone),
174
+ };
175
+ });
176
+ },
177
+ onDone: (event) => {
178
+ const completed = DEPLOY_STEPS.map((step) => step.id);
179
+ setCloudSaveUi({
180
+ isOpen: true,
181
+ phase: 'done',
182
+ currentStepId: 'live',
183
+ doneSteps: completed,
184
+ progress: 100,
185
+ deployUrl: event.deployUrl,
186
+ });
187
+ },
188
+ });
189
+ } catch (error: unknown) {
190
+ const message = error instanceof Error ? error.message : 'Cloud save failed.';
191
+ setCloudSaveUi((prev) => ({
192
+ ...prev,
193
+ isOpen: true,
194
+ phase: 'error',
195
+ errorMessage: message,
196
+ }));
197
+ if (rejectOnError) throw new Error(message);
198
+ } finally {
199
+ if (activeCloudSaveController.current === controller) {
200
+ activeCloudSaveController.current = null;
201
+ }
202
+ }
203
+ },
204
+ []
205
+ );
206
+
207
+ const closeCloudDrawer = useCallback(() => {
208
+ setCloudSaveUi(getInitialCloudSaveUiState());
209
+ }, []);
210
+
211
+ const retryCloudSave = useCallback(() => {
212
+ if (!pendingCloudSave.current) return;
213
+ void runCloudSave(pendingCloudSave.current, false);
214
+ }, [runCloudSave]);
215
+
86
216
  const config: JsonPagesConfig = {
87
217
  tenantId: TENANT_ID,
88
218
  registry: ComponentRegistry as JsonPagesConfig['registry'],
@@ -95,30 +225,9 @@ console.log("🔍 DEBUG ENV:", {
95
225
  addSection: addSectionConfig,
96
226
  persistence: {
97
227
  async saveToFile(state: ProjectState, slug: string): Promise<void> {
98
-
99
228
  // ☁️ SCENARIO A: CLOUD BRIDGE (Production)
100
- if (CLOUD_API_URL && CLOUD_API_KEY) {
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
- }
229
+ if (isCloudMode) {
230
+ await runCloudSave({ state, slug }, true);
122
231
  return;
123
232
  }
124
233
 
@@ -164,15 +273,35 @@ console.log("🔍 DEBUG ENV:", {
164
273
  },
165
274
  };
166
275
 
167
- return <JsonPagesEngine config={config} />;
276
+ return (
277
+ <>
278
+ <JsonPagesEngine config={config} />
279
+ <DopaDrawer
280
+ isOpen={cloudSaveUi.isOpen}
281
+ phase={cloudSaveUi.phase}
282
+ currentStepId={cloudSaveUi.currentStepId}
283
+ doneSteps={cloudSaveUi.doneSteps}
284
+ progress={cloudSaveUi.progress}
285
+ errorMessage={cloudSaveUi.errorMessage}
286
+ deployUrl={cloudSaveUi.deployUrl}
287
+ onClose={closeCloudDrawer}
288
+ onRetry={retryCloudSave}
289
+ />
290
+ </>
291
+ );
168
292
  }
169
293
 
170
294
  export default App;
171
295
 
172
296
  END_OF_FILE_CONTENT
173
297
  # SKIP: src/App.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
174
- echo "Creating src/App_.tsx..."
175
- cat << 'END_OF_FILE_CONTENT' > "src/App_.tsx"
298
+ echo "Creating src/_App.tsx..."
299
+ cat << 'END_OF_FILE_CONTENT' > "src/_App.tsx"
300
+ /**
301
+ * Thin Entry Point (Tenant).
302
+ * Data from getHydratedData (file-backed or draft); assets from public/assets/images.
303
+ * Supports Hybrid Persistence: Local Filesystem (Dev) or Cloud Bridge (Prod).
304
+ */
176
305
  import { useState, useEffect } from 'react';
177
306
  import { JsonPagesEngine } from '@jsonpages/core';
178
307
  import type { LibraryImageEntry } from '@jsonpages/core';
@@ -191,6 +320,10 @@ import { getFilePages } from '@/lib/getFilePages';
191
320
  import fontsCss from './fonts.css?inline';
192
321
  import tenantCss from './index.css?inline';
193
322
 
323
+ // Cloud Configuration (Injected by Vercel/Netlify Env Vars)
324
+ const CLOUD_API_URL = import.meta.env.VITE_JSONPAGES_CLOUD_URL;
325
+ const CLOUD_API_KEY = import.meta.env.VITE_JSONPAGES_API_KEY;
326
+
194
327
  const themeConfig = themeData as unknown as ThemeConfig;
195
328
  const menuConfig = menuData as unknown as MenuConfig;
196
329
  const TENANT_ID = 'alpha';
@@ -208,16 +341,22 @@ function App() {
208
341
  const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
209
342
 
210
343
  useEffect(() => {
344
+ // In Cloud mode, listing assets might be different or disabled for MVP
345
+ // For now, we keep the local fetch which will fail gracefully on Vercel (404)
211
346
  fetch('/api/list-assets')
212
347
  .then((r) => (r.ok ? r.json() : []))
213
348
  .then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
214
349
  .catch(() => setAssetsManifest([]));
215
350
  }, []);
216
-
351
+
352
+ console.log("🔍 DEBUG ENV:", {
353
+ URL: import.meta.env.VITE_JSONPAGES_CLOUD_URL,
354
+ KEY: import.meta.env.VITE_JSONPAGES_API_KEY ? "PRESENT" : "MISSING"
355
+ });
217
356
  const config: JsonPagesConfig = {
218
357
  tenantId: TENANT_ID,
219
358
  registry: ComponentRegistry as JsonPagesConfig['registry'],
220
- schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
359
+ schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
221
360
  pages,
222
361
  siteConfig,
223
362
  themeConfig,
@@ -226,11 +365,41 @@ function App() {
226
365
  addSection: addSectionConfig,
227
366
  persistence: {
228
367
  async saveToFile(state: ProjectState, slug: string): Promise<void> {
368
+
369
+ // ☁️ SCENARIO A: CLOUD BRIDGE (Production)
370
+ if (CLOUD_API_URL && CLOUD_API_KEY) {
371
+ console.log(`☁️ Saving ${slug} via Cloud Bridge...`);
372
+
373
+ const res = await fetch(`${CLOUD_API_URL}/save`, {
374
+ method: 'POST',
375
+ headers: {
376
+ 'Content-Type': 'application/json',
377
+ 'Authorization': `Bearer ${CLOUD_API_KEY}`
378
+ },
379
+ body: JSON.stringify({
380
+ // Mapping logical slug to physical path in repo
381
+ path: `src/data/pages/${slug}.json`,
382
+ // We save the page config specifically
383
+ content: state.page,
384
+ message: `Content update for ${slug} via Visual Editor`
385
+ }),
386
+ });
387
+
388
+ if (!res.ok) {
389
+ const err = await res.json().catch(() => ({}));
390
+ throw new Error(err.error || `Cloud save failed: ${res.status}`);
391
+ }
392
+ return;
393
+ }
394
+
395
+ // 💻 SCENARIO B: LOCAL FILESYSTEM (Development)
396
+ console.log(`💻 Saving ${slug} to Local Filesystem...`);
229
397
  const res = await fetch('/api/save-to-file', {
230
398
  method: 'POST',
231
399
  headers: { 'Content-Type': 'application/json' },
232
400
  body: JSON.stringify({ projectState: state, slug }),
233
401
  });
402
+
234
403
  const body = (await res.json().catch(() => ({}))) as { error?: string };
235
404
  if (!res.ok) throw new Error(body.error ?? `Save to file failed: ${res.status}`);
236
405
  },
@@ -239,19 +408,24 @@ function App() {
239
408
  assetsBaseUrl: '/assets',
240
409
  assetsManifest,
241
410
  async onAssetUpload(file: File): Promise<string> {
411
+ // Note: Asset upload in Cloud Mode requires the R2 Bridge (Next Step in Roadmap)
412
+ // For now, this works in Local Mode.
242
413
  if (!file.type.startsWith('image/')) throw new Error('Invalid file type.');
243
414
  if (file.size > MAX_UPLOAD_SIZE_BYTES) throw new Error(`File too large. Max ${MAX_UPLOAD_SIZE_BYTES / 1024 / 1024}MB.`);
415
+
244
416
  const base64 = await new Promise<string>((resolve, reject) => {
245
417
  const reader = new FileReader();
246
418
  reader.onload = () => resolve((reader.result as string).split(',')[1] ?? '');
247
419
  reader.onerror = () => reject(reader.error);
248
420
  reader.readAsDataURL(file);
249
421
  });
422
+
250
423
  const res = await fetch('/api/upload-asset', {
251
424
  method: 'POST',
252
425
  headers: { 'Content-Type': 'application/json' },
253
426
  body: JSON.stringify({ filename: file.name, mimeType: file.type || undefined, data: base64 }),
254
427
  });
428
+
255
429
  const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string };
256
430
  if (!res.ok) throw new Error(body.error || `Upload failed: ${res.status}`);
257
431
  if (typeof body.url !== 'string') throw new Error('Invalid server response: missing url');
@@ -265,6 +439,24 @@ function App() {
265
439
 
266
440
  export default App;
267
441
 
442
+ END_OF_FILE_CONTENT
443
+ echo "Creating src/_main.tsx..."
444
+ cat << 'END_OF_FILE_CONTENT' > "src/_main.tsx"
445
+ import '@/types'; // TBP: load type augmentation from capsule-driven types
446
+ import React from 'react';
447
+ import ReactDOM from 'react-dom/client';
448
+ import App from './App';
449
+ // ... resto del file
450
+
451
+ ReactDOM.createRoot(document.getElementById('root')!).render(
452
+ <React.StrictMode>
453
+ <App />
454
+ </React.StrictMode>
455
+ );
456
+
457
+
458
+
459
+
268
460
  END_OF_FILE_CONTENT
269
461
  mkdir -p "src/components"
270
462
  echo "Creating src/components/NotFound.tsx..."
@@ -3041,111 +3233,1200 @@ export type ProductTriadData = z.infer<typeof ProductTriadSchema>;
3041
3233
  export type ProductTriadSettings = z.infer<typeof BaseSectionSettingsSchema>;
3042
3234
 
3043
3235
  END_OF_FILE_CONTENT
3044
- mkdir -p "src/components/ui"
3045
- echo "Creating src/components/ui/checkbox.tsx..."
3046
- cat << 'END_OF_FILE_CONTENT' > "src/components/ui/checkbox.tsx"
3047
- "use client"
3048
-
3049
- import * as React from "react"
3050
- import { Checkbox as CheckboxPrimitive } from "radix-ui"
3236
+ mkdir -p "src/components/save-drawer"
3237
+ echo "Creating src/components/save-drawer/DeployConnector.tsx..."
3238
+ cat << 'END_OF_FILE_CONTENT' > "src/components/save-drawer/DeployConnector.tsx"
3239
+ import type { StepState } from '@/types/deploy';
3240
+
3241
+ interface DeployConnectorProps {
3242
+ fromState: StepState;
3243
+ toState: StepState;
3244
+ color: string;
3245
+ }
3051
3246
 
3052
- import { cn } from "@/lib/utils"
3053
- import { CheckIcon } from "lucide-react"
3247
+ export function DeployConnector({ fromState, toState, color }: DeployConnectorProps) {
3248
+ const filled = fromState === 'done' && toState === 'done';
3249
+ const filling = fromState === 'done' && toState === 'active';
3250
+ const lit = filled || filling;
3054
3251
 
3055
- function Checkbox({
3056
- className,
3057
- ...props
3058
- }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
3059
3252
  return (
3060
- <CheckboxPrimitive.Root
3061
- data-slot="checkbox"
3062
- className={cn(
3063
- "border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-[4px] border transition-colors group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50",
3064
- className
3065
- )}
3066
- {...props}
3067
- >
3068
- <CheckboxPrimitive.Indicator
3069
- data-slot="checkbox-indicator"
3070
- className="[&>svg]:size-3.5 grid place-content-center text-current transition-none"
3071
- >
3072
- <CheckIcon
3253
+ <div className="jp-drawer-connector">
3254
+ <div className="jp-drawer-connector-base" />
3255
+
3256
+ <div
3257
+ className="jp-drawer-connector-fill"
3258
+ style={{
3259
+ background: `linear-gradient(90deg, ${color}cc, ${color}66)`,
3260
+ width: filled ? '100%' : filling ? '100%' : '0%',
3261
+ transition: filling ? 'width 2s cubic-bezier(0.4,0,0.2,1)' : 'none',
3262
+ boxShadow: lit ? `0 0 8px ${color}77` : 'none',
3263
+ }}
3264
+ />
3265
+
3266
+ {filling && (
3267
+ <div
3268
+ className="jp-drawer-connector-orb"
3269
+ style={{
3270
+ background: color,
3271
+ boxShadow: `0 0 14px ${color}, 0 0 28px ${color}88`,
3272
+ animation: 'orb-travel 2s cubic-bezier(0.4,0,0.6,1) forwards',
3273
+ }}
3073
3274
  />
3074
- </CheckboxPrimitive.Indicator>
3075
- </CheckboxPrimitive.Root>
3076
- )
3275
+ )}
3276
+ </div>
3277
+ );
3077
3278
  }
3078
3279
 
3079
- export { Checkbox }
3280
+ END_OF_FILE_CONTENT
3281
+ # SKIP: src/components/save-drawer/DeployConnector.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
3282
+ # SKIP: src/components/save-drawer/DeployConnector.tsx:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
3283
+ echo "Creating src/components/save-drawer/DeployNode.tsx..."
3284
+ cat << 'END_OF_FILE_CONTENT' > "src/components/save-drawer/DeployNode.tsx"
3285
+ import type { CSSProperties } from 'react';
3286
+ import type { DeployStep, StepState } from '@/types/deploy';
3287
+
3288
+ interface DeployNodeProps {
3289
+ step: DeployStep;
3290
+ state: StepState;
3291
+ }
3080
3292
 
3293
+ export function DeployNode({ step, state }: DeployNodeProps) {
3294
+ const isActive = state === 'active';
3295
+ const isDone = state === 'done';
3296
+ const isPending = state === 'pending';
3081
3297
 
3298
+ return (
3299
+ <div className="jp-drawer-node-wrap">
3300
+ <div
3301
+ className={`jp-drawer-node ${isPending ? 'jp-drawer-node-pending' : ''}`}
3302
+ style={
3303
+ {
3304
+ background: isDone ? step.color : isActive ? 'rgba(0,0,0,0.5)' : undefined,
3305
+ borderWidth: isDone ? 0 : 1,
3306
+ borderColor: isActive ? `${step.color}80` : undefined,
3307
+ boxShadow: isDone
3308
+ ? `0 0 20px ${step.color}55, 0 0 40px ${step.color}22`
3309
+ : isActive
3310
+ ? `0 0 14px ${step.color}33`
3311
+ : undefined,
3312
+ animation: isActive ? 'node-glow 2s ease infinite' : undefined,
3313
+ ['--glow-color' as string]: step.color,
3314
+ } as CSSProperties
3315
+ }
3316
+ >
3317
+ {isDone && (
3318
+ <svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" aria-label="Done">
3319
+ <path
3320
+ className="stroke-dash-30 animate-check-draw"
3321
+ d="M5 13l4 4L19 7"
3322
+ stroke="#0a0f1a"
3323
+ strokeWidth="2.5"
3324
+ strokeLinecap="round"
3325
+ strokeLinejoin="round"
3326
+ />
3327
+ </svg>
3328
+ )}
3082
3329
 
3330
+ {isActive && (
3331
+ <span
3332
+ className="jp-drawer-node-glyph jp-drawer-node-glyph-active"
3333
+ style={{ color: step.color, animation: 'glyph-rotate 9s linear infinite' }}
3334
+ aria-hidden
3335
+ >
3336
+ {step.glyph}
3337
+ </span>
3338
+ )}
3083
3339
 
3340
+ {isPending && (
3341
+ <span className="jp-drawer-node-glyph jp-drawer-node-glyph-pending" aria-hidden>
3342
+ {step.glyph}
3343
+ </span>
3344
+ )}
3084
3345
 
3085
- END_OF_FILE_CONTENT
3086
- echo "Creating src/components/ui/input.tsx..."
3087
- cat << 'END_OF_FILE_CONTENT' > "src/components/ui/input.tsx"
3088
- import * as React from "react"
3346
+ {isActive && (
3347
+ <span
3348
+ className="jp-drawer-node-ring"
3349
+ style={{
3350
+ inset: -7,
3351
+ borderColor: `${step.color}50`,
3352
+ animation: 'ring-expand 2s ease-out infinite',
3353
+ }}
3354
+ />
3355
+ )}
3356
+ </div>
3089
3357
 
3090
- import { cn } from "@/lib/utils"
3358
+ <span
3359
+ className="jp-drawer-node-label"
3360
+ style={{ color: isDone ? step.color : isActive ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.18)' }}
3361
+ >
3362
+ {step.label}
3363
+ </span>
3364
+ </div>
3365
+ );
3366
+ }
3091
3367
 
3092
- function Input({ className, type, ...props }: React.ComponentProps<"input">) {
3093
- return (
3094
- <input
3095
- type={type}
3096
- data-slot="input"
3097
- className={cn(
3098
- "dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
3099
- className
3100
- )}
3101
- {...props}
3102
- />
3103
- )
3368
+ END_OF_FILE_CONTENT
3369
+ # SKIP: src/components/save-drawer/DeployNode.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
3370
+ # SKIP: src/components/save-drawer/DeployNode.tsx:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
3371
+ echo "Creating src/components/save-drawer/DopaDrawer.tsx..."
3372
+ cat << 'END_OF_FILE_CONTENT' > "src/components/save-drawer/DopaDrawer.tsx"
3373
+ import { useEffect, useMemo, useState } from 'react';
3374
+ import { createPortal } from 'react-dom';
3375
+ import type { StepId, StepState } from '@/types/deploy';
3376
+ import { DEPLOY_STEPS } from '@/lib/deploySteps';
3377
+ import fontsCss from '@/fonts.css?inline';
3378
+ import saverStyleCss from './saverStyle.css?inline';
3379
+ import { DeployNode } from './DeployNode';
3380
+ import { DeployConnector } from './DeployConnector';
3381
+ import { BuildBars, ElapsedTimer, Particles, SuccessBurst } from './Visuals';
3382
+
3383
+ interface DopaDrawerProps {
3384
+ isOpen: boolean;
3385
+ phase: 'idle' | 'running' | 'done' | 'error';
3386
+ currentStepId: StepId | null;
3387
+ doneSteps: StepId[];
3388
+ progress: number;
3389
+ errorMessage?: string;
3390
+ deployUrl?: string;
3391
+ onClose: () => void;
3392
+ onRetry: () => void;
3104
3393
  }
3105
3394
 
3106
- export { Input }
3395
+ export function DopaDrawer({
3396
+ isOpen,
3397
+ phase,
3398
+ currentStepId,
3399
+ doneSteps,
3400
+ progress,
3401
+ errorMessage,
3402
+ deployUrl,
3403
+ onClose,
3404
+ onRetry,
3405
+ }: DopaDrawerProps) {
3406
+ const [shadowMount, setShadowMount] = useState<HTMLElement | null>(null);
3407
+ const [burst, setBurst] = useState(false);
3408
+ const [countdown, setCountdown] = useState(3);
3409
+
3410
+ const isRunning = phase === 'running';
3411
+ const isDone = phase === 'done';
3412
+ const isError = phase === 'error';
3107
3413
 
3414
+ useEffect(() => {
3415
+ const host = document.createElement('div');
3416
+ host.setAttribute('data-jp-drawer-shadow-host', '');
3108
3417
 
3418
+ const shadowRoot = host.attachShadow({ mode: 'open' });
3419
+ const style = document.createElement('style');
3420
+ style.textContent = `${fontsCss}\n${saverStyleCss}`;
3109
3421
 
3422
+ const mount = document.createElement('div');
3423
+ shadowRoot.append(style, mount);
3110
3424
 
3425
+ document.body.appendChild(host);
3426
+ setShadowMount(mount);
3111
3427
 
3112
- END_OF_FILE_CONTENT
3113
- echo "Creating src/components/ui/label.tsx..."
3114
- cat << 'END_OF_FILE_CONTENT' > "src/components/ui/label.tsx"
3115
- import * as React from "react"
3116
- import { Label as LabelPrimitive } from "radix-ui"
3428
+ return () => {
3429
+ setShadowMount(null);
3430
+ host.remove();
3431
+ };
3432
+ }, []);
3117
3433
 
3118
- import { cn } from "@/lib/utils"
3434
+ useEffect(() => {
3435
+ if (!isOpen) {
3436
+ setBurst(false);
3437
+ setCountdown(3);
3438
+ return;
3439
+ }
3440
+ if (isDone) setBurst(true);
3441
+ }, [isDone, isOpen]);
3119
3442
 
3120
- function Label({
3121
- className,
3122
- ...props
3123
- }: React.ComponentProps<typeof LabelPrimitive.Root>) {
3124
- return (
3125
- <LabelPrimitive.Root
3126
- data-slot="label"
3127
- className={cn(
3128
- "gap-2 text-sm leading-none font-medium group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed",
3129
- className
3130
- )}
3131
- {...props}
3132
- />
3133
- )
3134
- }
3443
+ useEffect(() => {
3444
+ if (!isOpen || !isDone) return;
3445
+ setCountdown(3);
3446
+ const interval = window.setInterval(() => {
3447
+ setCountdown((prev) => {
3448
+ if (prev <= 1) {
3449
+ window.clearInterval(interval);
3450
+ onClose();
3451
+ return 0;
3452
+ }
3453
+ return prev - 1;
3454
+ });
3455
+ }, 1000);
3456
+ return () => window.clearInterval(interval);
3457
+ }, [isDone, isOpen, onClose]);
3458
+
3459
+ const currentStep = useMemo(
3460
+ () => DEPLOY_STEPS.find((step) => step.id === currentStepId) ?? null,
3461
+ [currentStepId]
3462
+ );
3135
3463
 
3136
- export { Label }
3464
+ const activeColor = isDone ? '#34d399' : isError ? '#f87171' : (currentStep?.color ?? '#60a5fa');
3465
+ const particleCount = isDone ? 40 : doneSteps.length === 3 ? 28 : doneSteps.length === 2 ? 16 : doneSteps.length === 1 ? 8 : 4;
3137
3466
 
3467
+ const stepState = (index: number): StepState => {
3468
+ const step = DEPLOY_STEPS[index];
3469
+ if (doneSteps.includes(step.id)) return 'done';
3470
+ if (phase === 'running' && currentStepId === step.id) return 'active';
3471
+ return 'pending';
3472
+ };
3138
3473
 
3474
+ if (!shadowMount || !isOpen || phase === 'idle') return null;
3475
+
3476
+ return createPortal(
3477
+ <div className="jp-drawer-root">
3478
+ <div
3479
+ className="jp-drawer-overlay animate-fade-in"
3480
+ onClick={isDone || isError ? onClose : undefined}
3481
+ aria-hidden
3482
+ />
3483
+
3484
+ <div
3485
+ role="status"
3486
+ aria-live="polite"
3487
+ aria-label={isDone ? 'Deploy completed' : isError ? 'Deploy failed' : 'Deploying'}
3488
+ className="jp-drawer-shell animate-drawer-up"
3489
+ style={{ bottom: 'max(2.25rem, env(safe-area-inset-bottom))' }}
3490
+ >
3491
+ <div
3492
+ className="jp-drawer-card"
3493
+ style={{
3494
+ backgroundColor: 'hsl(222 18% 7%)',
3495
+ 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`,
3496
+ transition: 'box-shadow 1.2s ease',
3497
+ }}
3498
+ >
3499
+ <div
3500
+ className="jp-drawer-ambient"
3501
+ style={{
3502
+ background: `radial-gradient(ellipse 70% 60% at 50% 110%, ${activeColor}12 0%, transparent 65%)`,
3503
+ transition: 'background 1.5s ease',
3504
+ animation: 'ambient-pulse 3.5s ease infinite',
3505
+ }}
3506
+ aria-hidden
3507
+ />
3508
+
3509
+ {isDone && (
3510
+ <div className="jp-drawer-shimmer" aria-hidden>
3511
+ <div
3512
+ className="jp-drawer-shimmer-bar"
3513
+ style={{
3514
+ background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.04), transparent)',
3515
+ animation: 'shimmer-sweep 1.4s 0.1s ease forwards',
3516
+ }}
3517
+ />
3518
+ </div>
3519
+ )}
3139
3520
 
3521
+ <Particles count={particleCount} color={activeColor} />
3522
+ {burst && <SuccessBurst />}
3140
3523
 
3524
+ <div className="jp-drawer-content">
3525
+ <div className="jp-drawer-header">
3526
+ <div className="jp-drawer-header-left">
3527
+ <div className="jp-drawer-status" style={{ color: activeColor }}>
3528
+ <span
3529
+ className="jp-drawer-status-dot"
3530
+ style={{
3531
+ background: activeColor,
3532
+ boxShadow: `0 0 6px ${activeColor}`,
3533
+ animation: isRunning ? 'ambient-pulse 1.5s ease infinite' : 'none',
3534
+ }}
3535
+ aria-hidden
3536
+ />
3537
+ {isDone ? 'Live' : isError ? 'Build failed' : currentStep?.verb ?? 'Saving'}
3538
+ </div>
3141
3539
 
3142
- END_OF_FILE_CONTENT
3143
- echo "Creating src/components/ui/select.tsx..."
3144
- cat << 'END_OF_FILE_CONTENT' > "src/components/ui/select.tsx"
3145
- import * as React from "react"
3146
- import { Select as SelectPrimitive } from "radix-ui"
3540
+ <div key={currentStep?.id ?? phase} className="jp-drawer-copy animate-text-in">
3541
+ {isDone ? (
3542
+ <div className="animate-success-pop">
3543
+ <p className="jp-drawer-copy-title jp-drawer-copy-title-lg">Your content is live.</p>
3544
+ <p className="jp-drawer-copy-sub">Deployed to production successfully</p>
3545
+ </div>
3546
+ ) : isError ? (
3547
+ <>
3548
+ <p className="jp-drawer-copy-title jp-drawer-copy-title-md">Deploy failed at build.</p>
3549
+ <p className="jp-drawer-copy-sub jp-drawer-copy-sub-error">{errorMessage ?? 'Check your Vercel logs or retry below'}</p>
3550
+ </>
3551
+ ) : currentStep ? (
3552
+ <>
3553
+ <p className="jp-drawer-poem-line jp-drawer-poem-line-1">{currentStep.poem[0]}</p>
3554
+ <p className="jp-drawer-poem-line jp-drawer-poem-line-2">{currentStep.poem[1]}</p>
3555
+ </>
3556
+ ) : null}
3557
+ </div>
3558
+ </div>
3147
3559
 
3148
- import { cn } from "@/lib/utils"
3560
+ <div className="jp-drawer-right">
3561
+ {isDone ? (
3562
+ <div className="jp-drawer-countdown-wrap animate-fade-up">
3563
+ <span className="jp-drawer-countdown-text" aria-live="polite">
3564
+ Chiusura in {countdown}s
3565
+ </span>
3566
+ <div className="jp-drawer-countdown-track">
3567
+ <div className="jp-drawer-countdown-bar countdown-bar" style={{ boxShadow: '0 0 6px #34d39988' }} />
3568
+ </div>
3569
+ </div>
3570
+ ) : (
3571
+ <ElapsedTimer running={isRunning} />
3572
+ )}
3573
+ </div>
3574
+ </div>
3575
+
3576
+ <div className="jp-drawer-track-row">
3577
+ {DEPLOY_STEPS.map((step, i) => (
3578
+ <div key={step.id} style={{ display: 'flex', alignItems: 'center', flex: i < DEPLOY_STEPS.length - 1 ? 1 : 'none' }}>
3579
+ <DeployNode step={step} state={stepState(i)} />
3580
+ {i < DEPLOY_STEPS.length - 1 && (
3581
+ <DeployConnector fromState={stepState(i)} toState={stepState(i + 1)} color={DEPLOY_STEPS[i + 1].color} />
3582
+ )}
3583
+ </div>
3584
+ ))}
3585
+ </div>
3586
+
3587
+ <div className="jp-drawer-bars-wrap">
3588
+ <BuildBars active={stepState(2) === 'active'} />
3589
+ </div>
3590
+
3591
+ <div className="jp-drawer-separator" />
3592
+
3593
+ <div className="jp-drawer-footer">
3594
+ <div className="jp-drawer-progress">
3595
+ <div
3596
+ className="jp-drawer-progress-indicator"
3597
+ style={{
3598
+ width: `${Math.max(0, Math.min(100, progress))}%`,
3599
+ background: `linear-gradient(90deg, ${DEPLOY_STEPS[0].color}, ${activeColor})`,
3600
+ }}
3601
+ />
3602
+ </div>
3603
+
3604
+ <div className="jp-drawer-cta">
3605
+ {isDone && (
3606
+ <div className="jp-drawer-btn-row animate-fade-up">
3607
+ <button type="button" className="jp-drawer-btn jp-drawer-btn-secondary" onClick={onClose}>
3608
+ Chiudi
3609
+ </button>
3610
+ <button
3611
+ type="button"
3612
+ className="jp-drawer-btn jp-drawer-btn-emerald"
3613
+ onClick={() => {
3614
+ if (deployUrl) window.open(deployUrl, '_blank', 'noopener,noreferrer');
3615
+ }}
3616
+ disabled={!deployUrl}
3617
+ >
3618
+ <span aria-hidden>↗</span> Open site
3619
+ </button>
3620
+ </div>
3621
+ )}
3622
+
3623
+ {isError && (
3624
+ <div className="jp-drawer-btn-row animate-fade-up">
3625
+ <button type="button" className="jp-drawer-btn jp-drawer-btn-ghost" onClick={onClose}>
3626
+ Annulla
3627
+ </button>
3628
+ <button type="button" className="jp-drawer-btn jp-drawer-btn-destructive" onClick={onRetry}>
3629
+ Retry
3630
+ </button>
3631
+ </div>
3632
+ )}
3633
+
3634
+ {isRunning && (
3635
+ <span className="jp-drawer-running-step" aria-hidden>
3636
+ {doneSteps.length + 1} / {DEPLOY_STEPS.length}
3637
+ </span>
3638
+ )}
3639
+ </div>
3640
+ </div>
3641
+ </div>
3642
+ </div>
3643
+ </div>
3644
+ </div>,
3645
+ shadowMount
3646
+ );
3647
+ }
3648
+
3649
+ END_OF_FILE_CONTENT
3650
+ # SKIP: src/components/save-drawer/DopaDrawer.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
3651
+ # SKIP: src/components/save-drawer/DopaDrawer.tsx:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
3652
+ echo "Creating src/components/save-drawer/Visuals.tsx..."
3653
+ cat << 'END_OF_FILE_CONTENT' > "src/components/save-drawer/Visuals.tsx"
3654
+ import { useEffect, useRef, useState } from 'react';
3655
+ import type { CSSProperties } from 'react';
3656
+
3657
+ interface Particle {
3658
+ id: number;
3659
+ x: number;
3660
+ y: number;
3661
+ size: number;
3662
+ dur: number;
3663
+ delay: number;
3664
+ }
3665
+
3666
+ const PARTICLE_POOL: Particle[] = Array.from({ length: 44 }, (_, i) => ({
3667
+ id: i,
3668
+ x: 5 + Math.random() * 90,
3669
+ y: 15 + Math.random() * 70,
3670
+ size: 1.5 + Math.random() * 2.5,
3671
+ dur: 2.8 + Math.random() * 3.5,
3672
+ delay: Math.random() * 4,
3673
+ }));
3674
+
3675
+ interface ParticlesProps {
3676
+ count: number;
3677
+ color: string;
3678
+ }
3679
+
3680
+ export function Particles({ count, color }: ParticlesProps) {
3681
+ return (
3682
+ <div className="jp-drawer-particles" aria-hidden>
3683
+ {PARTICLE_POOL.slice(0, count).map((particle) => (
3684
+ <div
3685
+ key={particle.id}
3686
+ className="jp-drawer-particle"
3687
+ style={{
3688
+ left: `${particle.x}%`,
3689
+ bottom: `${particle.y}%`,
3690
+ width: particle.size,
3691
+ height: particle.size,
3692
+ background: color,
3693
+ boxShadow: `0 0 ${particle.size * 3}px ${color}`,
3694
+ opacity: 0,
3695
+ animation: `particle-float ${particle.dur}s ${particle.delay}s ease-out infinite`,
3696
+ }}
3697
+ />
3698
+ ))}
3699
+ </div>
3700
+ );
3701
+ }
3702
+
3703
+ 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];
3704
+
3705
+ interface BuildBarsProps {
3706
+ active: boolean;
3707
+ }
3708
+
3709
+ export function BuildBars({ active }: BuildBarsProps) {
3710
+ if (!active) return <div className="jp-drawer-bars-placeholder" />;
3711
+
3712
+ return (
3713
+ <div className="jp-drawer-bars" aria-hidden>
3714
+ {BAR_H.map((height, i) => (
3715
+ <div
3716
+ key={i}
3717
+ className="jp-drawer-bar"
3718
+ style={{
3719
+ height: `${height * 100}%`,
3720
+ animation: `bar-eq ${0.42 + i * 0.06}s ${i * 0.04}s ease-in-out infinite alternate`,
3721
+ }}
3722
+ />
3723
+ ))}
3724
+ </div>
3725
+ );
3726
+ }
3727
+
3728
+ const BURST_COLORS = ['#34d399', '#60a5fa', '#a78bfa', '#f59e0b', '#f472b6'];
3729
+
3730
+ export function SuccessBurst() {
3731
+ return (
3732
+ <div className="jp-drawer-burst" aria-hidden>
3733
+ {Array.from({ length: 16 }).map((_, i) => (
3734
+ <div
3735
+ key={i}
3736
+ className="jp-drawer-burst-dot"
3737
+ style={
3738
+ {
3739
+ background: BURST_COLORS[i % BURST_COLORS.length],
3740
+ ['--r' as string]: `${i * 22.5}deg`,
3741
+ animation: `burst-ray 0.85s ${i * 0.03}s cubic-bezier(0,0.6,0.5,1) forwards`,
3742
+ transform: `rotate(${i * 22.5}deg)`,
3743
+ transformOrigin: '50% 50%',
3744
+ opacity: 0,
3745
+ } as CSSProperties
3746
+ }
3747
+ />
3748
+ ))}
3749
+ </div>
3750
+ );
3751
+ }
3752
+
3753
+ interface ElapsedTimerProps {
3754
+ running: boolean;
3755
+ }
3756
+
3757
+ export function ElapsedTimer({ running }: ElapsedTimerProps) {
3758
+ const [elapsed, setElapsed] = useState(0);
3759
+ const startRef = useRef<number | null>(null);
3760
+ const rafRef = useRef<number | null>(null);
3761
+
3762
+ useEffect(() => {
3763
+ if (!running) return;
3764
+ if (!startRef.current) startRef.current = performance.now();
3765
+
3766
+ const tick = () => {
3767
+ if (!startRef.current) return;
3768
+ setElapsed(Math.floor((performance.now() - startRef.current) / 1000));
3769
+ rafRef.current = requestAnimationFrame(tick);
3770
+ };
3771
+
3772
+ rafRef.current = requestAnimationFrame(tick);
3773
+ return () => {
3774
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
3775
+ };
3776
+ }, [running]);
3777
+
3778
+ const sec = String(elapsed % 60).padStart(2, '0');
3779
+ const min = String(Math.floor(elapsed / 60)).padStart(2, '0');
3780
+ return <span className="jp-drawer-elapsed" aria-live="off">{min}:{sec}</span>;
3781
+ }
3782
+
3783
+ END_OF_FILE_CONTENT
3784
+ # SKIP: src/components/save-drawer/Visuals.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
3785
+ # SKIP: src/components/save-drawer/Visuals.tsx:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
3786
+ echo "Creating src/components/save-drawer/saverStyle.css..."
3787
+ cat << 'END_OF_FILE_CONTENT' > "src/components/save-drawer/saverStyle.css"
3788
+ /* Save Drawer strict_full isolated stylesheet */
3789
+
3790
+ .jp-drawer-root {
3791
+ --background: 222 18% 6%;
3792
+ --foreground: 210 20% 96%;
3793
+ --card: 222 16% 8%;
3794
+ --card-foreground: 210 20% 96%;
3795
+ --primary: 0 0% 95%;
3796
+ --primary-foreground: 222 18% 6%;
3797
+ --secondary: 220 14% 13%;
3798
+ --secondary-foreground: 210 20% 96%;
3799
+ --destructive: 0 72% 51%;
3800
+ --destructive-foreground: 0 0% 98%;
3801
+ --border: 220 14% 13%;
3802
+ --radius: 0.6rem;
3803
+ font-family: 'Geist', system-ui, sans-serif;
3804
+ }
3805
+
3806
+ .jp-drawer-overlay {
3807
+ position: fixed;
3808
+ inset: 0;
3809
+ z-index: 2147483600;
3810
+ background: rgb(0 0 0 / 0.4);
3811
+ backdrop-filter: blur(2px);
3812
+ }
3813
+
3814
+ .jp-drawer-shell {
3815
+ position: fixed;
3816
+ left: 0;
3817
+ right: 0;
3818
+ z-index: 2147483601;
3819
+ display: flex;
3820
+ justify-content: center;
3821
+ padding: 0 1rem;
3822
+ }
3823
+
3824
+ .jp-drawer-card {
3825
+ position: relative;
3826
+ width: 100%;
3827
+ max-width: 31rem;
3828
+ overflow: hidden;
3829
+ border-radius: 1rem;
3830
+ border: 1px solid rgb(255 255 255 / 0.07);
3831
+ }
3832
+
3833
+ .jp-drawer-ambient {
3834
+ position: absolute;
3835
+ inset: 0;
3836
+ pointer-events: none;
3837
+ }
3838
+
3839
+ .jp-drawer-shimmer {
3840
+ position: absolute;
3841
+ inset: 0;
3842
+ overflow: hidden;
3843
+ pointer-events: none;
3844
+ }
3845
+
3846
+ .jp-drawer-shimmer-bar {
3847
+ position: absolute;
3848
+ inset-block: 0;
3849
+ width: 35%;
3850
+ }
3851
+
3852
+ .jp-drawer-content {
3853
+ position: relative;
3854
+ z-index: 10;
3855
+ padding: 2rem 2rem 1.75rem;
3856
+ }
3857
+
3858
+ .jp-drawer-header {
3859
+ margin-bottom: 1.5rem;
3860
+ display: flex;
3861
+ align-items: flex-start;
3862
+ justify-content: space-between;
3863
+ }
3864
+
3865
+ .jp-drawer-header-left {
3866
+ display: flex;
3867
+ flex-direction: column;
3868
+ gap: 0.625rem;
3869
+ }
3870
+
3871
+ .jp-drawer-status {
3872
+ display: flex;
3873
+ align-items: center;
3874
+ gap: 0.5rem;
3875
+ font-size: 0.75rem;
3876
+ font-weight: 700;
3877
+ text-transform: uppercase;
3878
+ letter-spacing: 0.1em;
3879
+ transition: color 0.5s;
3880
+ }
3881
+
3882
+ .jp-drawer-status-dot {
3883
+ width: 0.375rem;
3884
+ height: 0.375rem;
3885
+ border-radius: 9999px;
3886
+ display: inline-block;
3887
+ }
3888
+
3889
+ .jp-drawer-copy {
3890
+ min-height: 52px;
3891
+ }
3892
+
3893
+ .jp-drawer-copy-title {
3894
+ margin: 0;
3895
+ color: white;
3896
+ line-height: 1.25;
3897
+ font-weight: 600;
3898
+ }
3899
+
3900
+ .jp-drawer-copy-title-lg {
3901
+ font-size: 1.125rem;
3902
+ }
3903
+
3904
+ .jp-drawer-copy-title-md {
3905
+ font-size: 1rem;
3906
+ }
3907
+
3908
+ .jp-drawer-copy-sub {
3909
+ margin: 0.125rem 0 0;
3910
+ color: rgb(255 255 255 / 0.4);
3911
+ font-size: 0.875rem;
3912
+ }
3913
+
3914
+ .jp-drawer-copy-sub-error {
3915
+ color: rgb(255 255 255 / 0.35);
3916
+ }
3917
+
3918
+ .jp-drawer-poem-line {
3919
+ margin: 0;
3920
+ font-size: 0.875rem;
3921
+ font-weight: 300;
3922
+ line-height: 1.5;
3923
+ }
3924
+
3925
+ .jp-drawer-poem-line-1 {
3926
+ color: rgb(255 255 255 / 0.55);
3927
+ }
3928
+
3929
+ .jp-drawer-poem-line-2 {
3930
+ color: rgb(255 255 255 / 0.3);
3931
+ }
3932
+
3933
+ .jp-drawer-right {
3934
+ margin-left: 1.5rem;
3935
+ display: flex;
3936
+ flex-direction: column;
3937
+ align-items: flex-end;
3938
+ gap: 0.5rem;
3939
+ flex-shrink: 0;
3940
+ }
3941
+
3942
+ .jp-drawer-countdown-wrap {
3943
+ display: flex;
3944
+ flex-direction: column;
3945
+ align-items: flex-end;
3946
+ gap: 0.5rem;
3947
+ }
3948
+
3949
+ .jp-drawer-countdown-text {
3950
+ font-family: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
3951
+ font-size: 0.75rem;
3952
+ font-weight: 600;
3953
+ color: #34d399;
3954
+ }
3955
+
3956
+ .jp-drawer-countdown-track {
3957
+ width: 6rem;
3958
+ height: 0.125rem;
3959
+ border-radius: 9999px;
3960
+ overflow: hidden;
3961
+ background: rgb(255 255 255 / 0.1);
3962
+ }
3963
+
3964
+ .jp-drawer-countdown-bar {
3965
+ width: 100%;
3966
+ height: 100%;
3967
+ border-radius: 9999px;
3968
+ background: #34d399;
3969
+ }
3970
+
3971
+ .jp-drawer-track-row {
3972
+ margin-bottom: 1rem;
3973
+ display: flex;
3974
+ align-items: center;
3975
+ }
3976
+
3977
+ .jp-drawer-bars-wrap {
3978
+ margin-bottom: 1rem;
3979
+ display: flex;
3980
+ justify-content: center;
3981
+ }
3982
+
3983
+ .jp-drawer-separator {
3984
+ margin-bottom: 1rem;
3985
+ height: 1px;
3986
+ width: 100%;
3987
+ border: 0;
3988
+ background: rgb(255 255 255 / 0.06);
3989
+ }
3990
+
3991
+ .jp-drawer-footer {
3992
+ display: flex;
3993
+ align-items: center;
3994
+ gap: 1rem;
3995
+ }
3996
+
3997
+ .jp-drawer-progress {
3998
+ flex: 1;
3999
+ height: 2px;
4000
+ border-radius: 9999px;
4001
+ overflow: hidden;
4002
+ background: rgb(255 255 255 / 0.06);
4003
+ }
4004
+
4005
+ .jp-drawer-progress-indicator {
4006
+ height: 100%;
4007
+ }
4008
+
4009
+ .jp-drawer-cta {
4010
+ display: flex;
4011
+ align-items: center;
4012
+ gap: 0.5rem;
4013
+ flex-shrink: 0;
4014
+ }
4015
+
4016
+ .jp-drawer-running-step {
4017
+ font-family: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
4018
+ font-size: 0.75rem;
4019
+ color: rgb(255 255 255 / 0.2);
4020
+ }
4021
+
4022
+ .jp-drawer-btn-row {
4023
+ display: flex;
4024
+ gap: 0.5rem;
4025
+ }
4026
+
4027
+ .jp-drawer-btn {
4028
+ border: 1px solid transparent;
4029
+ border-radius: 0.375rem;
4030
+ font-size: 0.8125rem;
4031
+ font-weight: 500;
4032
+ line-height: 1;
4033
+ height: 2.25rem;
4034
+ padding: 0 0.75rem;
4035
+ cursor: pointer;
4036
+ transition: all 0.2s ease;
4037
+ display: inline-flex;
4038
+ align-items: center;
4039
+ justify-content: center;
4040
+ gap: 0.375rem;
4041
+ }
4042
+
4043
+ .jp-drawer-btn:disabled {
4044
+ opacity: 0.5;
4045
+ cursor: not-allowed;
4046
+ }
4047
+
4048
+ .jp-drawer-btn-secondary {
4049
+ background: hsl(var(--secondary));
4050
+ color: hsl(var(--secondary-foreground));
4051
+ }
4052
+
4053
+ .jp-drawer-btn-secondary:hover {
4054
+ filter: brightness(1.08);
4055
+ }
4056
+
4057
+ .jp-drawer-btn-emerald {
4058
+ background: #34d399;
4059
+ color: #18181b;
4060
+ font-weight: 600;
4061
+ }
4062
+
4063
+ .jp-drawer-btn-emerald:hover {
4064
+ background: #6ee7b7;
4065
+ }
4066
+
4067
+ .jp-drawer-btn-ghost {
4068
+ background: transparent;
4069
+ color: rgb(255 255 255 / 0.9);
4070
+ }
4071
+
4072
+ .jp-drawer-btn-ghost:hover {
4073
+ background: rgb(255 255 255 / 0.08);
4074
+ }
4075
+
4076
+ .jp-drawer-btn-destructive {
4077
+ background: hsl(var(--destructive));
4078
+ color: hsl(var(--destructive-foreground));
4079
+ }
4080
+
4081
+ .jp-drawer-btn-destructive:hover {
4082
+ filter: brightness(1.06);
4083
+ }
4084
+
4085
+ .jp-drawer-node-wrap {
4086
+ position: relative;
4087
+ z-index: 10;
4088
+ display: flex;
4089
+ flex-direction: column;
4090
+ align-items: center;
4091
+ gap: 0.625rem;
4092
+ }
4093
+
4094
+ .jp-drawer-node {
4095
+ position: relative;
4096
+ width: 3rem;
4097
+ height: 3rem;
4098
+ border-radius: 9999px;
4099
+ border: 1px solid transparent;
4100
+ display: flex;
4101
+ align-items: center;
4102
+ justify-content: center;
4103
+ transition: all 0.5s;
4104
+ }
4105
+
4106
+ .jp-drawer-node-pending {
4107
+ border-color: rgb(255 255 255 / 0.08);
4108
+ background: rgb(255 255 255 / 0.02);
4109
+ }
4110
+
4111
+ .jp-drawer-node-glyph {
4112
+ font-size: 1.125rem;
4113
+ line-height: 1;
4114
+ }
4115
+
4116
+ .jp-drawer-node-glyph-active {
4117
+ display: inline-block;
4118
+ }
4119
+
4120
+ .jp-drawer-node-glyph-pending {
4121
+ color: rgb(255 255 255 / 0.15);
4122
+ }
4123
+
4124
+ .jp-drawer-node-ring {
4125
+ position: absolute;
4126
+ border-radius: 9999px;
4127
+ border: 1px solid transparent;
4128
+ }
4129
+
4130
+ .jp-drawer-node-label {
4131
+ font-size: 10px;
4132
+ font-weight: 600;
4133
+ text-transform: uppercase;
4134
+ letter-spacing: 0.1em;
4135
+ transition: color 0.5s;
4136
+ }
4137
+
4138
+ .jp-drawer-connector {
4139
+ position: relative;
4140
+ z-index: 0;
4141
+ flex: 1;
4142
+ height: 2px;
4143
+ margin-top: -24px;
4144
+ }
4145
+
4146
+ .jp-drawer-connector-base {
4147
+ position: absolute;
4148
+ inset: 0;
4149
+ border-radius: 9999px;
4150
+ background: rgb(255 255 255 / 0.08);
4151
+ }
4152
+
4153
+ .jp-drawer-connector-fill {
4154
+ position: absolute;
4155
+ left: 0;
4156
+ right: auto;
4157
+ top: 0;
4158
+ bottom: 0;
4159
+ border-radius: 9999px;
4160
+ }
4161
+
4162
+ .jp-drawer-connector-orb {
4163
+ position: absolute;
4164
+ top: 50%;
4165
+ transform: translateY(-50%);
4166
+ width: 10px;
4167
+ height: 10px;
4168
+ border-radius: 9999px;
4169
+ }
4170
+
4171
+ .jp-drawer-particles {
4172
+ position: absolute;
4173
+ inset: 0;
4174
+ overflow: hidden;
4175
+ pointer-events: none;
4176
+ }
4177
+
4178
+ .jp-drawer-particle {
4179
+ position: absolute;
4180
+ border-radius: 9999px;
4181
+ }
4182
+
4183
+ .jp-drawer-bars {
4184
+ height: 1.75rem;
4185
+ display: flex;
4186
+ align-items: flex-end;
4187
+ gap: 3px;
4188
+ }
4189
+
4190
+ .jp-drawer-bars-placeholder {
4191
+ height: 1.75rem;
4192
+ }
4193
+
4194
+ .jp-drawer-bar {
4195
+ width: 3px;
4196
+ border-radius: 2px;
4197
+ background: #f59e0b;
4198
+ transform-origin: bottom;
4199
+ }
4200
+
4201
+ .jp-drawer-burst {
4202
+ position: absolute;
4203
+ inset: 0;
4204
+ display: flex;
4205
+ align-items: center;
4206
+ justify-content: center;
4207
+ pointer-events: none;
4208
+ }
4209
+
4210
+ .jp-drawer-burst-dot {
4211
+ position: absolute;
4212
+ width: 5px;
4213
+ height: 5px;
4214
+ border-radius: 9999px;
4215
+ }
4216
+
4217
+ .jp-drawer-elapsed {
4218
+ font-family: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
4219
+ font-size: 0.75rem;
4220
+ letter-spacing: 0.1em;
4221
+ color: rgb(255 255 255 / 0.25);
4222
+ }
4223
+
4224
+ /* Animation helper classes */
4225
+ .animate-drawer-up { animation: drawer-up 0.45s cubic-bezier(0.22, 1, 0.36, 1) forwards; }
4226
+ .animate-fade-in { animation: fade-in 0.25s ease forwards; }
4227
+ .animate-fade-up { animation: fade-up 0.35s ease forwards; }
4228
+ .animate-text-in { animation: text-in 0.3s ease forwards; }
4229
+ .animate-success-pop { animation: success-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }
4230
+ .countdown-bar { animation: countdown-drain 3s linear forwards; }
4231
+
4232
+ .stroke-dash-30 {
4233
+ stroke-dasharray: 30;
4234
+ stroke-dashoffset: 30;
4235
+ }
4236
+
4237
+ .animate-check-draw {
4238
+ animation: check-draw 0.4s 0.05s ease forwards;
4239
+ }
4240
+
4241
+ @keyframes check-draw {
4242
+ to { stroke-dashoffset: 0; }
4243
+ }
4244
+
4245
+ @keyframes drawer-up {
4246
+ from { transform: translateY(100%); opacity: 0; }
4247
+ to { transform: translateY(0); opacity: 1; }
4248
+ }
4249
+
4250
+ @keyframes fade-in {
4251
+ from { opacity: 0; }
4252
+ to { opacity: 1; }
4253
+ }
4254
+
4255
+ @keyframes fade-up {
4256
+ from { opacity: 0; transform: translateY(8px); }
4257
+ to { transform: translateY(0); opacity: 1; }
4258
+ }
4259
+
4260
+ @keyframes text-in {
4261
+ from { opacity: 0; transform: translateX(-6px); }
4262
+ to { opacity: 1; transform: translateX(0); }
4263
+ }
4264
+
4265
+ @keyframes success-pop {
4266
+ 0% { transform: scale(0.88); opacity: 0; }
4267
+ 60% { transform: scale(1.04); }
4268
+ 100% { transform: scale(1); opacity: 1; }
4269
+ }
4270
+
4271
+ @keyframes ambient-pulse {
4272
+ 0%, 100% { opacity: 0.3; }
4273
+ 50% { opacity: 0.65; }
4274
+ }
4275
+
4276
+ @keyframes shimmer-sweep {
4277
+ from { transform: translateX(-100%); }
4278
+ to { transform: translateX(250%); }
4279
+ }
4280
+
4281
+ @keyframes node-glow {
4282
+ 0%, 100% { box-shadow: 0 0 12px var(--glow-color,#60a5fa55); }
4283
+ 50% { box-shadow: 0 0 28px var(--glow-color,#60a5fa88), 0 0 48px var(--glow-color,#60a5fa22); }
4284
+ }
4285
+
4286
+ @keyframes glyph-rotate {
4287
+ from { transform: rotate(0deg); }
4288
+ to { transform: rotate(360deg); }
4289
+ }
4290
+
4291
+ @keyframes ring-expand {
4292
+ from { transform: scale(1); opacity: 0.7; }
4293
+ to { transform: scale(2.1); opacity: 0; }
4294
+ }
4295
+
4296
+ @keyframes orb-travel {
4297
+ from { left: 0%; }
4298
+ to { left: calc(100% - 10px); }
4299
+ }
4300
+
4301
+ @keyframes particle-float {
4302
+ 0% { transform: translateY(0) scale(1); opacity: 0; }
4303
+ 15% { opacity: 1; }
4304
+ 100% { transform: translateY(-90px) scale(0.3); opacity: 0; }
4305
+ }
4306
+
4307
+ @keyframes bar-eq {
4308
+ from { transform: scaleY(0.4); }
4309
+ to { transform: scaleY(1); }
4310
+ }
4311
+
4312
+ @keyframes burst-ray {
4313
+ 0% { transform: rotate(var(--r, 0deg)) translateX(0); opacity: 1; }
4314
+ 100% { transform: rotate(var(--r, 0deg)) translateX(56px); opacity: 0; }
4315
+ }
4316
+
4317
+ @keyframes countdown-drain {
4318
+ from { width: 100%; }
4319
+ to { width: 0%; }
4320
+ }
4321
+
4322
+ END_OF_FILE_CONTENT
4323
+ # SKIP: src/components/save-drawer/saverStyle.css:Zone.Identifier è un file binario e non può essere convertito in testo.
4324
+ # SKIP: src/components/save-drawer/saverStyle.css:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
4325
+ mkdir -p "src/components/ui"
4326
+ echo "Creating src/components/ui/checkbox.tsx..."
4327
+ cat << 'END_OF_FILE_CONTENT' > "src/components/ui/checkbox.tsx"
4328
+ "use client"
4329
+
4330
+ import * as React from "react"
4331
+ import { Checkbox as CheckboxPrimitive } from "radix-ui"
4332
+
4333
+ import { cn } from "@/lib/utils"
4334
+ import { CheckIcon } from "lucide-react"
4335
+
4336
+ function Checkbox({
4337
+ className,
4338
+ ...props
4339
+ }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
4340
+ return (
4341
+ <CheckboxPrimitive.Root
4342
+ data-slot="checkbox"
4343
+ className={cn(
4344
+ "border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-[4px] border transition-colors group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50",
4345
+ className
4346
+ )}
4347
+ {...props}
4348
+ >
4349
+ <CheckboxPrimitive.Indicator
4350
+ data-slot="checkbox-indicator"
4351
+ className="[&>svg]:size-3.5 grid place-content-center text-current transition-none"
4352
+ >
4353
+ <CheckIcon
4354
+ />
4355
+ </CheckboxPrimitive.Indicator>
4356
+ </CheckboxPrimitive.Root>
4357
+ )
4358
+ }
4359
+
4360
+ export { Checkbox }
4361
+
4362
+
4363
+
4364
+
4365
+
4366
+ END_OF_FILE_CONTENT
4367
+ echo "Creating src/components/ui/input.tsx..."
4368
+ cat << 'END_OF_FILE_CONTENT' > "src/components/ui/input.tsx"
4369
+ import * as React from "react"
4370
+
4371
+ import { cn } from "@/lib/utils"
4372
+
4373
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
4374
+ return (
4375
+ <input
4376
+ type={type}
4377
+ data-slot="input"
4378
+ className={cn(
4379
+ "dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
4380
+ className
4381
+ )}
4382
+ {...props}
4383
+ />
4384
+ )
4385
+ }
4386
+
4387
+ export { Input }
4388
+
4389
+
4390
+
4391
+
4392
+
4393
+ END_OF_FILE_CONTENT
4394
+ echo "Creating src/components/ui/label.tsx..."
4395
+ cat << 'END_OF_FILE_CONTENT' > "src/components/ui/label.tsx"
4396
+ import * as React from "react"
4397
+ import { Label as LabelPrimitive } from "radix-ui"
4398
+
4399
+ import { cn } from "@/lib/utils"
4400
+
4401
+ function Label({
4402
+ className,
4403
+ ...props
4404
+ }: React.ComponentProps<typeof LabelPrimitive.Root>) {
4405
+ return (
4406
+ <LabelPrimitive.Root
4407
+ data-slot="label"
4408
+ className={cn(
4409
+ "gap-2 text-sm leading-none font-medium group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed",
4410
+ className
4411
+ )}
4412
+ {...props}
4413
+ />
4414
+ )
4415
+ }
4416
+
4417
+ export { Label }
4418
+
4419
+
4420
+
4421
+
4422
+
4423
+ END_OF_FILE_CONTENT
4424
+ echo "Creating src/components/ui/select.tsx..."
4425
+ cat << 'END_OF_FILE_CONTENT' > "src/components/ui/select.tsx"
4426
+ import * as React from "react"
4427
+ import { Select as SelectPrimitive } from "radix-ui"
4428
+
4429
+ import { cn } from "@/lib/utils"
3149
4430
  import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
3150
4431
 
3151
4432
  function Select({
@@ -4587,6 +5868,7 @@ export const ComponentRegistry: {
4587
5868
  };
4588
5869
 
4589
5870
  END_OF_FILE_CONTENT
5871
+ # SKIP: src/lib/ComponentRegistry.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
4590
5872
  echo "Creating src/lib/IconResolver.tsx..."
4591
5873
  cat << 'END_OF_FILE_CONTENT' > "src/lib/IconResolver.tsx"
4592
5874
  import React from 'react';
@@ -4645,6 +5927,7 @@ export const Icon: React.FC<IconProps> = ({ name, size = 20, className }) => {
4645
5927
 
4646
5928
 
4647
5929
  END_OF_FILE_CONTENT
5930
+ # SKIP: src/lib/IconResolver.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
4648
5931
  echo "Creating src/lib/addSectionConfig.ts..."
4649
5932
  cat << 'END_OF_FILE_CONTENT' > "src/lib/addSectionConfig.ts"
4650
5933
  import type { AddSectionConfig } from '@jsonpages/core';
@@ -4704,6 +5987,7 @@ export const addSectionConfig: AddSectionConfig = {
4704
5987
  };
4705
5988
 
4706
5989
  END_OF_FILE_CONTENT
5990
+ # SKIP: src/lib/addSectionConfig.ts:Zone.Identifier è un file binario e non può essere convertito in testo.
4707
5991
  echo "Creating src/lib/base-schemas.ts..."
4708
5992
  cat << 'END_OF_FILE_CONTENT' > "src/lib/base-schemas.ts"
4709
5993
  import { z } from 'zod';
@@ -4746,6 +6030,179 @@ export const CtaSchema = z.object({
4746
6030
  });
4747
6031
 
4748
6032
  END_OF_FILE_CONTENT
6033
+ # SKIP: src/lib/base-schemas.ts:Zone.Identifier è un file binario e non può essere convertito in testo.
6034
+ echo "Creating src/lib/cloudSaveStream.ts..."
6035
+ cat << 'END_OF_FILE_CONTENT' > "src/lib/cloudSaveStream.ts"
6036
+ import type { StepId } from '@/types/deploy';
6037
+
6038
+ interface SaveStreamStepEvent {
6039
+ id: StepId;
6040
+ status: 'running' | 'done';
6041
+ label?: string;
6042
+ }
6043
+
6044
+ interface SaveStreamLogEvent {
6045
+ stepId: StepId;
6046
+ message: string;
6047
+ }
6048
+
6049
+ interface SaveStreamDoneEvent {
6050
+ deployUrl?: string;
6051
+ commitSha?: string;
6052
+ }
6053
+
6054
+ interface SaveStreamErrorEvent {
6055
+ message?: string;
6056
+ }
6057
+
6058
+ interface StartCloudSaveStreamInput {
6059
+ apiBaseUrl: string;
6060
+ apiKey: string;
6061
+ path: string;
6062
+ content: unknown;
6063
+ message?: string;
6064
+ signal?: AbortSignal;
6065
+ onStep: (event: SaveStreamStepEvent) => void;
6066
+ onLog?: (event: SaveStreamLogEvent) => void;
6067
+ onDone: (event: SaveStreamDoneEvent) => void;
6068
+ }
6069
+
6070
+ function parseSseEventBlock(rawBlock: string): { event: string; data: string } | null {
6071
+ const lines = rawBlock
6072
+ .split('\n')
6073
+ .map((line) => line.trimEnd())
6074
+ .filter((line) => line.length > 0);
6075
+
6076
+ if (lines.length === 0) return null;
6077
+
6078
+ let eventName = 'message';
6079
+ const dataLines: string[] = [];
6080
+ for (const line of lines) {
6081
+ if (line.startsWith('event:')) {
6082
+ eventName = line.slice(6).trim();
6083
+ continue;
6084
+ }
6085
+ if (line.startsWith('data:')) {
6086
+ dataLines.push(line.slice(5).trim());
6087
+ }
6088
+ }
6089
+ return { event: eventName, data: dataLines.join('\n') };
6090
+ }
6091
+
6092
+ export async function startCloudSaveStream(input: StartCloudSaveStreamInput): Promise<void> {
6093
+ const response = await fetch(`${input.apiBaseUrl}/save-stream`, {
6094
+ method: 'POST',
6095
+ headers: {
6096
+ 'Content-Type': 'application/json',
6097
+ Authorization: `Bearer ${input.apiKey}`,
6098
+ },
6099
+ body: JSON.stringify({
6100
+ path: input.path,
6101
+ content: input.content,
6102
+ message: input.message,
6103
+ }),
6104
+ signal: input.signal,
6105
+ });
6106
+
6107
+ if (!response.ok || !response.body) {
6108
+ const body = (await response.json().catch(() => ({}))) as SaveStreamErrorEvent;
6109
+ throw new Error(body.message ?? `Cloud save stream failed: ${response.status}`);
6110
+ }
6111
+
6112
+ const reader = response.body.getReader();
6113
+ const decoder = new TextDecoder();
6114
+ let buffer = '';
6115
+ let receivedDone = false;
6116
+
6117
+ while (true) {
6118
+ const { value, done } = await reader.read();
6119
+ if (!done) {
6120
+ buffer += decoder.decode(value, { stream: true });
6121
+ } else {
6122
+ buffer += decoder.decode();
6123
+ }
6124
+
6125
+ const chunks = buffer.split('\n\n');
6126
+ buffer = done ? '' : (chunks.pop() ?? '');
6127
+
6128
+ for (const chunk of chunks) {
6129
+ const parsed = parseSseEventBlock(chunk);
6130
+ if (!parsed) continue;
6131
+ if (!parsed.data) continue;
6132
+
6133
+ if (parsed.event === 'step') {
6134
+ const payload = JSON.parse(parsed.data) as SaveStreamStepEvent;
6135
+ input.onStep(payload);
6136
+ } else if (parsed.event === 'log') {
6137
+ const payload = JSON.parse(parsed.data) as SaveStreamLogEvent;
6138
+ input.onLog?.(payload);
6139
+ } else if (parsed.event === 'error') {
6140
+ const payload = JSON.parse(parsed.data) as SaveStreamErrorEvent;
6141
+ throw new Error(payload.message ?? 'Cloud save failed.');
6142
+ } else if (parsed.event === 'done') {
6143
+ const payload = JSON.parse(parsed.data) as SaveStreamDoneEvent;
6144
+ input.onDone(payload);
6145
+ receivedDone = true;
6146
+ }
6147
+ }
6148
+
6149
+ if (done) break;
6150
+ }
6151
+
6152
+ if (!receivedDone) {
6153
+ throw new Error('Cloud save stream ended before completion.');
6154
+ }
6155
+ }
6156
+
6157
+ END_OF_FILE_CONTENT
6158
+ # SKIP: src/lib/cloudSaveStream.ts:Zone.Identifier è un file binario e non può essere convertito in testo.
6159
+ # SKIP: src/lib/cloudSaveStream.ts:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
6160
+ echo "Creating src/lib/deploySteps.ts..."
6161
+ cat << 'END_OF_FILE_CONTENT' > "src/lib/deploySteps.ts"
6162
+ import type { DeployStep } from '@/types/deploy';
6163
+
6164
+ export const DEPLOY_STEPS: readonly DeployStep[] = [
6165
+ {
6166
+ id: 'commit',
6167
+ label: 'Commit',
6168
+ verb: 'Committing',
6169
+ poem: ['Crystallizing your edit', 'into permanent history.'],
6170
+ color: '#60a5fa',
6171
+ glyph: '◈',
6172
+ duration: 2200,
6173
+ },
6174
+ {
6175
+ id: 'push',
6176
+ label: 'Push',
6177
+ verb: 'Pushing',
6178
+ poem: ['Sending your vision', 'across the wire.'],
6179
+ color: '#a78bfa',
6180
+ glyph: '◎',
6181
+ duration: 2800,
6182
+ },
6183
+ {
6184
+ id: 'build',
6185
+ label: 'Build',
6186
+ verb: 'Building',
6187
+ poem: ['Assembling the pieces,', 'brick by digital brick.'],
6188
+ color: '#f59e0b',
6189
+ glyph: '⬡',
6190
+ duration: 7500,
6191
+ },
6192
+ {
6193
+ id: 'live',
6194
+ label: 'Live',
6195
+ verb: 'Going live',
6196
+ poem: ['Your content', 'is now breathing.'],
6197
+ color: '#34d399',
6198
+ glyph: '✦',
6199
+ duration: 1600,
6200
+ },
6201
+ ] as const;
6202
+
6203
+ END_OF_FILE_CONTENT
6204
+ # SKIP: src/lib/deploySteps.ts:Zone.Identifier è un file binario e non può essere convertito in testo.
6205
+ # SKIP: src/lib/deploySteps.ts:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
4749
6206
  echo "Creating src/lib/draftStorage.ts..."
4750
6207
  cat << 'END_OF_FILE_CONTENT' > "src/lib/draftStorage.ts"
4751
6208
  /**
@@ -4774,6 +6231,7 @@ export function getHydratedData(
4774
6231
  }
4775
6232
 
4776
6233
  END_OF_FILE_CONTENT
6234
+ # SKIP: src/lib/draftStorage.ts:Zone.Identifier è un file binario e non può essere convertito in testo.
4777
6235
  echo "Creating src/lib/getFilePages.ts..."
4778
6236
  cat << 'END_OF_FILE_CONTENT' > "src/lib/getFilePages.ts"
4779
6237
  /**
@@ -4809,6 +6267,7 @@ export function getFilePages(): Record<string, PageConfig> {
4809
6267
  }
4810
6268
 
4811
6269
  END_OF_FILE_CONTENT
6270
+ # SKIP: src/lib/getFilePages.ts:Zone.Identifier è un file binario e non può essere convertito in testo.
4812
6271
  echo "Creating src/lib/schemas.ts..."
4813
6272
  cat << 'END_OF_FILE_CONTENT' > "src/lib/schemas.ts"
4814
6273
  export { BaseSectionData, BaseArrayItem, BaseSectionSettingsSchema, CtaSchema } from './base-schemas';
@@ -4856,6 +6315,7 @@ export const SECTION_SCHEMAS = {
4856
6315
  export type SectionType = keyof typeof SECTION_SCHEMAS;
4857
6316
 
4858
6317
  END_OF_FILE_CONTENT
6318
+ # SKIP: src/lib/schemas.ts:Zone.Identifier è un file binario e non può essere convertito in testo.
4859
6319
  echo "Creating src/lib/utils.ts..."
4860
6320
  cat << 'END_OF_FILE_CONTENT' > "src/lib/utils.ts"
4861
6321
  import { clsx, type ClassValue } from 'clsx';
@@ -4866,6 +6326,7 @@ export function cn(...inputs: ClassValue[]) {
4866
6326
  }
4867
6327
 
4868
6328
  END_OF_FILE_CONTENT
6329
+ # SKIP: src/lib/utils.ts:Zone.Identifier è un file binario e non può essere convertito in testo.
4869
6330
  echo "Creating src/main.tsx..."
4870
6331
  cat << 'END_OF_FILE_CONTENT' > "src/main.tsx"
4871
6332
  import '@/types'; // TBP: load type augmentation from capsule-driven types
@@ -4885,26 +6346,9 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
4885
6346
 
4886
6347
  END_OF_FILE_CONTENT
4887
6348
  # SKIP: src/main.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
4888
- echo "Creating src/main_.tsx..."
4889
- cat << 'END_OF_FILE_CONTENT' > "src/main_.tsx"
4890
- import '@/types'; // TBP: load type augmentation from capsule-driven types
4891
- import React from 'react';
4892
- import ReactDOM from 'react-dom/client';
4893
- import App from './App';
4894
- // ... resto del file
4895
-
4896
- ReactDOM.createRoot(document.getElementById('root')!).render(
4897
- <React.StrictMode>
4898
- <App />
4899
- </React.StrictMode>
4900
- );
4901
-
4902
-
4903
-
4904
-
4905
- END_OF_FILE_CONTENT
4906
6349
  # SKIP: src/registry-types.ts è un file binario e non può essere convertito in testo.
4907
6350
  mkdir -p "src/server"
6351
+ mkdir -p "src/types"
4908
6352
  echo "Creating src/types.ts..."
4909
6353
  cat << 'END_OF_FILE_CONTENT' > "src/types.ts"
4910
6354
  import type { MenuItem } from '@jsonpages/core';
@@ -4994,6 +6438,26 @@ declare module '@jsonpages/core' {
4994
6438
  export * from '@jsonpages/core';
4995
6439
 
4996
6440
  END_OF_FILE_CONTENT
6441
+ echo "Creating src/types/deploy.ts..."
6442
+ cat << 'END_OF_FILE_CONTENT' > "src/types/deploy.ts"
6443
+ export type StepId = 'commit' | 'push' | 'build' | 'live';
6444
+
6445
+ export type StepState = 'pending' | 'active' | 'done';
6446
+
6447
+ export type DeployPhase = 'idle' | 'running' | 'done' | 'error';
6448
+
6449
+ export interface DeployStep {
6450
+ id: StepId;
6451
+ label: string;
6452
+ verb: string;
6453
+ poem: [string, string];
6454
+ color: string;
6455
+ glyph: string;
6456
+ duration: number;
6457
+ }
6458
+
6459
+ END_OF_FILE_CONTENT
6460
+ # SKIP: src/types/deploy.ts:Zone.Identifier è un file binario e non può essere convertito in testo.
4997
6461
  echo "Creating src/vite-env.d.ts..."
4998
6462
  cat << 'END_OF_FILE_CONTENT' > "src/vite-env.d.ts"
4999
6463
  /// <reference types="vite/client" />
@@ -5005,4 +6469,26 @@ declare module '*?inline' {
5005
6469
 
5006
6470
 
5007
6471
 
6472
+ END_OF_FILE_CONTENT
6473
+ echo "Creating vercel.json..."
6474
+ cat << 'END_OF_FILE_CONTENT' > "vercel.json"
6475
+ {
6476
+ "rewrites": [
6477
+ {
6478
+ "source": "/(.*)",
6479
+ "destination": "/index.html"
6480
+ }
6481
+ ],
6482
+ "headers": [
6483
+ {
6484
+ "source": "/assets/(.*)",
6485
+ "headers": [
6486
+ {
6487
+ "key": "Cache-Control",
6488
+ "value": "public, max-age=31536000, immutable"
6489
+ }
6490
+ ]
6491
+ }
6492
+ ]
6493
+ }
5008
6494
  END_OF_FILE_CONTENT