@jsonpages/cli 3.0.49 → 3.0.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/src_tenant_alpha.sh +21 -999
- package/assets/vercel.json +1 -1
- package/package.json +1 -1
|
@@ -41,7 +41,6 @@ import { addSectionConfig } from '@/lib/addSectionConfig';
|
|
|
41
41
|
import { getHydratedData } from '@/lib/draftStorage';
|
|
42
42
|
import type { JsonPagesConfig, ProjectState } from '@jsonpages/core';
|
|
43
43
|
import type { SiteConfig, ThemeConfig, MenuConfig } from '@/types';
|
|
44
|
-
import { SaveDrawer } from '@/components/SaveDrawer';
|
|
45
44
|
|
|
46
45
|
import siteData from '@/data/config/site.json';
|
|
47
46
|
import themeData from '@/data/config/theme.json';
|
|
@@ -70,7 +69,6 @@ function getInitialData() {
|
|
|
70
69
|
function App() {
|
|
71
70
|
const [{ pages, siteConfig }] = useState(getInitialData);
|
|
72
71
|
const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
|
|
73
|
-
const [saveDrawer, setSaveDrawer] = useState<{ state: ProjectState; slug: string } | null>(null);
|
|
74
72
|
|
|
75
73
|
useEffect(() => {
|
|
76
74
|
// In Cloud mode, listing assets might be different or disabled for MVP
|
|
@@ -93,14 +91,7 @@ function App() {
|
|
|
93
91
|
addSection: addSectionConfig,
|
|
94
92
|
persistence: {
|
|
95
93
|
async saveToFile(state: ProjectState, slug: string): Promise<void> {
|
|
96
|
-
|
|
97
|
-
// ☁️ SCENARIO A: CLOUD BRIDGE (Production) — open SaveDrawer, stream save-stream
|
|
98
|
-
if (CLOUD_API_URL && CLOUD_API_KEY) {
|
|
99
|
-
setSaveDrawer({ state, slug });
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 💻 SCENARIO B: LOCAL FILESYSTEM (Development)
|
|
94
|
+
// Local only (cloud uses saveToFileStream and Core’s SaveProgressDrawer)
|
|
104
95
|
console.log(`💻 Saving ${slug} to Local Filesystem...`);
|
|
105
96
|
const res = await fetch('/api/save-to-file', {
|
|
106
97
|
method: 'POST',
|
|
@@ -111,6 +102,25 @@ function App() {
|
|
|
111
102
|
const body = (await res.json().catch(() => ({}))) as { error?: string };
|
|
112
103
|
if (!res.ok) throw new Error(body.error ?? `Save to file failed: ${res.status}`);
|
|
113
104
|
},
|
|
105
|
+
...(CLOUD_API_URL && CLOUD_API_KEY
|
|
106
|
+
? {
|
|
107
|
+
saveToFileStream: async (state: ProjectState, slug: string): Promise<Response> => {
|
|
108
|
+
const res = await fetch(`${CLOUD_API_URL}/save-stream`, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: {
|
|
111
|
+
'Content-Type': 'application/json',
|
|
112
|
+
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
113
|
+
},
|
|
114
|
+
body: JSON.stringify({
|
|
115
|
+
path: `src/data/pages/${slug}.json`,
|
|
116
|
+
content: state.page,
|
|
117
|
+
message: `Content update for ${slug} via Visual Editor`,
|
|
118
|
+
}),
|
|
119
|
+
});
|
|
120
|
+
return res;
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
: {}),
|
|
114
124
|
},
|
|
115
125
|
assets: {
|
|
116
126
|
assetsBaseUrl: '/assets',
|
|
@@ -142,20 +152,7 @@ function App() {
|
|
|
142
152
|
},
|
|
143
153
|
};
|
|
144
154
|
|
|
145
|
-
return
|
|
146
|
-
<>
|
|
147
|
-
<JsonPagesEngine config={config} />
|
|
148
|
-
{saveDrawer && CLOUD_API_URL && CLOUD_API_KEY && (
|
|
149
|
-
<SaveDrawer
|
|
150
|
-
cloudUrl={CLOUD_API_URL}
|
|
151
|
-
apiKey={CLOUD_API_KEY}
|
|
152
|
-
state={saveDrawer.state}
|
|
153
|
-
slug={saveDrawer.slug}
|
|
154
|
-
onClose={() => setSaveDrawer(null)}
|
|
155
|
-
/>
|
|
156
|
-
)}
|
|
157
|
-
</>
|
|
158
|
-
);
|
|
155
|
+
return <JsonPagesEngine config={config} />;
|
|
159
156
|
}
|
|
160
157
|
|
|
161
158
|
export default App;
|
|
@@ -292,858 +289,6 @@ export const NotFound: React.FC = () => {
|
|
|
292
289
|
|
|
293
290
|
|
|
294
291
|
|
|
295
|
-
END_OF_FILE_CONTENT
|
|
296
|
-
mkdir -p "src/components/SaveDrawer"
|
|
297
|
-
echo "Creating src/components/SaveDrawer/DeployConnector.tsx..."
|
|
298
|
-
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/DeployConnector.tsx"
|
|
299
|
-
import type { StepState } from './types';
|
|
300
|
-
|
|
301
|
-
interface DeployConnectorProps {
|
|
302
|
-
fromState: StepState;
|
|
303
|
-
toState: StepState;
|
|
304
|
-
color: string;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export function DeployConnector({ fromState, toState, color }: DeployConnectorProps) {
|
|
308
|
-
const filled = fromState === 'done' && toState === 'done';
|
|
309
|
-
const filling = fromState === 'done' && toState === 'active';
|
|
310
|
-
const lit = filled || filling;
|
|
311
|
-
|
|
312
|
-
return (
|
|
313
|
-
<div className="relative flex-1 z-0" style={{ height: 2, marginTop: -24 }}>
|
|
314
|
-
<div className="absolute inset-0 bg-white/8 rounded-full" />
|
|
315
|
-
|
|
316
|
-
<div
|
|
317
|
-
className="absolute top-0 left-0 bottom-0 rounded-full"
|
|
318
|
-
style={{
|
|
319
|
-
background: `linear-gradient(90deg, ${color}cc, ${color}66)`,
|
|
320
|
-
width: filled ? '100%' : filling ? '100%' : '0%',
|
|
321
|
-
transition: filling ? 'width 2s cubic-bezier(0.4,0,0.2,1)' : 'none',
|
|
322
|
-
boxShadow: lit ? `0 0 8px ${color}77` : 'none',
|
|
323
|
-
}}
|
|
324
|
-
/>
|
|
325
|
-
|
|
326
|
-
{filling && (
|
|
327
|
-
<div
|
|
328
|
-
className="absolute rounded-full"
|
|
329
|
-
style={{
|
|
330
|
-
top: '50%',
|
|
331
|
-
transform: 'translateY(-50%)',
|
|
332
|
-
width: 10,
|
|
333
|
-
height: 10,
|
|
334
|
-
background: color,
|
|
335
|
-
boxShadow: `0 0 14px ${color}, 0 0 28px ${color}88`,
|
|
336
|
-
animation: 'save-orb-travel 2s cubic-bezier(0.4,0,0.6,1) forwards',
|
|
337
|
-
}}
|
|
338
|
-
/>
|
|
339
|
-
)}
|
|
340
|
-
</div>
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
END_OF_FILE_CONTENT
|
|
345
|
-
echo "Creating src/components/SaveDrawer/DeployNode.tsx..."
|
|
346
|
-
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/DeployNode.tsx"
|
|
347
|
-
import type { DeployStep, StepState } from './types';
|
|
348
|
-
import { cn } from '@/lib/utils';
|
|
349
|
-
|
|
350
|
-
interface DeployNodeProps {
|
|
351
|
-
step: DeployStep;
|
|
352
|
-
state: StepState;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
export function DeployNode({ step, state }: DeployNodeProps) {
|
|
356
|
-
const isActive = state === 'active';
|
|
357
|
-
const isDone = state === 'done';
|
|
358
|
-
const isPending = state === 'pending';
|
|
359
|
-
|
|
360
|
-
return (
|
|
361
|
-
<div className="flex flex-col items-center gap-2.5 relative z-10">
|
|
362
|
-
<div
|
|
363
|
-
className={cn(
|
|
364
|
-
'relative flex h-12 w-12 items-center justify-center rounded-full border transition-all duration-500',
|
|
365
|
-
isPending && 'border-white/8 bg-white/[0.02]',
|
|
366
|
-
isActive && 'border',
|
|
367
|
-
isDone && 'border-0'
|
|
368
|
-
)}
|
|
369
|
-
style={{
|
|
370
|
-
background: isDone ? step.color : isActive ? 'rgba(0,0,0,0.5)' : undefined,
|
|
371
|
-
borderColor: isActive ? `${step.color}80` : undefined,
|
|
372
|
-
boxShadow: isDone
|
|
373
|
-
? `0 0 20px ${step.color}55, 0 0 40px ${step.color}22`
|
|
374
|
-
: isActive
|
|
375
|
-
? `0 0 14px ${step.color}33`
|
|
376
|
-
: undefined,
|
|
377
|
-
animation: isActive ? 'save-node-glow 2s ease infinite' : undefined,
|
|
378
|
-
['--glow-color' as string]: step.color,
|
|
379
|
-
}}
|
|
380
|
-
>
|
|
381
|
-
{isDone && (
|
|
382
|
-
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" aria-label="Done">
|
|
383
|
-
<path
|
|
384
|
-
className="stroke-dash-30 save-check-draw"
|
|
385
|
-
d="M5 13l4 4L19 7"
|
|
386
|
-
stroke="#0a0f1a"
|
|
387
|
-
strokeWidth="2.5"
|
|
388
|
-
strokeLinecap="round"
|
|
389
|
-
strokeLinejoin="round"
|
|
390
|
-
/>
|
|
391
|
-
</svg>
|
|
392
|
-
)}
|
|
393
|
-
|
|
394
|
-
{isActive && (
|
|
395
|
-
<span
|
|
396
|
-
className="text-lg leading-none"
|
|
397
|
-
style={{
|
|
398
|
-
color: step.color,
|
|
399
|
-
animation: 'save-glyph-rotate 9s linear infinite',
|
|
400
|
-
display: 'inline-block',
|
|
401
|
-
}}
|
|
402
|
-
aria-hidden
|
|
403
|
-
>
|
|
404
|
-
{step.glyph}
|
|
405
|
-
</span>
|
|
406
|
-
)}
|
|
407
|
-
|
|
408
|
-
{isPending && (
|
|
409
|
-
<span className="text-lg leading-none text-white/15" aria-hidden>
|
|
410
|
-
{step.glyph}
|
|
411
|
-
</span>
|
|
412
|
-
)}
|
|
413
|
-
|
|
414
|
-
{isActive && (
|
|
415
|
-
<span
|
|
416
|
-
className="absolute rounded-full border"
|
|
417
|
-
style={{
|
|
418
|
-
inset: -7,
|
|
419
|
-
borderColor: `${step.color}50`,
|
|
420
|
-
animation: 'save-ring-expand 2s ease-out infinite',
|
|
421
|
-
}}
|
|
422
|
-
/>
|
|
423
|
-
)}
|
|
424
|
-
</div>
|
|
425
|
-
|
|
426
|
-
<span
|
|
427
|
-
className="text-[10px] font-semibold uppercase tracking-[0.1em] transition-colors duration-500"
|
|
428
|
-
style={{
|
|
429
|
-
color: isDone ? step.color : isActive ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.18)',
|
|
430
|
-
}}
|
|
431
|
-
>
|
|
432
|
-
{step.label}
|
|
433
|
-
</span>
|
|
434
|
-
</div>
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
END_OF_FILE_CONTENT
|
|
439
|
-
echo "Creating src/components/SaveDrawer/SaveDrawer.tsx..."
|
|
440
|
-
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/SaveDrawer.tsx"
|
|
441
|
-
/**
|
|
442
|
-
* SaveDrawer — Identical to prototype DopaDrawer (dopa-save).
|
|
443
|
-
* Bottom sheet for Cloud save; driven by real SSE (save-stream).
|
|
444
|
-
* Done: countdown 3s + "Chiusura in Xs" + Chiudi + Open site → auto-close. Error: Annulla + Retry.
|
|
445
|
-
*/
|
|
446
|
-
import { useState } from 'react';
|
|
447
|
-
import { cn } from '@/lib/utils';
|
|
448
|
-
import { Separator } from '@/components/ui/separator';
|
|
449
|
-
import { Progress } from '@/components/ui/progress';
|
|
450
|
-
import { DeployNode } from './DeployNode';
|
|
451
|
-
import { DeployConnector } from './DeployConnector';
|
|
452
|
-
import { Particles, BuildBars, SuccessBurst, ElapsedTimer } from './Visuals';
|
|
453
|
-
import { useSaveStream } from './useSaveStream';
|
|
454
|
-
import { DEPLOY_STEPS } from './steps';
|
|
455
|
-
import type { ProjectState } from '@jsonpages/core';
|
|
456
|
-
|
|
457
|
-
export interface SaveDrawerProps {
|
|
458
|
-
cloudUrl: string;
|
|
459
|
-
apiKey: string;
|
|
460
|
-
state: ProjectState;
|
|
461
|
-
slug: string;
|
|
462
|
-
onClose: () => void;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
export function SaveDrawer({ cloudUrl, apiKey, state, slug, onClose }: SaveDrawerProps) {
|
|
466
|
-
const [burst, setBurst] = useState(false);
|
|
467
|
-
|
|
468
|
-
const {
|
|
469
|
-
phase,
|
|
470
|
-
currentStep,
|
|
471
|
-
stepState,
|
|
472
|
-
doneSteps,
|
|
473
|
-
progress,
|
|
474
|
-
deployUrl,
|
|
475
|
-
countdown,
|
|
476
|
-
logs,
|
|
477
|
-
errorMessage,
|
|
478
|
-
retry,
|
|
479
|
-
} = useSaveStream({
|
|
480
|
-
cloudUrl,
|
|
481
|
-
apiKey,
|
|
482
|
-
state,
|
|
483
|
-
slug,
|
|
484
|
-
onClose,
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
const isRunning = phase === 'running';
|
|
488
|
-
const isDone = phase === 'done';
|
|
489
|
-
const isError = phase === 'error';
|
|
490
|
-
|
|
491
|
-
if (isDone && !burst) setBurst(true);
|
|
492
|
-
|
|
493
|
-
const activeColor = isDone
|
|
494
|
-
? '#34d399'
|
|
495
|
-
: isError
|
|
496
|
-
? '#f87171'
|
|
497
|
-
: (currentStep?.color ?? '#60a5fa');
|
|
498
|
-
|
|
499
|
-
const particleCount =
|
|
500
|
-
isDone ? 40
|
|
501
|
-
: doneSteps.length === 3 ? 28
|
|
502
|
-
: doneSteps.length === 2 ? 16
|
|
503
|
-
: doneSteps.length === 1 ? 8
|
|
504
|
-
: 4;
|
|
505
|
-
|
|
506
|
-
return (
|
|
507
|
-
<>
|
|
508
|
-
<div
|
|
509
|
-
className="fixed inset-0 bg-black/40 backdrop-blur-[2px] z-40 save-fade-in"
|
|
510
|
-
onClick={isDone || isError ? onClose : undefined}
|
|
511
|
-
aria-hidden
|
|
512
|
-
/>
|
|
513
|
-
|
|
514
|
-
<div
|
|
515
|
-
role="status"
|
|
516
|
-
aria-live="polite"
|
|
517
|
-
aria-label={isDone ? 'Deploy completed' : isError ? 'Deploy failed' : 'Deploying'}
|
|
518
|
-
className="fixed inset-x-0 bottom-0 z-50 flex justify-center pb-6 px-4 save-drawer-up"
|
|
519
|
-
>
|
|
520
|
-
<div
|
|
521
|
-
className={cn(
|
|
522
|
-
'relative w-full max-w-2xl overflow-hidden rounded-2xl',
|
|
523
|
-
'border border-white/[0.07] bg-[hsl(222,18%,7%)]'
|
|
524
|
-
)}
|
|
525
|
-
style={{
|
|
526
|
-
boxShadow: `0 0 0 1px rgba(255,255,255,0.04), 0 -20px 60px rgba(0,0,0,0.6), 0 0 80px ${activeColor}0d`,
|
|
527
|
-
transition: 'box-shadow 1.2s ease',
|
|
528
|
-
}}
|
|
529
|
-
>
|
|
530
|
-
<div
|
|
531
|
-
className="pointer-events-none absolute inset-0"
|
|
532
|
-
style={{
|
|
533
|
-
background: `radial-gradient(ellipse 70% 60% at 50% 110%, ${activeColor}12 0%, transparent 65%)`,
|
|
534
|
-
transition: 'background 1.5s ease',
|
|
535
|
-
animation: 'save-ambient-pulse 3.5s ease infinite',
|
|
536
|
-
}}
|
|
537
|
-
aria-hidden
|
|
538
|
-
/>
|
|
539
|
-
|
|
540
|
-
{isDone && (
|
|
541
|
-
<div className="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden>
|
|
542
|
-
<div
|
|
543
|
-
className="absolute inset-y-0 w-[35%]"
|
|
544
|
-
style={{
|
|
545
|
-
background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.04), transparent)',
|
|
546
|
-
animation: 'save-shimmer-sweep 1.4s 0.1s ease forwards',
|
|
547
|
-
}}
|
|
548
|
-
/>
|
|
549
|
-
</div>
|
|
550
|
-
)}
|
|
551
|
-
|
|
552
|
-
<Particles count={particleCount} color={activeColor} />
|
|
553
|
-
{burst && <SuccessBurst />}
|
|
554
|
-
|
|
555
|
-
<div className="relative z-10 px-8 pt-7 pb-6">
|
|
556
|
-
<div className="flex items-start justify-between mb-7">
|
|
557
|
-
<div className="flex flex-col gap-2.5">
|
|
558
|
-
<div
|
|
559
|
-
className="flex items-center gap-2 text-[11px] font-bold uppercase tracking-[0.1em] transition-colors duration-500"
|
|
560
|
-
style={{ color: activeColor }}
|
|
561
|
-
>
|
|
562
|
-
<span
|
|
563
|
-
className="inline-block h-1.5 w-1.5 rounded-full"
|
|
564
|
-
style={{
|
|
565
|
-
background: activeColor,
|
|
566
|
-
boxShadow: `0 0 6px ${activeColor}`,
|
|
567
|
-
animation: isRunning ? 'save-ambient-pulse 1.5s ease infinite' : 'none',
|
|
568
|
-
}}
|
|
569
|
-
aria-hidden
|
|
570
|
-
/>
|
|
571
|
-
{isDone ? 'Live' : isError ? 'Build failed' : currentStep?.verb ?? 'Saving'}
|
|
572
|
-
</div>
|
|
573
|
-
|
|
574
|
-
<div key={currentStep?.id ?? phase} className="save-text-in min-h-[42px]">
|
|
575
|
-
{isDone ? (
|
|
576
|
-
<div className="save-success-pop">
|
|
577
|
-
<p className="text-lg font-semibold text-white leading-snug">Your content is live.</p>
|
|
578
|
-
<p className="text-sm text-white/40 mt-0.5">Deployed to production successfully</p>
|
|
579
|
-
</div>
|
|
580
|
-
) : isError ? (
|
|
581
|
-
<>
|
|
582
|
-
<p className="text-base font-semibold text-white leading-snug">
|
|
583
|
-
Deploy failed at build.
|
|
584
|
-
</p>
|
|
585
|
-
<p className="text-sm text-white/35 mt-0.5">
|
|
586
|
-
{errorMessage ?? 'Check your Vercel logs or retry below'}
|
|
587
|
-
</p>
|
|
588
|
-
</>
|
|
589
|
-
) : currentStep ? (
|
|
590
|
-
<>
|
|
591
|
-
<p className="text-sm font-light text-white/55 leading-relaxed">{currentStep.poem[0]}</p>
|
|
592
|
-
<p className="text-sm font-light text-white/30 leading-relaxed">{currentStep.poem[1]}</p>
|
|
593
|
-
</>
|
|
594
|
-
) : null}
|
|
595
|
-
</div>
|
|
596
|
-
</div>
|
|
597
|
-
|
|
598
|
-
<div className="flex flex-col items-end gap-1.5 shrink-0 ml-6">
|
|
599
|
-
{isDone ? (
|
|
600
|
-
<div className="flex flex-col items-end gap-1.5 save-fade-up">
|
|
601
|
-
<span
|
|
602
|
-
className="font-mono text-xs text-emerald-400 font-semibold"
|
|
603
|
-
aria-live="polite"
|
|
604
|
-
>
|
|
605
|
-
Chiusura in {countdown}s
|
|
606
|
-
</span>
|
|
607
|
-
<div className="w-24 h-0.5 bg-white/10 rounded-full overflow-hidden">
|
|
608
|
-
<div
|
|
609
|
-
className="h-full bg-emerald-400 rounded-full countdown-bar"
|
|
610
|
-
style={{ boxShadow: '0 0 6px #34d39988' }}
|
|
611
|
-
/>
|
|
612
|
-
</div>
|
|
613
|
-
</div>
|
|
614
|
-
) : (
|
|
615
|
-
<ElapsedTimer running={isRunning} />
|
|
616
|
-
)}
|
|
617
|
-
</div>
|
|
618
|
-
</div>
|
|
619
|
-
|
|
620
|
-
<div className="flex items-center mb-5">
|
|
621
|
-
{DEPLOY_STEPS.map((step, i) => (
|
|
622
|
-
<div
|
|
623
|
-
key={step.id}
|
|
624
|
-
className={cn('flex items-center', i < DEPLOY_STEPS.length - 1 ? 'flex-1' : 'flex-none')}
|
|
625
|
-
>
|
|
626
|
-
<DeployNode step={step} state={stepState(i)} />
|
|
627
|
-
{i < DEPLOY_STEPS.length - 1 && (
|
|
628
|
-
<DeployConnector
|
|
629
|
-
fromState={stepState(i)}
|
|
630
|
-
toState={stepState(i + 1)}
|
|
631
|
-
color={DEPLOY_STEPS[i + 1].color}
|
|
632
|
-
/>
|
|
633
|
-
)}
|
|
634
|
-
</div>
|
|
635
|
-
))}
|
|
636
|
-
</div>
|
|
637
|
-
|
|
638
|
-
<div className="flex justify-center mb-5">
|
|
639
|
-
<BuildBars active={stepState(2) === 'active'} />
|
|
640
|
-
</div>
|
|
641
|
-
|
|
642
|
-
<Separator className="mb-5 bg-white/[0.06]" />
|
|
643
|
-
|
|
644
|
-
{isRunning && currentStep && (logs[currentStep.id]?.length ?? 0) > 0 && (
|
|
645
|
-
<div className="mb-4 max-h-16 overflow-y-auto rounded-md bg-white/[0.04] px-3 py-2 font-mono text-[11px] text-white/50 leading-relaxed">
|
|
646
|
-
{logs[currentStep.id]?.map((line, i) => (
|
|
647
|
-
<div key={i}>{line}</div>
|
|
648
|
-
))}
|
|
649
|
-
</div>
|
|
650
|
-
)}
|
|
651
|
-
|
|
652
|
-
<div className="flex items-center gap-4">
|
|
653
|
-
<div className="flex-1">
|
|
654
|
-
<Progress
|
|
655
|
-
value={progress}
|
|
656
|
-
className="h-[2px] bg-white/[0.06]"
|
|
657
|
-
indicatorClassName="transition-none"
|
|
658
|
-
indicatorStyle={{
|
|
659
|
-
background: `linear-gradient(90deg, ${DEPLOY_STEPS[0].color}, ${activeColor})`,
|
|
660
|
-
}}
|
|
661
|
-
/>
|
|
662
|
-
</div>
|
|
663
|
-
|
|
664
|
-
<div className="flex items-center gap-2 shrink-0">
|
|
665
|
-
{isDone && (
|
|
666
|
-
<div className="flex gap-2 save-fade-up">
|
|
667
|
-
<button
|
|
668
|
-
type="button"
|
|
669
|
-
onClick={onClose}
|
|
670
|
-
className="h-9 rounded-md px-3 inline-flex items-center justify-center text-sm font-medium border border-white/20 bg-white/5 text-white/90 hover:bg-white/10 transition-colors"
|
|
671
|
-
>
|
|
672
|
-
Chiudi
|
|
673
|
-
</button>
|
|
674
|
-
<button
|
|
675
|
-
type="button"
|
|
676
|
-
onClick={() =>
|
|
677
|
-
deployUrl ? window.open(deployUrl, '_blank') : window.location.reload()
|
|
678
|
-
}
|
|
679
|
-
className="h-9 rounded-md px-3 inline-flex items-center justify-center gap-1.5 text-sm font-semibold bg-emerald-400 text-zinc-900 hover:bg-emerald-300 transition-colors"
|
|
680
|
-
>
|
|
681
|
-
<span aria-hidden>↗</span> Open site
|
|
682
|
-
</button>
|
|
683
|
-
</div>
|
|
684
|
-
)}
|
|
685
|
-
|
|
686
|
-
{isError && (
|
|
687
|
-
<div className="flex gap-2 save-fade-up">
|
|
688
|
-
<button
|
|
689
|
-
type="button"
|
|
690
|
-
onClick={onClose}
|
|
691
|
-
className="h-9 rounded-md px-3 inline-flex items-center justify-center text-sm font-medium text-white/80 hover:bg-white/10 transition-colors"
|
|
692
|
-
>
|
|
693
|
-
Annulla
|
|
694
|
-
</button>
|
|
695
|
-
<button
|
|
696
|
-
type="button"
|
|
697
|
-
onClick={retry}
|
|
698
|
-
className="h-9 rounded-md px-3 inline-flex items-center justify-center text-sm font-medium bg-red-600 text-white hover:bg-red-500 transition-colors"
|
|
699
|
-
>
|
|
700
|
-
Retry
|
|
701
|
-
</button>
|
|
702
|
-
</div>
|
|
703
|
-
)}
|
|
704
|
-
|
|
705
|
-
{isRunning && (
|
|
706
|
-
<span className="font-mono text-xs text-white/20" aria-hidden>
|
|
707
|
-
{doneSteps.length + 1} / {DEPLOY_STEPS.length}
|
|
708
|
-
</span>
|
|
709
|
-
)}
|
|
710
|
-
</div>
|
|
711
|
-
</div>
|
|
712
|
-
</div>
|
|
713
|
-
</div>
|
|
714
|
-
</div>
|
|
715
|
-
</>
|
|
716
|
-
);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
END_OF_FILE_CONTENT
|
|
720
|
-
echo "Creating src/components/SaveDrawer/Visuals.tsx..."
|
|
721
|
-
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/Visuals.tsx"
|
|
722
|
-
import { useRef, useState, useEffect } from 'react';
|
|
723
|
-
|
|
724
|
-
const POOL: { id: number; x: number; y: number; size: number; dur: number; delay: number }[] = Array.from(
|
|
725
|
-
{ length: 44 },
|
|
726
|
-
(_, i) => ({
|
|
727
|
-
id: i,
|
|
728
|
-
x: 5 + Math.random() * 90,
|
|
729
|
-
y: 15 + Math.random() * 70,
|
|
730
|
-
size: 1.5 + Math.random() * 2.5,
|
|
731
|
-
dur: 2.8 + Math.random() * 3.5,
|
|
732
|
-
delay: Math.random() * 4,
|
|
733
|
-
})
|
|
734
|
-
);
|
|
735
|
-
|
|
736
|
-
interface ParticlesProps {
|
|
737
|
-
count: number;
|
|
738
|
-
color: string;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
export function Particles({ count, color }: ParticlesProps) {
|
|
742
|
-
return (
|
|
743
|
-
<div className="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden>
|
|
744
|
-
{POOL.slice(0, count).map((p) => (
|
|
745
|
-
<div
|
|
746
|
-
key={p.id}
|
|
747
|
-
className="absolute rounded-full"
|
|
748
|
-
style={{
|
|
749
|
-
left: `${p.x}%`,
|
|
750
|
-
bottom: `${p.y}%`,
|
|
751
|
-
width: p.size,
|
|
752
|
-
height: p.size,
|
|
753
|
-
background: color,
|
|
754
|
-
boxShadow: `0 0 ${p.size * 3}px ${color}`,
|
|
755
|
-
opacity: 0,
|
|
756
|
-
animation: `save-particle-float ${p.dur}s ${p.delay}s ease-out infinite`,
|
|
757
|
-
}}
|
|
758
|
-
/>
|
|
759
|
-
))}
|
|
760
|
-
</div>
|
|
761
|
-
);
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
const BAR_H = [0.45, 0.75, 0.55, 0.9, 0.65, 0.8, 0.5, 0.72, 0.6, 0.85, 0.42, 0.7];
|
|
765
|
-
|
|
766
|
-
interface BuildBarsProps {
|
|
767
|
-
active: boolean;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
export function BuildBars({ active }: BuildBarsProps) {
|
|
771
|
-
if (!active) return <div className="h-7" />;
|
|
772
|
-
return (
|
|
773
|
-
<div className="flex items-end gap-[3px] h-7" aria-hidden>
|
|
774
|
-
{BAR_H.map((h, i) => (
|
|
775
|
-
<div
|
|
776
|
-
key={i}
|
|
777
|
-
className="w-[3px] rounded-sm"
|
|
778
|
-
style={{
|
|
779
|
-
background: '#f59e0b',
|
|
780
|
-
height: `${h * 100}%`,
|
|
781
|
-
transformOrigin: 'bottom',
|
|
782
|
-
animation: `save-bar-eq ${0.42 + i * 0.06}s ${i * 0.04}s ease-in-out infinite alternate`,
|
|
783
|
-
}}
|
|
784
|
-
/>
|
|
785
|
-
))}
|
|
786
|
-
</div>
|
|
787
|
-
);
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
const BURST_COLORS = ['#34d399', '#60a5fa', '#a78bfa', '#f59e0b', '#f472b6'];
|
|
791
|
-
|
|
792
|
-
export function SuccessBurst() {
|
|
793
|
-
return (
|
|
794
|
-
<div className="pointer-events-none absolute inset-0 flex items-center justify-center" aria-hidden>
|
|
795
|
-
{Array.from({ length: 16 }).map((_, i) => (
|
|
796
|
-
<div
|
|
797
|
-
key={i}
|
|
798
|
-
className="absolute rounded-full"
|
|
799
|
-
style={{
|
|
800
|
-
width: 5,
|
|
801
|
-
height: 5,
|
|
802
|
-
background: BURST_COLORS[i % BURST_COLORS.length],
|
|
803
|
-
['--r' as string]: `${i * 22.5}deg`,
|
|
804
|
-
animation: `save-burst-ray 0.85s ${i * 0.03}s cubic-bezier(0,0.6,0.5,1) forwards`,
|
|
805
|
-
transform: `rotate(${i * 22.5}deg)`,
|
|
806
|
-
transformOrigin: '50% 50%',
|
|
807
|
-
opacity: 0,
|
|
808
|
-
}}
|
|
809
|
-
/>
|
|
810
|
-
))}
|
|
811
|
-
</div>
|
|
812
|
-
);
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
interface ElapsedTimerProps {
|
|
816
|
-
running: boolean;
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
export function ElapsedTimer({ running }: ElapsedTimerProps) {
|
|
820
|
-
const [elapsed, setElapsed] = useState(0);
|
|
821
|
-
const startRef = useRef<number | null>(null);
|
|
822
|
-
const rafRef = useRef<number | null>(null);
|
|
823
|
-
|
|
824
|
-
useEffect(() => {
|
|
825
|
-
if (!running) return;
|
|
826
|
-
if (!startRef.current) startRef.current = performance.now();
|
|
827
|
-
|
|
828
|
-
const tick = (): void => {
|
|
829
|
-
setElapsed(Math.floor((performance.now() - (startRef.current ?? 0)) / 1000));
|
|
830
|
-
rafRef.current = requestAnimationFrame(tick);
|
|
831
|
-
};
|
|
832
|
-
rafRef.current = requestAnimationFrame(tick);
|
|
833
|
-
return () => {
|
|
834
|
-
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
835
|
-
};
|
|
836
|
-
}, [running]);
|
|
837
|
-
|
|
838
|
-
const s = String(elapsed % 60).padStart(2, '0');
|
|
839
|
-
const m = String(Math.floor(elapsed / 60)).padStart(2, '0');
|
|
840
|
-
|
|
841
|
-
return (
|
|
842
|
-
<span className="font-mono text-xs text-white/25 tracking-widest" aria-live="off">
|
|
843
|
-
{m}:{s}
|
|
844
|
-
</span>
|
|
845
|
-
);
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
END_OF_FILE_CONTENT
|
|
849
|
-
echo "Creating src/components/SaveDrawer/index.ts..."
|
|
850
|
-
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/index.ts"
|
|
851
|
-
export { SaveDrawer } from './SaveDrawer';
|
|
852
|
-
|
|
853
|
-
END_OF_FILE_CONTENT
|
|
854
|
-
echo "Creating src/components/SaveDrawer/steps.ts..."
|
|
855
|
-
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/steps.ts"
|
|
856
|
-
import type { DeployStep } from './types';
|
|
857
|
-
|
|
858
|
-
export const DEPLOY_STEPS: readonly DeployStep[] = [
|
|
859
|
-
{
|
|
860
|
-
id: 'commit',
|
|
861
|
-
label: 'Commit',
|
|
862
|
-
verb: 'Committing',
|
|
863
|
-
poem: ['Crystallizing your edit', 'into permanent history.'],
|
|
864
|
-
color: '#60a5fa',
|
|
865
|
-
glyph: '◈',
|
|
866
|
-
duration: 2200,
|
|
867
|
-
},
|
|
868
|
-
{
|
|
869
|
-
id: 'push',
|
|
870
|
-
label: 'Push',
|
|
871
|
-
verb: 'Pushing',
|
|
872
|
-
poem: ['Sending your vision', 'across the wire.'],
|
|
873
|
-
color: '#a78bfa',
|
|
874
|
-
glyph: '◎',
|
|
875
|
-
duration: 2800,
|
|
876
|
-
},
|
|
877
|
-
{
|
|
878
|
-
id: 'build',
|
|
879
|
-
label: 'Build',
|
|
880
|
-
verb: 'Building',
|
|
881
|
-
poem: ['Assembling the pieces,', 'brick by digital brick.'],
|
|
882
|
-
color: '#f59e0b',
|
|
883
|
-
glyph: '⬡',
|
|
884
|
-
duration: 7500,
|
|
885
|
-
},
|
|
886
|
-
{
|
|
887
|
-
id: 'live',
|
|
888
|
-
label: 'Live',
|
|
889
|
-
verb: 'Going live',
|
|
890
|
-
poem: ['Your content', 'is now breathing.'],
|
|
891
|
-
color: '#34d399',
|
|
892
|
-
glyph: '✦',
|
|
893
|
-
duration: 1600,
|
|
894
|
-
},
|
|
895
|
-
] as const;
|
|
896
|
-
|
|
897
|
-
END_OF_FILE_CONTENT
|
|
898
|
-
echo "Creating src/components/SaveDrawer/types.ts..."
|
|
899
|
-
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/types.ts"
|
|
900
|
-
/** Save/deploy flow types (aligned with prototype) */
|
|
901
|
-
|
|
902
|
-
export type StepId = 'commit' | 'push' | 'build' | 'live';
|
|
903
|
-
|
|
904
|
-
export type StepState = 'pending' | 'active' | 'done';
|
|
905
|
-
|
|
906
|
-
export type DeployPhase = 'running' | 'done' | 'error';
|
|
907
|
-
|
|
908
|
-
export interface DeployStep {
|
|
909
|
-
id: StepId;
|
|
910
|
-
label: string;
|
|
911
|
-
verb: string;
|
|
912
|
-
poem: [string, string];
|
|
913
|
-
color: string;
|
|
914
|
-
glyph: string;
|
|
915
|
-
duration: number;
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
END_OF_FILE_CONTENT
|
|
919
|
-
echo "Creating src/components/SaveDrawer/useSaveStream.ts..."
|
|
920
|
-
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/useSaveStream.ts"
|
|
921
|
-
/**
|
|
922
|
-
* useSaveStream — Same API as prototype useSaveOrchestrator, driven by SSE from save-stream.
|
|
923
|
-
* Event handling aligned with CreateTenantFlow (parseSSE, step/log/error/done, post-loop, try/catch).
|
|
924
|
-
* Payload is read from a ref at effect start so the effect does not depend on state.page (avoids re-run and stream kill).
|
|
925
|
-
*/
|
|
926
|
-
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
927
|
-
import type { ProjectState } from '@jsonpages/core';
|
|
928
|
-
import type { DeployPhase, StepId, StepState } from './types';
|
|
929
|
-
import { DEPLOY_STEPS } from './steps';
|
|
930
|
-
|
|
931
|
-
function parseSSE(buffer: string): { event: string; data: string }[] {
|
|
932
|
-
const lines = buffer.split('\n');
|
|
933
|
-
const out: { event: string; data: string }[] = [];
|
|
934
|
-
let event = 'message';
|
|
935
|
-
let data = '';
|
|
936
|
-
for (const line of lines) {
|
|
937
|
-
if (line.startsWith('event:')) event = line.slice(6).trim();
|
|
938
|
-
else if (line.startsWith('data:')) data = line.slice(5).trim();
|
|
939
|
-
else if (line === '' && data) {
|
|
940
|
-
out.push({ event, data });
|
|
941
|
-
data = '';
|
|
942
|
-
event = 'message';
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
if (data) out.push({ event, data });
|
|
946
|
-
return out;
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
interface UseSaveStreamOptions {
|
|
950
|
-
cloudUrl: string;
|
|
951
|
-
apiKey: string;
|
|
952
|
-
state: ProjectState;
|
|
953
|
-
slug: string;
|
|
954
|
-
onClose: () => void;
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
export function useSaveStream({
|
|
958
|
-
cloudUrl,
|
|
959
|
-
apiKey,
|
|
960
|
-
state,
|
|
961
|
-
slug,
|
|
962
|
-
onClose,
|
|
963
|
-
}: UseSaveStreamOptions) {
|
|
964
|
-
const [phase, setPhase] = useState<DeployPhase>('running');
|
|
965
|
-
const [doneSteps, setDoneSteps] = useState<StepId[]>([]);
|
|
966
|
-
const [activeStepId, setActiveStepId] = useState<StepId | null>('commit');
|
|
967
|
-
const [progress, setProgress] = useState(0);
|
|
968
|
-
const [deployUrl, setDeployUrl] = useState<string | null>(null);
|
|
969
|
-
const [countdown, setCountdown] = useState(3);
|
|
970
|
-
const [logs, setLogs] = useState<Record<string, string[]>>({});
|
|
971
|
-
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
972
|
-
const progressRef = useRef(0);
|
|
973
|
-
const [runId, setRunId] = useState(0);
|
|
974
|
-
|
|
975
|
-
const payloadRef = useRef<{ path: string; content: unknown; message: string } | null>(null);
|
|
976
|
-
|
|
977
|
-
const addLog = useCallback((stepId: string, message: string) => {
|
|
978
|
-
setLogs((prev) => ({ ...prev, [stepId]: [...(prev[stepId] || []), message] }));
|
|
979
|
-
}, []);
|
|
980
|
-
|
|
981
|
-
const stepState = useCallback(
|
|
982
|
-
(i: number): StepState => {
|
|
983
|
-
const step = DEPLOY_STEPS[i];
|
|
984
|
-
if (!step) return 'pending';
|
|
985
|
-
if (doneSteps.includes(step.id)) return 'done';
|
|
986
|
-
if (activeStepId === step.id && phase === 'running') return 'active';
|
|
987
|
-
return 'pending';
|
|
988
|
-
},
|
|
989
|
-
[doneSteps, activeStepId, phase]
|
|
990
|
-
);
|
|
991
|
-
|
|
992
|
-
const currentStep = DEPLOY_STEPS.find((s) => s.id === activeStepId) ?? DEPLOY_STEPS[0] ?? null;
|
|
993
|
-
|
|
994
|
-
useEffect(() => {
|
|
995
|
-
let cancelled = false;
|
|
996
|
-
let gotDone = false;
|
|
997
|
-
setPhase('running');
|
|
998
|
-
setDoneSteps([]);
|
|
999
|
-
setActiveStepId('commit');
|
|
1000
|
-
setProgress(0);
|
|
1001
|
-
setDeployUrl(null);
|
|
1002
|
-
setCountdown(3);
|
|
1003
|
-
setLogs({});
|
|
1004
|
-
setErrorMessage(null);
|
|
1005
|
-
progressRef.current = 0;
|
|
1006
|
-
|
|
1007
|
-
payloadRef.current = {
|
|
1008
|
-
path: `src/data/pages/${slug}.json`,
|
|
1009
|
-
content: state.page,
|
|
1010
|
-
message: `Content update for ${slug} via Visual Editor`,
|
|
1011
|
-
};
|
|
1012
|
-
|
|
1013
|
-
const base = cloudUrl.replace(/\/$/, '');
|
|
1014
|
-
const url = base.includes('/api/v1')
|
|
1015
|
-
? `${base}/save-stream`
|
|
1016
|
-
: `${base}/api/v1/save-stream`;
|
|
1017
|
-
|
|
1018
|
-
const run = async () => {
|
|
1019
|
-
try {
|
|
1020
|
-
const payload = payloadRef.current;
|
|
1021
|
-
if (!payload || cancelled) return;
|
|
1022
|
-
|
|
1023
|
-
const res = await fetch(url, {
|
|
1024
|
-
method: 'POST',
|
|
1025
|
-
headers: {
|
|
1026
|
-
'Content-Type': 'application/json',
|
|
1027
|
-
Authorization: `Bearer ${apiKey}`,
|
|
1028
|
-
},
|
|
1029
|
-
body: JSON.stringify(payload),
|
|
1030
|
-
});
|
|
1031
|
-
|
|
1032
|
-
if (!res.ok || !res.body) {
|
|
1033
|
-
if (!cancelled) {
|
|
1034
|
-
setPhase('error');
|
|
1035
|
-
setErrorMessage(res.status ? `HTTP ${res.status}` : 'Request failed');
|
|
1036
|
-
}
|
|
1037
|
-
return;
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
const reader = res.body.getReader();
|
|
1041
|
-
const dec = new TextDecoder();
|
|
1042
|
-
let buffer = '';
|
|
1043
|
-
|
|
1044
|
-
while (true) {
|
|
1045
|
-
const { value, done: streamDone } = await reader.read();
|
|
1046
|
-
if (streamDone || cancelled) break;
|
|
1047
|
-
buffer += dec.decode(value, { stream: true });
|
|
1048
|
-
const parts = buffer.split('\n\n');
|
|
1049
|
-
buffer = parts.pop() || '';
|
|
1050
|
-
|
|
1051
|
-
for (const part of parts) {
|
|
1052
|
-
const parsed = parseSSE(part + '\n\n');
|
|
1053
|
-
for (const { event: ev, data: raw } of parsed) {
|
|
1054
|
-
if (cancelled) return;
|
|
1055
|
-
try {
|
|
1056
|
-
const data = JSON.parse(raw) as Record<string, unknown>;
|
|
1057
|
-
if (ev === 'step') {
|
|
1058
|
-
const id = data.id as StepId | undefined;
|
|
1059
|
-
const status = data.status as string | undefined;
|
|
1060
|
-
if (id && status === 'running') {
|
|
1061
|
-
setActiveStepId(id);
|
|
1062
|
-
const idx = DEPLOY_STEPS.findIndex((s) => s.id === id);
|
|
1063
|
-
if (idx >= 0) progressRef.current = Math.round((idx / DEPLOY_STEPS.length) * 100);
|
|
1064
|
-
setProgress(progressRef.current);
|
|
1065
|
-
}
|
|
1066
|
-
if (id && status === 'done') {
|
|
1067
|
-
setDoneSteps((prev) => [...prev, id].filter(Boolean));
|
|
1068
|
-
setActiveStepId(null);
|
|
1069
|
-
const idx = DEPLOY_STEPS.findIndex((s) => s.id === id);
|
|
1070
|
-
progressRef.current = idx >= 0 ? Math.round(((idx + 1) / DEPLOY_STEPS.length) * 100) : 100;
|
|
1071
|
-
setProgress(progressRef.current);
|
|
1072
|
-
}
|
|
1073
|
-
} else if (ev === 'log' && data.stepId && data.message) {
|
|
1074
|
-
addLog(data.stepId as string, data.message as string);
|
|
1075
|
-
} else if (ev === 'error') {
|
|
1076
|
-
if (!cancelled) {
|
|
1077
|
-
setErrorMessage((data.message as string) || 'Errore');
|
|
1078
|
-
setPhase('error');
|
|
1079
|
-
}
|
|
1080
|
-
return;
|
|
1081
|
-
} else if (ev === 'done') {
|
|
1082
|
-
gotDone = true;
|
|
1083
|
-
setDeployUrl((data.deployUrl as string) ?? null);
|
|
1084
|
-
setProgress(100);
|
|
1085
|
-
setDoneSteps(['commit', 'push', 'build', 'live']);
|
|
1086
|
-
setActiveStepId(null);
|
|
1087
|
-
setPhase('done');
|
|
1088
|
-
return;
|
|
1089
|
-
}
|
|
1090
|
-
} catch {
|
|
1091
|
-
// ignore parse errors
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
if (!cancelled && !gotDone) {
|
|
1098
|
-
setErrorMessage('Operazione interrotta o build non completato.');
|
|
1099
|
-
setPhase('error');
|
|
1100
|
-
}
|
|
1101
|
-
} catch (e) {
|
|
1102
|
-
if (!cancelled) {
|
|
1103
|
-
setErrorMessage(e instanceof Error ? e.message : 'Errore');
|
|
1104
|
-
setPhase('error');
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
};
|
|
1108
|
-
|
|
1109
|
-
run();
|
|
1110
|
-
return () => {
|
|
1111
|
-
cancelled = true;
|
|
1112
|
-
};
|
|
1113
|
-
}, [cloudUrl, apiKey, slug, runId, addLog]);
|
|
1114
|
-
|
|
1115
|
-
useEffect(() => {
|
|
1116
|
-
if (phase !== 'done') return;
|
|
1117
|
-
setCountdown(3);
|
|
1118
|
-
const iv = setInterval(() => {
|
|
1119
|
-
setCountdown((s) => {
|
|
1120
|
-
if (s <= 1) {
|
|
1121
|
-
clearInterval(iv);
|
|
1122
|
-
onClose();
|
|
1123
|
-
return 0;
|
|
1124
|
-
}
|
|
1125
|
-
return s - 1;
|
|
1126
|
-
});
|
|
1127
|
-
}, 1000);
|
|
1128
|
-
return () => clearInterval(iv);
|
|
1129
|
-
}, [phase, onClose]);
|
|
1130
|
-
|
|
1131
|
-
const retry = useCallback(() => setRunId((r) => r + 1), []);
|
|
1132
|
-
|
|
1133
|
-
return {
|
|
1134
|
-
phase,
|
|
1135
|
-
currentStep,
|
|
1136
|
-
stepState,
|
|
1137
|
-
doneSteps,
|
|
1138
|
-
progress,
|
|
1139
|
-
deployUrl,
|
|
1140
|
-
countdown,
|
|
1141
|
-
logs,
|
|
1142
|
-
errorMessage,
|
|
1143
|
-
retry,
|
|
1144
|
-
};
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
292
|
END_OF_FILE_CONTENT
|
|
1148
293
|
mkdir -p "src/components/arch-layers"
|
|
1149
294
|
echo "Creating src/components/arch-layers/View.tsx..."
|
|
@@ -3982,47 +3127,6 @@ export { Label }
|
|
|
3982
3127
|
|
|
3983
3128
|
|
|
3984
3129
|
|
|
3985
|
-
END_OF_FILE_CONTENT
|
|
3986
|
-
echo "Creating src/components/ui/progress.tsx..."
|
|
3987
|
-
cat << 'END_OF_FILE_CONTENT' > "src/components/ui/progress.tsx"
|
|
3988
|
-
import * as React from 'react';
|
|
3989
|
-
import { cn } from '@/lib/utils';
|
|
3990
|
-
|
|
3991
|
-
interface ProgressProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
3992
|
-
value?: number | null;
|
|
3993
|
-
indicatorClassName?: string;
|
|
3994
|
-
indicatorStyle?: React.CSSProperties;
|
|
3995
|
-
}
|
|
3996
|
-
|
|
3997
|
-
const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
|
|
3998
|
-
({ className, value, indicatorClassName, indicatorStyle, ...props }, ref) => (
|
|
3999
|
-
<div
|
|
4000
|
-
ref={ref}
|
|
4001
|
-
role="progressbar"
|
|
4002
|
-
aria-valuenow={value ?? 0}
|
|
4003
|
-
aria-valuemin={0}
|
|
4004
|
-
aria-valuemax={100}
|
|
4005
|
-
className={cn('relative h-2 w-full overflow-hidden rounded-full bg-secondary', className)}
|
|
4006
|
-
{...props}
|
|
4007
|
-
>
|
|
4008
|
-
<div
|
|
4009
|
-
className={cn(
|
|
4010
|
-
'h-full min-w-full bg-primary transition-all duration-700 ease-out',
|
|
4011
|
-
indicatorClassName
|
|
4012
|
-
)}
|
|
4013
|
-
style={{
|
|
4014
|
-
width: '100%',
|
|
4015
|
-
transform: `translateX(-${100 - (value ?? 0)}%)`,
|
|
4016
|
-
...indicatorStyle,
|
|
4017
|
-
}}
|
|
4018
|
-
/>
|
|
4019
|
-
</div>
|
|
4020
|
-
)
|
|
4021
|
-
);
|
|
4022
|
-
Progress.displayName = 'Progress';
|
|
4023
|
-
|
|
4024
|
-
export { Progress };
|
|
4025
|
-
|
|
4026
3130
|
END_OF_FILE_CONTENT
|
|
4027
3131
|
echo "Creating src/components/ui/select.tsx..."
|
|
4028
3132
|
cat << 'END_OF_FILE_CONTENT' > "src/components/ui/select.tsx"
|
|
@@ -5418,88 +4522,6 @@ html {
|
|
|
5418
4522
|
opacity: 0;
|
|
5419
4523
|
}
|
|
5420
4524
|
|
|
5421
|
-
/* ==========================================================================
|
|
5422
|
-
SAVE DRAWER (prototype-identical animations)
|
|
5423
|
-
========================================================================== */
|
|
5424
|
-
@keyframes save-drawer-up {
|
|
5425
|
-
from { transform: translateY(100%); opacity: 0; }
|
|
5426
|
-
to { transform: translateY(0); opacity: 1; }
|
|
5427
|
-
}
|
|
5428
|
-
@keyframes save-fade-in {
|
|
5429
|
-
from { opacity: 0; }
|
|
5430
|
-
to { opacity: 1; }
|
|
5431
|
-
}
|
|
5432
|
-
@keyframes save-fade-up {
|
|
5433
|
-
from { opacity: 0; transform: translateY(8px); }
|
|
5434
|
-
to { opacity: 1; transform: translateY(0); }
|
|
5435
|
-
}
|
|
5436
|
-
@keyframes save-success-pop {
|
|
5437
|
-
0% { transform: scale(0.88); opacity: 0; }
|
|
5438
|
-
60% { transform: scale(1.04); }
|
|
5439
|
-
100% { transform: scale(1); opacity: 1; }
|
|
5440
|
-
}
|
|
5441
|
-
@keyframes save-ring-expand {
|
|
5442
|
-
from { transform: scale(1); opacity: 0.7; }
|
|
5443
|
-
to { transform: scale(2.1); opacity: 0; }
|
|
5444
|
-
}
|
|
5445
|
-
@keyframes save-orb-travel {
|
|
5446
|
-
from { left: 0%; }
|
|
5447
|
-
to { left: calc(100% - 10px); }
|
|
5448
|
-
}
|
|
5449
|
-
@keyframes save-glyph-rotate {
|
|
5450
|
-
from { transform: rotate(0deg); }
|
|
5451
|
-
to { transform: rotate(360deg); }
|
|
5452
|
-
}
|
|
5453
|
-
@keyframes save-particle-float {
|
|
5454
|
-
0% { transform: translateY(0) scale(1); opacity: 0; }
|
|
5455
|
-
15% { opacity: 1; }
|
|
5456
|
-
100% { transform: translateY(-90px) scale(0.3); opacity: 0; }
|
|
5457
|
-
}
|
|
5458
|
-
@keyframes save-bar-eq {
|
|
5459
|
-
from { transform: scaleY(0.4); }
|
|
5460
|
-
to { transform: scaleY(1); }
|
|
5461
|
-
}
|
|
5462
|
-
@keyframes save-ambient-pulse {
|
|
5463
|
-
0%, 100% { opacity: 0.3; }
|
|
5464
|
-
50% { opacity: 0.65; }
|
|
5465
|
-
}
|
|
5466
|
-
@keyframes save-shimmer-sweep {
|
|
5467
|
-
from { transform: translateX(-100%); }
|
|
5468
|
-
to { transform: translateX(250%); }
|
|
5469
|
-
}
|
|
5470
|
-
@keyframes save-text-in {
|
|
5471
|
-
from { opacity: 0; transform: translateX(-6px); }
|
|
5472
|
-
to { opacity: 1; transform: translateX(0); }
|
|
5473
|
-
}
|
|
5474
|
-
@keyframes save-node-glow {
|
|
5475
|
-
0%, 100% { box-shadow: 0 0 12px var(--glow-color, #60a5fa55); }
|
|
5476
|
-
50% { box-shadow: 0 0 28px var(--glow-color, #60a5fa88), 0 0 48px var(--glow-color, #60a5fa22); }
|
|
5477
|
-
}
|
|
5478
|
-
@keyframes save-burst-ray {
|
|
5479
|
-
0% { transform: rotate(var(--r, 0deg)) translateX(0); opacity: 1; }
|
|
5480
|
-
100% { transform: rotate(var(--r, 0deg)) translateX(56px); opacity: 0; }
|
|
5481
|
-
}
|
|
5482
|
-
|
|
5483
|
-
.save-drawer-up { animation: save-drawer-up 0.45s cubic-bezier(0.22, 1, 0.36, 1) forwards; }
|
|
5484
|
-
.save-fade-in { animation: save-fade-in 0.25s ease forwards; }
|
|
5485
|
-
.save-fade-up { animation: save-fade-up 0.35s ease forwards; }
|
|
5486
|
-
.save-success-pop { animation: save-success-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }
|
|
5487
|
-
.save-text-in { animation: save-text-in 0.3s ease forwards; }
|
|
5488
|
-
|
|
5489
|
-
.stroke-dash-30 { stroke-dasharray: 30; stroke-dashoffset: 30; }
|
|
5490
|
-
.save-check-draw { animation: save-check-draw 0.4s 0.05s ease forwards; }
|
|
5491
|
-
@keyframes save-check-draw {
|
|
5492
|
-
to { stroke-dashoffset: 0; }
|
|
5493
|
-
}
|
|
5494
|
-
|
|
5495
|
-
/* Countdown drain — 3s linear (dopa-save identical) */
|
|
5496
|
-
.countdown-bar {
|
|
5497
|
-
animation: countdown-drain 3s linear forwards;
|
|
5498
|
-
}
|
|
5499
|
-
@keyframes countdown-drain {
|
|
5500
|
-
from { width: 100%; }
|
|
5501
|
-
to { width: 0%; }
|
|
5502
|
-
}
|
|
5503
4525
|
|
|
5504
4526
|
|
|
5505
4527
|
END_OF_FILE_CONTENT
|
package/assets/vercel.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"rewrites":[{"source":"/(.*)","destination":"/index.html"}],"headers":[{"source":"/assets/(.*)","headers":[{"key":"Cache-Control","value":"public, max-age=31536000, immutable"}]}]}
|
|
1
|
+
{"rewrites":[{"source":"/(.*)","destination":"/index.html"}],"headers":[{"source":"/assets/(.*)","headers":[{"key":"Cache-Control","value":"public, max-age=31536000, immutable"}]}]}
|