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