@jsonpages/cli 3.0.58 → 3.0.60
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 +762 -1491
- package/package.json +1 -1
- package/src/index.js +16 -7
|
@@ -32,22 +32,20 @@ cat << 'END_OF_FILE_CONTENT' > "src/App.tsx"
|
|
|
32
32
|
* Data from getHydratedData (file-backed or draft); assets from public/assets/images.
|
|
33
33
|
* Supports Hybrid Persistence: Local Filesystem (Dev) or Cloud Bridge (Prod).
|
|
34
34
|
*/
|
|
35
|
-
import {
|
|
35
|
+
import { useState, useEffect } from 'react';
|
|
36
36
|
import { JsonPagesEngine } from '@jsonpages/core';
|
|
37
|
-
import type {
|
|
37
|
+
import type { LibraryImageEntry } from '@jsonpages/core';
|
|
38
38
|
import { ComponentRegistry } from '@/lib/ComponentRegistry';
|
|
39
39
|
import { SECTION_SCHEMAS } from '@/lib/schemas';
|
|
40
40
|
import { addSectionConfig } from '@/lib/addSectionConfig';
|
|
41
41
|
import { getHydratedData } from '@/lib/draftStorage';
|
|
42
|
+
import type { JsonPagesConfig, ProjectState } from '@jsonpages/core';
|
|
42
43
|
import type { SiteConfig, ThemeConfig, MenuConfig } from '@/types';
|
|
43
|
-
|
|
44
|
-
import { DEPLOY_STEPS } from '@/lib/deploySteps';
|
|
45
|
-
import { startCloudSaveStream } from '@/lib/cloudSaveStream';
|
|
44
|
+
|
|
46
45
|
import siteData from '@/data/config/site.json';
|
|
47
46
|
import themeData from '@/data/config/theme.json';
|
|
48
47
|
import menuData from '@/data/config/menu.json';
|
|
49
48
|
import { getFilePages } from '@/lib/getFilePages';
|
|
50
|
-
import { DopaDrawer } from '@/components/save-drawer/DopaDrawer';
|
|
51
49
|
|
|
52
50
|
import fontsCss from './fonts.css?inline';
|
|
53
51
|
import tenantCss from './index.css?inline';
|
|
@@ -64,41 +62,13 @@ const filePages = getFilePages();
|
|
|
64
62
|
const fileSiteConfig = siteData as unknown as SiteConfig;
|
|
65
63
|
const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024;
|
|
66
64
|
|
|
67
|
-
interface CloudSaveUiState {
|
|
68
|
-
isOpen: boolean;
|
|
69
|
-
phase: DeployPhase;
|
|
70
|
-
currentStepId: StepId | null;
|
|
71
|
-
doneSteps: StepId[];
|
|
72
|
-
progress: number;
|
|
73
|
-
errorMessage?: string;
|
|
74
|
-
deployUrl?: string;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
65
|
function getInitialData() {
|
|
78
66
|
return getHydratedData(TENANT_ID, filePages, fileSiteConfig);
|
|
79
67
|
}
|
|
80
68
|
|
|
81
|
-
function getInitialCloudSaveUiState(): CloudSaveUiState {
|
|
82
|
-
return {
|
|
83
|
-
isOpen: false,
|
|
84
|
-
phase: 'idle',
|
|
85
|
-
currentStepId: null,
|
|
86
|
-
doneSteps: [],
|
|
87
|
-
progress: 0,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function stepProgress(doneSteps: StepId[]): number {
|
|
92
|
-
return Math.round((doneSteps.length / DEPLOY_STEPS.length) * 100);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
69
|
function App() {
|
|
96
70
|
const [{ pages, siteConfig }] = useState(getInitialData);
|
|
97
71
|
const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
|
|
98
|
-
const [cloudSaveUi, setCloudSaveUi] = useState<CloudSaveUiState>(getInitialCloudSaveUiState);
|
|
99
|
-
const activeCloudSaveController = useRef<AbortController | null>(null);
|
|
100
|
-
const pendingCloudSave = useRef<{ state: ProjectState; slug: string } | null>(null);
|
|
101
|
-
const isCloudMode = Boolean(CLOUD_API_URL && CLOUD_API_KEY);
|
|
102
72
|
|
|
103
73
|
useEffect(() => {
|
|
104
74
|
// In Cloud mode, listing assets might be different or disabled for MVP
|
|
@@ -108,111 +78,11 @@ function App() {
|
|
|
108
78
|
.then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
|
|
109
79
|
.catch(() => setAssetsManifest([]));
|
|
110
80
|
}, []);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}, []);
|
|
117
|
-
|
|
118
|
-
const runCloudSave = useCallback(
|
|
119
|
-
async (
|
|
120
|
-
payload: { state: ProjectState; slug: string },
|
|
121
|
-
rejectOnError: boolean
|
|
122
|
-
): Promise<void> => {
|
|
123
|
-
if (!CLOUD_API_URL || !CLOUD_API_KEY) {
|
|
124
|
-
const noCloudError = new Error('Cloud mode is not configured.');
|
|
125
|
-
if (rejectOnError) throw noCloudError;
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
pendingCloudSave.current = payload;
|
|
130
|
-
activeCloudSaveController.current?.abort();
|
|
131
|
-
const controller = new AbortController();
|
|
132
|
-
activeCloudSaveController.current = controller;
|
|
133
|
-
|
|
134
|
-
setCloudSaveUi({
|
|
135
|
-
isOpen: true,
|
|
136
|
-
phase: 'running',
|
|
137
|
-
currentStepId: null,
|
|
138
|
-
doneSteps: [],
|
|
139
|
-
progress: 0,
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
await startCloudSaveStream({
|
|
144
|
-
apiBaseUrl: CLOUD_API_URL,
|
|
145
|
-
apiKey: CLOUD_API_KEY,
|
|
146
|
-
path: `src/data/pages/${payload.slug}.json`,
|
|
147
|
-
content: payload.state.page,
|
|
148
|
-
message: `Content update for ${payload.slug} via Visual Editor`,
|
|
149
|
-
signal: controller.signal,
|
|
150
|
-
onStep: (event) => {
|
|
151
|
-
setCloudSaveUi((prev) => {
|
|
152
|
-
if (event.status === 'running') {
|
|
153
|
-
return {
|
|
154
|
-
...prev,
|
|
155
|
-
isOpen: true,
|
|
156
|
-
phase: 'running',
|
|
157
|
-
currentStepId: event.id,
|
|
158
|
-
errorMessage: undefined,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (prev.doneSteps.includes(event.id)) {
|
|
163
|
-
return prev;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const nextDone = [...prev.doneSteps, event.id];
|
|
167
|
-
return {
|
|
168
|
-
...prev,
|
|
169
|
-
isOpen: true,
|
|
170
|
-
phase: 'running',
|
|
171
|
-
currentStepId: event.id,
|
|
172
|
-
doneSteps: nextDone,
|
|
173
|
-
progress: stepProgress(nextDone),
|
|
174
|
-
};
|
|
175
|
-
});
|
|
176
|
-
},
|
|
177
|
-
onDone: (event) => {
|
|
178
|
-
const completed = DEPLOY_STEPS.map((step) => step.id);
|
|
179
|
-
setCloudSaveUi({
|
|
180
|
-
isOpen: true,
|
|
181
|
-
phase: 'done',
|
|
182
|
-
currentStepId: 'live',
|
|
183
|
-
doneSteps: completed,
|
|
184
|
-
progress: 100,
|
|
185
|
-
deployUrl: event.deployUrl,
|
|
186
|
-
});
|
|
187
|
-
},
|
|
188
|
-
});
|
|
189
|
-
} catch (error: unknown) {
|
|
190
|
-
const message = error instanceof Error ? error.message : 'Cloud save failed.';
|
|
191
|
-
setCloudSaveUi((prev) => ({
|
|
192
|
-
...prev,
|
|
193
|
-
isOpen: true,
|
|
194
|
-
phase: 'error',
|
|
195
|
-
errorMessage: message,
|
|
196
|
-
}));
|
|
197
|
-
if (rejectOnError) throw new Error(message);
|
|
198
|
-
} finally {
|
|
199
|
-
if (activeCloudSaveController.current === controller) {
|
|
200
|
-
activeCloudSaveController.current = null;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
},
|
|
204
|
-
[]
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
const closeCloudDrawer = useCallback(() => {
|
|
208
|
-
setCloudSaveUi(getInitialCloudSaveUiState());
|
|
209
|
-
}, []);
|
|
210
|
-
|
|
211
|
-
const retryCloudSave = useCallback(() => {
|
|
212
|
-
if (!pendingCloudSave.current) return;
|
|
213
|
-
void runCloudSave(pendingCloudSave.current, false);
|
|
214
|
-
}, [runCloudSave]);
|
|
215
|
-
|
|
81
|
+
|
|
82
|
+
console.log("🔍 DEBUG ENV:", {
|
|
83
|
+
URL: import.meta.env.VITE_JSONPAGES_CLOUD_URL,
|
|
84
|
+
KEY: import.meta.env.VITE_JSONPAGES_API_KEY ? "PRESENT" : "MISSING"
|
|
85
|
+
});
|
|
216
86
|
const config: JsonPagesConfig = {
|
|
217
87
|
tenantId: TENANT_ID,
|
|
218
88
|
registry: ComponentRegistry as JsonPagesConfig['registry'],
|
|
@@ -225,9 +95,30 @@ function App() {
|
|
|
225
95
|
addSection: addSectionConfig,
|
|
226
96
|
persistence: {
|
|
227
97
|
async saveToFile(state: ProjectState, slug: string): Promise<void> {
|
|
98
|
+
|
|
228
99
|
// ☁️ SCENARIO A: CLOUD BRIDGE (Production)
|
|
229
|
-
if (
|
|
230
|
-
|
|
100
|
+
if (CLOUD_API_URL && CLOUD_API_KEY) {
|
|
101
|
+
console.log(`☁️ Saving ${slug} via Cloud Bridge...`);
|
|
102
|
+
|
|
103
|
+
const res = await fetch(`${CLOUD_API_URL}/save`, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: {
|
|
106
|
+
'Content-Type': 'application/json',
|
|
107
|
+
'Authorization': `Bearer ${CLOUD_API_KEY}`
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify({
|
|
110
|
+
// Mapping logical slug to physical path in repo
|
|
111
|
+
path: `src/data/pages/${slug}.json`,
|
|
112
|
+
// We save the page config specifically
|
|
113
|
+
content: state.page,
|
|
114
|
+
message: `Content update for ${slug} via Visual Editor`
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!res.ok) {
|
|
119
|
+
const err = await res.json().catch(() => ({}));
|
|
120
|
+
throw new Error(err.error || `Cloud save failed: ${res.status}`);
|
|
121
|
+
}
|
|
231
122
|
return;
|
|
232
123
|
}
|
|
233
124
|
|
|
@@ -273,35 +164,15 @@ function App() {
|
|
|
273
164
|
},
|
|
274
165
|
};
|
|
275
166
|
|
|
276
|
-
return
|
|
277
|
-
<>
|
|
278
|
-
<JsonPagesEngine config={config} />
|
|
279
|
-
<DopaDrawer
|
|
280
|
-
isOpen={cloudSaveUi.isOpen}
|
|
281
|
-
phase={cloudSaveUi.phase}
|
|
282
|
-
currentStepId={cloudSaveUi.currentStepId}
|
|
283
|
-
doneSteps={cloudSaveUi.doneSteps}
|
|
284
|
-
progress={cloudSaveUi.progress}
|
|
285
|
-
errorMessage={cloudSaveUi.errorMessage}
|
|
286
|
-
deployUrl={cloudSaveUi.deployUrl}
|
|
287
|
-
onClose={closeCloudDrawer}
|
|
288
|
-
onRetry={retryCloudSave}
|
|
289
|
-
/>
|
|
290
|
-
</>
|
|
291
|
-
);
|
|
167
|
+
return <JsonPagesEngine config={config} />;
|
|
292
168
|
}
|
|
293
169
|
|
|
294
170
|
export default App;
|
|
295
171
|
|
|
296
172
|
END_OF_FILE_CONTENT
|
|
297
173
|
# SKIP: src/App.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
298
|
-
echo "Creating src/
|
|
299
|
-
cat << 'END_OF_FILE_CONTENT' > "src/
|
|
300
|
-
/**
|
|
301
|
-
* Thin Entry Point (Tenant).
|
|
302
|
-
* Data from getHydratedData (file-backed or draft); assets from public/assets/images.
|
|
303
|
-
* Supports Hybrid Persistence: Local Filesystem (Dev) or Cloud Bridge (Prod).
|
|
304
|
-
*/
|
|
174
|
+
echo "Creating src/App_.tsx..."
|
|
175
|
+
cat << 'END_OF_FILE_CONTENT' > "src/App_.tsx"
|
|
305
176
|
import { useState, useEffect } from 'react';
|
|
306
177
|
import { JsonPagesEngine } from '@jsonpages/core';
|
|
307
178
|
import type { LibraryImageEntry } from '@jsonpages/core';
|
|
@@ -320,10 +191,6 @@ import { getFilePages } from '@/lib/getFilePages';
|
|
|
320
191
|
import fontsCss from './fonts.css?inline';
|
|
321
192
|
import tenantCss from './index.css?inline';
|
|
322
193
|
|
|
323
|
-
// Cloud Configuration (Injected by Vercel/Netlify Env Vars)
|
|
324
|
-
const CLOUD_API_URL = import.meta.env.VITE_JSONPAGES_CLOUD_URL;
|
|
325
|
-
const CLOUD_API_KEY = import.meta.env.VITE_JSONPAGES_API_KEY;
|
|
326
|
-
|
|
327
194
|
const themeConfig = themeData as unknown as ThemeConfig;
|
|
328
195
|
const menuConfig = menuData as unknown as MenuConfig;
|
|
329
196
|
const TENANT_ID = 'alpha';
|
|
@@ -341,22 +208,16 @@ function App() {
|
|
|
341
208
|
const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
|
|
342
209
|
|
|
343
210
|
useEffect(() => {
|
|
344
|
-
// In Cloud mode, listing assets might be different or disabled for MVP
|
|
345
|
-
// For now, we keep the local fetch which will fail gracefully on Vercel (404)
|
|
346
211
|
fetch('/api/list-assets')
|
|
347
212
|
.then((r) => (r.ok ? r.json() : []))
|
|
348
213
|
.then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
|
|
349
214
|
.catch(() => setAssetsManifest([]));
|
|
350
215
|
}, []);
|
|
351
|
-
|
|
352
|
-
console.log("🔍 DEBUG ENV:", {
|
|
353
|
-
URL: import.meta.env.VITE_JSONPAGES_CLOUD_URL,
|
|
354
|
-
KEY: import.meta.env.VITE_JSONPAGES_API_KEY ? "PRESENT" : "MISSING"
|
|
355
|
-
});
|
|
216
|
+
|
|
356
217
|
const config: JsonPagesConfig = {
|
|
357
218
|
tenantId: TENANT_ID,
|
|
358
219
|
registry: ComponentRegistry as JsonPagesConfig['registry'],
|
|
359
|
-
schemas:
|
|
220
|
+
schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
|
|
360
221
|
pages,
|
|
361
222
|
siteConfig,
|
|
362
223
|
themeConfig,
|
|
@@ -365,41 +226,11 @@ console.log("🔍 DEBUG ENV:", {
|
|
|
365
226
|
addSection: addSectionConfig,
|
|
366
227
|
persistence: {
|
|
367
228
|
async saveToFile(state: ProjectState, slug: string): Promise<void> {
|
|
368
|
-
|
|
369
|
-
// ☁️ SCENARIO A: CLOUD BRIDGE (Production)
|
|
370
|
-
if (CLOUD_API_URL && CLOUD_API_KEY) {
|
|
371
|
-
console.log(`☁️ Saving ${slug} via Cloud Bridge...`);
|
|
372
|
-
|
|
373
|
-
const res = await fetch(`${CLOUD_API_URL}/save`, {
|
|
374
|
-
method: 'POST',
|
|
375
|
-
headers: {
|
|
376
|
-
'Content-Type': 'application/json',
|
|
377
|
-
'Authorization': `Bearer ${CLOUD_API_KEY}`
|
|
378
|
-
},
|
|
379
|
-
body: JSON.stringify({
|
|
380
|
-
// Mapping logical slug to physical path in repo
|
|
381
|
-
path: `src/data/pages/${slug}.json`,
|
|
382
|
-
// We save the page config specifically
|
|
383
|
-
content: state.page,
|
|
384
|
-
message: `Content update for ${slug} via Visual Editor`
|
|
385
|
-
}),
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
if (!res.ok) {
|
|
389
|
-
const err = await res.json().catch(() => ({}));
|
|
390
|
-
throw new Error(err.error || `Cloud save failed: ${res.status}`);
|
|
391
|
-
}
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// 💻 SCENARIO B: LOCAL FILESYSTEM (Development)
|
|
396
|
-
console.log(`💻 Saving ${slug} to Local Filesystem...`);
|
|
397
229
|
const res = await fetch('/api/save-to-file', {
|
|
398
230
|
method: 'POST',
|
|
399
231
|
headers: { 'Content-Type': 'application/json' },
|
|
400
232
|
body: JSON.stringify({ projectState: state, slug }),
|
|
401
233
|
});
|
|
402
|
-
|
|
403
234
|
const body = (await res.json().catch(() => ({}))) as { error?: string };
|
|
404
235
|
if (!res.ok) throw new Error(body.error ?? `Save to file failed: ${res.status}`);
|
|
405
236
|
},
|
|
@@ -408,24 +239,19 @@ console.log("🔍 DEBUG ENV:", {
|
|
|
408
239
|
assetsBaseUrl: '/assets',
|
|
409
240
|
assetsManifest,
|
|
410
241
|
async onAssetUpload(file: File): Promise<string> {
|
|
411
|
-
// Note: Asset upload in Cloud Mode requires the R2 Bridge (Next Step in Roadmap)
|
|
412
|
-
// For now, this works in Local Mode.
|
|
413
242
|
if (!file.type.startsWith('image/')) throw new Error('Invalid file type.');
|
|
414
243
|
if (file.size > MAX_UPLOAD_SIZE_BYTES) throw new Error(`File too large. Max ${MAX_UPLOAD_SIZE_BYTES / 1024 / 1024}MB.`);
|
|
415
|
-
|
|
416
244
|
const base64 = await new Promise<string>((resolve, reject) => {
|
|
417
245
|
const reader = new FileReader();
|
|
418
246
|
reader.onload = () => resolve((reader.result as string).split(',')[1] ?? '');
|
|
419
247
|
reader.onerror = () => reject(reader.error);
|
|
420
248
|
reader.readAsDataURL(file);
|
|
421
249
|
});
|
|
422
|
-
|
|
423
250
|
const res = await fetch('/api/upload-asset', {
|
|
424
251
|
method: 'POST',
|
|
425
252
|
headers: { 'Content-Type': 'application/json' },
|
|
426
253
|
body: JSON.stringify({ filename: file.name, mimeType: file.type || undefined, data: base64 }),
|
|
427
254
|
});
|
|
428
|
-
|
|
429
255
|
const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string };
|
|
430
256
|
if (!res.ok) throw new Error(body.error || `Upload failed: ${res.status}`);
|
|
431
257
|
if (typeof body.url !== 'string') throw new Error('Invalid server response: missing url');
|
|
@@ -439,24 +265,6 @@ console.log("🔍 DEBUG ENV:", {
|
|
|
439
265
|
|
|
440
266
|
export default App;
|
|
441
267
|
|
|
442
|
-
END_OF_FILE_CONTENT
|
|
443
|
-
echo "Creating src/_main.tsx..."
|
|
444
|
-
cat << 'END_OF_FILE_CONTENT' > "src/_main.tsx"
|
|
445
|
-
import '@/types'; // TBP: load type augmentation from capsule-driven types
|
|
446
|
-
import React from 'react';
|
|
447
|
-
import ReactDOM from 'react-dom/client';
|
|
448
|
-
import App from './App';
|
|
449
|
-
// ... resto del file
|
|
450
|
-
|
|
451
|
-
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
452
|
-
<React.StrictMode>
|
|
453
|
-
<App />
|
|
454
|
-
</React.StrictMode>
|
|
455
|
-
);
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
268
|
END_OF_FILE_CONTENT
|
|
461
269
|
mkdir -p "src/components"
|
|
462
270
|
echo "Creating src/components/NotFound.tsx..."
|
|
@@ -3234,1094 +3042,604 @@ export type ProductTriadSettings = z.infer<typeof BaseSectionSettingsSchema>;
|
|
|
3234
3042
|
|
|
3235
3043
|
END_OF_FILE_CONTENT
|
|
3236
3044
|
mkdir -p "src/components/save-drawer"
|
|
3237
|
-
echo "Creating src/components/save-drawer/DeployConnector.tsx..."
|
|
3238
|
-
cat << 'END_OF_FILE_CONTENT' > "src/components/save-drawer/DeployConnector.tsx"
|
|
3239
|
-
import type { StepState } from '@/types/deploy';
|
|
3240
|
-
|
|
3241
|
-
interface DeployConnectorProps {
|
|
3242
|
-
fromState: StepState;
|
|
3243
|
-
toState: StepState;
|
|
3244
|
-
color: string;
|
|
3245
|
-
}
|
|
3246
|
-
|
|
3247
|
-
export function DeployConnector({ fromState, toState, color }: DeployConnectorProps) {
|
|
3248
|
-
const filled = fromState === 'done' && toState === 'done';
|
|
3249
|
-
const filling = fromState === 'done' && toState === 'active';
|
|
3250
|
-
const lit = filled || filling;
|
|
3251
|
-
|
|
3252
|
-
return (
|
|
3253
|
-
<div className="jp-drawer-connector">
|
|
3254
|
-
<div className="jp-drawer-connector-base" />
|
|
3255
|
-
|
|
3256
|
-
<div
|
|
3257
|
-
className="jp-drawer-connector-fill"
|
|
3258
|
-
style={{
|
|
3259
|
-
background: `linear-gradient(90deg, ${color}cc, ${color}66)`,
|
|
3260
|
-
width: filled ? '100%' : filling ? '100%' : '0%',
|
|
3261
|
-
transition: filling ? 'width 2s cubic-bezier(0.4,0,0.2,1)' : 'none',
|
|
3262
|
-
boxShadow: lit ? `0 0 8px ${color}77` : 'none',
|
|
3263
|
-
}}
|
|
3264
|
-
/>
|
|
3265
|
-
|
|
3266
|
-
{filling && (
|
|
3267
|
-
<div
|
|
3268
|
-
className="jp-drawer-connector-orb"
|
|
3269
|
-
style={{
|
|
3270
|
-
background: color,
|
|
3271
|
-
boxShadow: `0 0 14px ${color}, 0 0 28px ${color}88`,
|
|
3272
|
-
animation: 'orb-travel 2s cubic-bezier(0.4,0,0.6,1) forwards',
|
|
3273
|
-
}}
|
|
3274
|
-
/>
|
|
3275
|
-
)}
|
|
3276
|
-
</div>
|
|
3277
|
-
);
|
|
3278
|
-
}
|
|
3279
|
-
|
|
3280
|
-
END_OF_FILE_CONTENT
|
|
3281
3045
|
# SKIP: src/components/save-drawer/DeployConnector.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
3282
3046
|
# SKIP: src/components/save-drawer/DeployConnector.tsx:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3047
|
+
# SKIP: src/components/save-drawer/DeployNode.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
3048
|
+
# SKIP: src/components/save-drawer/DeployNode.tsx:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
3049
|
+
# SKIP: src/components/save-drawer/DopaDrawer.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
3050
|
+
# SKIP: src/components/save-drawer/DopaDrawer.tsx:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
3051
|
+
# SKIP: src/components/save-drawer/Visuals.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
3052
|
+
# SKIP: src/components/save-drawer/Visuals.tsx:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
3053
|
+
# SKIP: src/components/save-drawer/saverStyle.css:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
3054
|
+
# SKIP: src/components/save-drawer/saverStyle.css:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
3055
|
+
mkdir -p "src/components/tiptap"
|
|
3056
|
+
echo "Creating src/components/tiptap/View.tsx..."
|
|
3057
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/tiptap/View.tsx"
|
|
3058
|
+
import React from 'react';
|
|
3059
|
+
import ReactMarkdown from 'react-markdown';
|
|
3060
|
+
import remarkGfm from 'remark-gfm';
|
|
3061
|
+
import rehypeSanitize from 'rehype-sanitize';
|
|
3062
|
+
import { useEditor, EditorContent } from '@tiptap/react';
|
|
3063
|
+
import type { Editor } from '@tiptap/react';
|
|
3064
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
3065
|
+
import Link from '@tiptap/extension-link';
|
|
3066
|
+
import Image from '@tiptap/extension-image';
|
|
3067
|
+
import Underline from '@tiptap/extension-underline';
|
|
3068
|
+
import { Markdown } from 'tiptap-markdown';
|
|
3069
|
+
import {
|
|
3070
|
+
Undo2,
|
|
3071
|
+
Redo2,
|
|
3072
|
+
Heading,
|
|
3073
|
+
List,
|
|
3074
|
+
ListOrdered,
|
|
3075
|
+
Bold,
|
|
3076
|
+
Italic,
|
|
3077
|
+
Strikethrough,
|
|
3078
|
+
Underline as UnderlineIcon,
|
|
3079
|
+
Code2,
|
|
3080
|
+
Quote,
|
|
3081
|
+
SquareCode,
|
|
3082
|
+
Link2,
|
|
3083
|
+
Unlink2,
|
|
3084
|
+
ImagePlus,
|
|
3085
|
+
Eraser,
|
|
3086
|
+
} from 'lucide-react';
|
|
3087
|
+
import { STUDIO_EVENTS, useConfig, useStudio } from '@jsonpages/core';
|
|
3088
|
+
import type { TiptapData, TiptapSettings } from './types';
|
|
3089
|
+
|
|
3090
|
+
const ToolbarButton: React.FC<{
|
|
3091
|
+
active?: boolean;
|
|
3092
|
+
title: string;
|
|
3093
|
+
onClick: () => void;
|
|
3094
|
+
children: React.ReactNode;
|
|
3095
|
+
}> = ({ active = false, title, onClick, children }) => (
|
|
3096
|
+
<button
|
|
3097
|
+
type="button"
|
|
3098
|
+
title={title}
|
|
3099
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
3100
|
+
onClick={onClick}
|
|
3101
|
+
className={[
|
|
3102
|
+
'inline-flex h-7 min-w-7 items-center justify-center rounded-md border px-2 text-xs transition-colors',
|
|
3103
|
+
active
|
|
3104
|
+
? 'bg-blue-600/25 border-blue-400/60 text-blue-100'
|
|
3105
|
+
: 'bg-zinc-900 border-zinc-700 text-zinc-200 hover:bg-zinc-800',
|
|
3106
|
+
].join(' ')}
|
|
3107
|
+
>
|
|
3108
|
+
{children}
|
|
3109
|
+
</button>
|
|
3110
|
+
);
|
|
3292
3111
|
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3112
|
+
const ToolbarSeparator: React.FC = () => <span className="mx-1 h-5 w-px bg-zinc-700" aria-hidden />;
|
|
3113
|
+
|
|
3114
|
+
const UploadableImage = Image.extend({
|
|
3115
|
+
addAttributes() {
|
|
3116
|
+
return {
|
|
3117
|
+
...this.parent?.(),
|
|
3118
|
+
uploadId: {
|
|
3119
|
+
default: null,
|
|
3120
|
+
parseHTML: (element: HTMLElement) => element.getAttribute('data-upload-id'),
|
|
3121
|
+
renderHTML: (attributes: Record<string, unknown>) =>
|
|
3122
|
+
attributes.uploadId ? { 'data-upload-id': String(attributes.uploadId) } : {},
|
|
3123
|
+
},
|
|
3124
|
+
uploading: {
|
|
3125
|
+
default: false,
|
|
3126
|
+
parseHTML: (element: HTMLElement) => element.getAttribute('data-uploading') === 'true',
|
|
3127
|
+
renderHTML: (attributes: Record<string, unknown>) =>
|
|
3128
|
+
attributes.uploading ? { 'data-uploading': 'true' } : {},
|
|
3129
|
+
},
|
|
3130
|
+
uploadError: {
|
|
3131
|
+
default: false,
|
|
3132
|
+
parseHTML: (element: HTMLElement) => element.getAttribute('data-upload-error') === 'true',
|
|
3133
|
+
renderHTML: (attributes: Record<string, unknown>) =>
|
|
3134
|
+
attributes.uploadError ? { 'data-upload-error': 'true' } : {},
|
|
3135
|
+
},
|
|
3136
|
+
awaitingUpload: {
|
|
3137
|
+
default: false,
|
|
3138
|
+
parseHTML: (element: HTMLElement) => element.getAttribute('data-awaiting-upload') === 'true',
|
|
3139
|
+
renderHTML: (attributes: Record<string, unknown>) =>
|
|
3140
|
+
attributes.awaitingUpload ? { 'data-awaiting-upload': 'true' } : {},
|
|
3141
|
+
},
|
|
3142
|
+
};
|
|
3143
|
+
},
|
|
3144
|
+
});
|
|
3297
3145
|
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
? `0 0 20px ${step.color}55, 0 0 40px ${step.color}22`
|
|
3309
|
-
: isActive
|
|
3310
|
-
? `0 0 14px ${step.color}33`
|
|
3311
|
-
: undefined,
|
|
3312
|
-
animation: isActive ? 'node-glow 2s ease infinite' : undefined,
|
|
3313
|
-
['--glow-color' as string]: step.color,
|
|
3314
|
-
} as CSSProperties
|
|
3315
|
-
}
|
|
3316
|
-
>
|
|
3317
|
-
{isDone && (
|
|
3318
|
-
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" aria-label="Done">
|
|
3319
|
-
<path
|
|
3320
|
-
className="stroke-dash-30 animate-check-draw"
|
|
3321
|
-
d="M5 13l4 4L19 7"
|
|
3322
|
-
stroke="#0a0f1a"
|
|
3323
|
-
strokeWidth="2.5"
|
|
3324
|
-
strokeLinecap="round"
|
|
3325
|
-
strokeLinejoin="round"
|
|
3326
|
-
/>
|
|
3327
|
-
</svg>
|
|
3328
|
-
)}
|
|
3146
|
+
const getEditorMarkdown = (editor: Editor | null | undefined): string => {
|
|
3147
|
+
const storage = editor?.storage as
|
|
3148
|
+
| {
|
|
3149
|
+
markdown?: {
|
|
3150
|
+
getMarkdown?: () => string;
|
|
3151
|
+
};
|
|
3152
|
+
}
|
|
3153
|
+
| undefined;
|
|
3154
|
+
return storage?.markdown?.getMarkdown?.() ?? '';
|
|
3155
|
+
};
|
|
3329
3156
|
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3157
|
+
const UPLOAD_PLACEHOLDER_SRC =
|
|
3158
|
+
"data:image/svg+xml;utf8," +
|
|
3159
|
+
encodeURIComponent(
|
|
3160
|
+
`<svg xmlns='http://www.w3.org/2000/svg' width='1200' height='420' viewBox='0 0 1200 420'>
|
|
3161
|
+
<rect width='1200' height='420' fill='#090B14' stroke='#3F3F46' stroke-width='3' stroke-dasharray='10 10' rx='12' />
|
|
3162
|
+
<g font-family='Inter,Arial,sans-serif' text-anchor='middle' fill='#A1A1AA'>
|
|
3163
|
+
<text x='600' y='190' font-size='30' font-weight='700'>Uploading image...</text>
|
|
3164
|
+
<text x='600' y='235' font-size='22'>Please wait while we process your file</text>
|
|
3165
|
+
</g>
|
|
3166
|
+
</svg>`
|
|
3167
|
+
);
|
|
3339
3168
|
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3169
|
+
const UPLOAD_PICKER_PLACEHOLDER_SRC =
|
|
3170
|
+
"data:image/svg+xml;utf8," +
|
|
3171
|
+
encodeURIComponent(
|
|
3172
|
+
`<svg xmlns='http://www.w3.org/2000/svg' width='1200' height='420' viewBox='0 0 1200 420'>
|
|
3173
|
+
<rect width='1200' height='420' fill='#090B14' stroke='#3F3F46' stroke-width='3' stroke-dasharray='10 10' rx='12' />
|
|
3174
|
+
<g font-family='Inter,Arial,sans-serif' text-anchor='middle'>
|
|
3175
|
+
<text x='600' y='210' font-size='34' font-weight='700' fill='#E4E4E7'>Click to upload or drag and drop</text>
|
|
3176
|
+
<text x='600' y='255' font-size='24' font-weight='500' fill='#A1A1AA'>Maximum 3 files, 5MB each.</text>
|
|
3177
|
+
</g>
|
|
3178
|
+
</svg>`
|
|
3179
|
+
);
|
|
3345
3180
|
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3181
|
+
const updateImageByUploadId = (
|
|
3182
|
+
editor: Editor,
|
|
3183
|
+
uploadId: string,
|
|
3184
|
+
patch: Record<string, unknown>
|
|
3185
|
+
): boolean => {
|
|
3186
|
+
let targetPos: number | null = null;
|
|
3187
|
+
editor.state.doc.descendants((node: { type: { name: string }; attrs?: Record<string, unknown> }, pos: number) => {
|
|
3188
|
+
if (node.type.name === 'image' && node.attrs?.uploadId === uploadId) {
|
|
3189
|
+
targetPos = pos;
|
|
3190
|
+
return false;
|
|
3191
|
+
}
|
|
3192
|
+
return true;
|
|
3193
|
+
});
|
|
3194
|
+
if (targetPos == null) return false;
|
|
3195
|
+
const current = editor.state.doc.nodeAt(targetPos);
|
|
3196
|
+
if (!current) return false;
|
|
3197
|
+
const attrs = { ...current.attrs, ...patch };
|
|
3198
|
+
editor.view.dispatch(editor.state.tr.setNodeMarkup(targetPos, undefined, attrs));
|
|
3199
|
+
return true;
|
|
3200
|
+
};
|
|
3357
3201
|
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3202
|
+
const StudioTiptapEditor: React.FC<{ data: TiptapData }> = ({ data }) => {
|
|
3203
|
+
const { assets } = useConfig();
|
|
3204
|
+
const rootRef = React.useRef<HTMLElement | null>(null);
|
|
3205
|
+
const editorHostRef = React.useRef<HTMLDivElement | null>(null);
|
|
3206
|
+
const editorInstanceRef = React.useRef<Editor | null>(null);
|
|
3207
|
+
const fileInputRef = React.useRef<HTMLInputElement | null>(null);
|
|
3208
|
+
const pendingUploadsRef = React.useRef<Map<string, Promise<void>>>(new Map());
|
|
3209
|
+
const pendingPickerUploadIdRef = React.useRef<string | null>(null);
|
|
3210
|
+
const latestMarkdownRef = React.useRef<string>(data.content ?? '');
|
|
3211
|
+
const latestEmittedMarkdownRef = React.useRef<string>(data.content ?? '');
|
|
3212
|
+
|
|
3213
|
+
const resolveSectionId = React.useCallback(() => {
|
|
3214
|
+
const sectionEl =
|
|
3215
|
+
rootRef.current ??
|
|
3216
|
+
(editorHostRef.current?.closest('[data-section-id]') as HTMLElement | null);
|
|
3217
|
+
rootRef.current = sectionEl;
|
|
3218
|
+
return sectionEl?.getAttribute('data-section-id') ?? null;
|
|
3219
|
+
}, []);
|
|
3220
|
+
|
|
3221
|
+
const emitInlineUpdate = React.useCallback((markdown: string) => {
|
|
3222
|
+
latestMarkdownRef.current = markdown;
|
|
3223
|
+
const sectionId = resolveSectionId();
|
|
3224
|
+
if (!sectionId) return;
|
|
3225
|
+
window.parent.postMessage(
|
|
3226
|
+
{
|
|
3227
|
+
type: STUDIO_EVENTS.INLINE_FIELD_UPDATE,
|
|
3228
|
+
sectionId,
|
|
3229
|
+
fieldKey: 'content',
|
|
3230
|
+
value: markdown,
|
|
3231
|
+
},
|
|
3232
|
+
window.location.origin
|
|
3233
|
+
);
|
|
3234
|
+
latestEmittedMarkdownRef.current = markdown;
|
|
3235
|
+
}, [resolveSectionId]);
|
|
3236
|
+
|
|
3237
|
+
const insertUploadPlaceholder = React.useCallback(
|
|
3238
|
+
(uploadId: string, src: string, awaitingUpload: boolean) => {
|
|
3239
|
+
const editor = editorInstanceRef.current;
|
|
3240
|
+
if (!editor) return;
|
|
3241
|
+
editor
|
|
3242
|
+
.chain()
|
|
3243
|
+
.focus()
|
|
3244
|
+
.setImage({
|
|
3245
|
+
src,
|
|
3246
|
+
alt: 'upload-placeholder',
|
|
3247
|
+
title: awaitingUpload ? 'Click to upload' : 'Uploading...',
|
|
3248
|
+
uploadId,
|
|
3249
|
+
uploading: !awaitingUpload,
|
|
3250
|
+
awaitingUpload,
|
|
3251
|
+
uploadError: false,
|
|
3252
|
+
} as any)
|
|
3253
|
+
.run();
|
|
3254
|
+
emitInlineUpdate(getEditorMarkdown(editor));
|
|
3255
|
+
},
|
|
3256
|
+
[emitInlineUpdate]
|
|
3365
3257
|
);
|
|
3366
|
-
}
|
|
3367
3258
|
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3259
|
+
const startUploadForId = React.useCallback(
|
|
3260
|
+
async (uploadId: string, file: File) => {
|
|
3261
|
+
const uploadAsset = assets?.onAssetUpload;
|
|
3262
|
+
if (!uploadAsset) return;
|
|
3263
|
+
const editor = editorInstanceRef.current;
|
|
3264
|
+
if (!editor) return;
|
|
3265
|
+
updateImageByUploadId(editor, uploadId, {
|
|
3266
|
+
src: UPLOAD_PLACEHOLDER_SRC,
|
|
3267
|
+
alt: file.name,
|
|
3268
|
+
title: file.name,
|
|
3269
|
+
uploading: true,
|
|
3270
|
+
awaitingUpload: false,
|
|
3271
|
+
uploadError: false,
|
|
3272
|
+
});
|
|
3273
|
+
const task = (async () => {
|
|
3274
|
+
try {
|
|
3275
|
+
const url = await uploadAsset(file);
|
|
3276
|
+
const currentEditor = editorInstanceRef.current;
|
|
3277
|
+
if (currentEditor) {
|
|
3278
|
+
updateImageByUploadId(currentEditor, uploadId, {
|
|
3279
|
+
src: url,
|
|
3280
|
+
alt: file.name,
|
|
3281
|
+
title: file.name,
|
|
3282
|
+
uploadId: null,
|
|
3283
|
+
uploading: false,
|
|
3284
|
+
awaitingUpload: false,
|
|
3285
|
+
uploadError: false,
|
|
3286
|
+
});
|
|
3287
|
+
emitInlineUpdate(getEditorMarkdown(currentEditor));
|
|
3288
|
+
}
|
|
3289
|
+
} catch (err) {
|
|
3290
|
+
console.error('[tiptap] upload failed', err);
|
|
3291
|
+
const currentEditor = editorInstanceRef.current;
|
|
3292
|
+
if (currentEditor) {
|
|
3293
|
+
updateImageByUploadId(currentEditor, uploadId, {
|
|
3294
|
+
uploading: false,
|
|
3295
|
+
awaitingUpload: false,
|
|
3296
|
+
uploadError: true,
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
3299
|
+
} finally {
|
|
3300
|
+
pendingUploadsRef.current.delete(uploadId);
|
|
3301
|
+
}
|
|
3302
|
+
})();
|
|
3303
|
+
pendingUploadsRef.current.set(uploadId, task);
|
|
3304
|
+
await task;
|
|
3305
|
+
},
|
|
3306
|
+
[assets, emitInlineUpdate]
|
|
3307
|
+
);
|
|
3394
3308
|
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
onClose,
|
|
3404
|
-
onRetry,
|
|
3405
|
-
}: DopaDrawerProps) {
|
|
3406
|
-
const [shadowMount, setShadowMount] = useState<HTMLElement | null>(null);
|
|
3407
|
-
const [burst, setBurst] = useState(false);
|
|
3408
|
-
const [countdown, setCountdown] = useState(3);
|
|
3409
|
-
|
|
3410
|
-
const isRunning = phase === 'running';
|
|
3411
|
-
const isDone = phase === 'done';
|
|
3412
|
-
const isError = phase === 'error';
|
|
3309
|
+
const uploadImage = React.useCallback(
|
|
3310
|
+
async (file: File) => {
|
|
3311
|
+
const uploadId = crypto.randomUUID();
|
|
3312
|
+
insertUploadPlaceholder(uploadId, UPLOAD_PLACEHOLDER_SRC, false);
|
|
3313
|
+
await startUploadForId(uploadId, file);
|
|
3314
|
+
},
|
|
3315
|
+
[insertUploadPlaceholder, startUploadForId]
|
|
3316
|
+
);
|
|
3413
3317
|
|
|
3414
|
-
|
|
3415
|
-
const
|
|
3416
|
-
|
|
3318
|
+
const setFocusLock = React.useCallback((enabled: boolean) => {
|
|
3319
|
+
const sectionEl = rootRef.current;
|
|
3320
|
+
if (!sectionEl) return;
|
|
3321
|
+
if (enabled) sectionEl.classList.add('jp-editorial-focus');
|
|
3322
|
+
else sectionEl.classList.remove('jp-editorial-focus');
|
|
3323
|
+
}, []);
|
|
3417
3324
|
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3325
|
+
const editor = useEditor(
|
|
3326
|
+
{
|
|
3327
|
+
extensions: [
|
|
3328
|
+
StarterKit,
|
|
3329
|
+
Link.configure({ openOnClick: false, autolink: true }),
|
|
3330
|
+
Underline,
|
|
3331
|
+
UploadableImage,
|
|
3332
|
+
Markdown.configure({ html: false }),
|
|
3333
|
+
],
|
|
3334
|
+
content: data.content ?? '',
|
|
3335
|
+
autofocus: false,
|
|
3336
|
+
editorProps: {
|
|
3337
|
+
attributes: {
|
|
3338
|
+
class:
|
|
3339
|
+
'prose prose-invert max-w-none min-h-[220px] rounded-lg border border-zinc-800 bg-zinc-950/60 p-4 text-zinc-100 outline-none',
|
|
3340
|
+
},
|
|
3341
|
+
handleDrop: (_view: unknown, event: DragEvent) => {
|
|
3342
|
+
const file = event.dataTransfer?.files?.[0];
|
|
3343
|
+
if (!file || !file.type.startsWith('image/') || !assets?.onAssetUpload) return false;
|
|
3344
|
+
event.preventDefault();
|
|
3345
|
+
void (async () => {
|
|
3346
|
+
try {
|
|
3347
|
+
await uploadImage(file);
|
|
3348
|
+
} catch (err) {
|
|
3349
|
+
console.error('[tiptap] upload failed on drop', err);
|
|
3350
|
+
}
|
|
3351
|
+
})();
|
|
3352
|
+
return true;
|
|
3353
|
+
},
|
|
3354
|
+
handlePaste: (_view: unknown, event: ClipboardEvent) => {
|
|
3355
|
+
const file = Array.from(event.clipboardData?.files ?? []).find((f: File) =>
|
|
3356
|
+
f.type.startsWith('image/')
|
|
3357
|
+
);
|
|
3358
|
+
if (!file || !assets?.onAssetUpload) return false;
|
|
3359
|
+
event.preventDefault();
|
|
3360
|
+
void (async () => {
|
|
3361
|
+
try {
|
|
3362
|
+
await uploadImage(file);
|
|
3363
|
+
} catch (err) {
|
|
3364
|
+
console.error('[tiptap] upload failed on paste', err);
|
|
3365
|
+
}
|
|
3366
|
+
})();
|
|
3367
|
+
return true;
|
|
3368
|
+
},
|
|
3369
|
+
handleClickOn: (_view: unknown, _pos: number, node: { type: { name: string }; attrs?: Record<string, unknown> }) => {
|
|
3370
|
+
if (node.type.name !== 'image') return false;
|
|
3371
|
+
if (node.attrs?.awaitingUpload !== true) return false;
|
|
3372
|
+
const uploadId = typeof node.attrs?.uploadId === 'string' ? node.attrs.uploadId : null;
|
|
3373
|
+
if (!uploadId) return false;
|
|
3374
|
+
pendingPickerUploadIdRef.current = uploadId;
|
|
3375
|
+
fileInputRef.current?.click();
|
|
3376
|
+
return true;
|
|
3377
|
+
},
|
|
3378
|
+
},
|
|
3379
|
+
onUpdate: ({ editor: currentEditor }: { editor: Editor }) => {
|
|
3380
|
+
const markdown = getEditorMarkdown(currentEditor);
|
|
3381
|
+
emitInlineUpdate(markdown);
|
|
3382
|
+
},
|
|
3383
|
+
onFocus: () => {
|
|
3384
|
+
setFocusLock(true);
|
|
3385
|
+
},
|
|
3386
|
+
onBlur: ({ editor: currentEditor }: { editor: Editor }) => {
|
|
3387
|
+
const markdown = getEditorMarkdown(currentEditor);
|
|
3388
|
+
if (markdown !== latestEmittedMarkdownRef.current) {
|
|
3389
|
+
emitInlineUpdate(markdown);
|
|
3390
|
+
}
|
|
3391
|
+
setFocusLock(false);
|
|
3392
|
+
},
|
|
3393
|
+
}
|
|
3394
|
+
);
|
|
3421
3395
|
|
|
3422
|
-
|
|
3423
|
-
|
|
3396
|
+
React.useEffect(() => {
|
|
3397
|
+
const sectionEl = editorHostRef.current?.closest('[data-section-id]') as HTMLElement | null;
|
|
3398
|
+
rootRef.current = sectionEl;
|
|
3399
|
+
}, []);
|
|
3424
3400
|
|
|
3425
|
-
|
|
3426
|
-
|
|
3401
|
+
React.useEffect(() => {
|
|
3402
|
+
editorInstanceRef.current = editor ?? null;
|
|
3403
|
+
}, [editor]);
|
|
3427
3404
|
|
|
3405
|
+
React.useEffect(() => {
|
|
3406
|
+
const handleInlineFlushRequest = () => {
|
|
3407
|
+
void (async () => {
|
|
3408
|
+
const pending = Array.from(pendingUploadsRef.current.values());
|
|
3409
|
+
if (pending.length > 0) {
|
|
3410
|
+
await Promise.allSettled(pending);
|
|
3411
|
+
}
|
|
3412
|
+
const markdown = getEditorMarkdown(editorInstanceRef.current);
|
|
3413
|
+
emitInlineUpdate(markdown);
|
|
3414
|
+
})();
|
|
3415
|
+
};
|
|
3416
|
+
window.addEventListener(STUDIO_EVENTS.REQUEST_INLINE_FLUSH, handleInlineFlushRequest as EventListener);
|
|
3428
3417
|
return () => {
|
|
3429
|
-
|
|
3430
|
-
host.remove();
|
|
3418
|
+
window.removeEventListener(STUDIO_EVENTS.REQUEST_INLINE_FLUSH, handleInlineFlushRequest as EventListener);
|
|
3431
3419
|
};
|
|
3432
|
-
}, []);
|
|
3420
|
+
}, [emitInlineUpdate]);
|
|
3421
|
+
|
|
3422
|
+
React.useEffect(() => {
|
|
3423
|
+
if (!editor) return;
|
|
3424
|
+
const nextMarkdown = data.content ?? '';
|
|
3425
|
+
if (nextMarkdown === latestMarkdownRef.current) return;
|
|
3426
|
+
editor.commands.setContent(nextMarkdown);
|
|
3427
|
+
latestMarkdownRef.current = nextMarkdown;
|
|
3428
|
+
}, [data.content, editor]);
|
|
3429
|
+
|
|
3430
|
+
React.useEffect(
|
|
3431
|
+
() => () => {
|
|
3432
|
+
const editorInstance = editorInstanceRef.current;
|
|
3433
|
+
const markdown = getEditorMarkdown(editorInstance);
|
|
3434
|
+
if (markdown !== latestEmittedMarkdownRef.current) {
|
|
3435
|
+
emitInlineUpdate(markdown);
|
|
3436
|
+
}
|
|
3437
|
+
setFocusLock(false);
|
|
3438
|
+
},
|
|
3439
|
+
[emitInlineUpdate, setFocusLock]
|
|
3440
|
+
);
|
|
3433
3441
|
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3442
|
+
const setParagraph = () => editor?.chain().focus().setParagraph().run();
|
|
3443
|
+
const setHeading = (level: 1 | 2 | 3) => editor?.chain().focus().toggleHeading({ level }).run();
|
|
3444
|
+
const setLink = () => {
|
|
3445
|
+
if (!editor) return;
|
|
3446
|
+
const prev = editor.getAttributes('link').href as string | undefined;
|
|
3447
|
+
const href = window.prompt('URL', prev ?? 'https://');
|
|
3448
|
+
if (href == null) return;
|
|
3449
|
+
if (href.trim() === '') {
|
|
3450
|
+
editor.chain().focus().unsetLink().run();
|
|
3438
3451
|
return;
|
|
3439
3452
|
}
|
|
3440
|
-
|
|
3441
|
-
}
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3453
|
+
editor.chain().focus().extendMarkRange('link').setLink({ href }).run();
|
|
3454
|
+
};
|
|
3455
|
+
const clearFormatting = () => {
|
|
3456
|
+
editor?.chain().focus().unsetAllMarks().clearNodes().run();
|
|
3457
|
+
};
|
|
3458
|
+
const onPickImageClick = () => {
|
|
3459
|
+
if (pendingPickerUploadIdRef.current) return;
|
|
3460
|
+
const uploadId = crypto.randomUUID();
|
|
3461
|
+
pendingPickerUploadIdRef.current = uploadId;
|
|
3462
|
+
insertUploadPlaceholder(uploadId, UPLOAD_PICKER_PLACEHOLDER_SRC, true);
|
|
3463
|
+
};
|
|
3464
|
+
const onFileSelected = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
3465
|
+
const file = event.target.files?.[0];
|
|
3466
|
+
const pendingUploadId = pendingPickerUploadIdRef.current;
|
|
3467
|
+
if (!file || !file.type.startsWith('image/') || !assets?.onAssetUpload) {
|
|
3468
|
+
event.target.value = '';
|
|
3469
|
+
return;
|
|
3470
|
+
}
|
|
3471
|
+
void (async () => {
|
|
3472
|
+
try {
|
|
3473
|
+
if (pendingUploadId) {
|
|
3474
|
+
await startUploadForId(pendingUploadId, file);
|
|
3475
|
+
pendingPickerUploadIdRef.current = null;
|
|
3476
|
+
} else {
|
|
3477
|
+
await uploadImage(file);
|
|
3452
3478
|
}
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
const currentStep = useMemo(
|
|
3460
|
-
() => DEPLOY_STEPS.find((step) => step.id === currentStepId) ?? null,
|
|
3461
|
-
[currentStepId]
|
|
3462
|
-
);
|
|
3463
|
-
|
|
3464
|
-
const activeColor = isDone ? '#34d399' : isError ? '#f87171' : (currentStep?.color ?? '#60a5fa');
|
|
3465
|
-
const particleCount = isDone ? 40 : doneSteps.length === 3 ? 28 : doneSteps.length === 2 ? 16 : doneSteps.length === 1 ? 8 : 4;
|
|
3466
|
-
|
|
3467
|
-
const stepState = (index: number): StepState => {
|
|
3468
|
-
const step = DEPLOY_STEPS[index];
|
|
3469
|
-
if (doneSteps.includes(step.id)) return 'done';
|
|
3470
|
-
if (phase === 'running' && currentStepId === step.id) return 'active';
|
|
3471
|
-
return 'pending';
|
|
3479
|
+
} catch (err) {
|
|
3480
|
+
console.error('[tiptap] upload failed from picker', err);
|
|
3481
|
+
} finally {
|
|
3482
|
+
event.target.value = '';
|
|
3483
|
+
}
|
|
3484
|
+
})();
|
|
3472
3485
|
};
|
|
3486
|
+
const isActive = (name: string, attrs?: Record<string, unknown>) => editor?.isActive(name, attrs) ?? false;
|
|
3473
3487
|
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
aria-hidden
|
|
3482
|
-
/>
|
|
3483
|
-
|
|
3484
|
-
<div
|
|
3485
|
-
role="status"
|
|
3486
|
-
aria-live="polite"
|
|
3487
|
-
aria-label={isDone ? 'Deploy completed' : isError ? 'Deploy failed' : 'Deploying'}
|
|
3488
|
-
className="jp-drawer-shell animate-drawer-up"
|
|
3489
|
-
style={{ bottom: 'max(2.25rem, env(safe-area-inset-bottom))' }}
|
|
3490
|
-
>
|
|
3488
|
+
return (
|
|
3489
|
+
<div
|
|
3490
|
+
ref={editorHostRef}
|
|
3491
|
+
data-jp-field="content"
|
|
3492
|
+
className="space-y-3"
|
|
3493
|
+
>
|
|
3494
|
+
{editor && (
|
|
3491
3495
|
<div
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
backgroundColor: 'hsl(222 18% 7%)',
|
|
3495
|
-
boxShadow: `0 0 0 1px rgba(255,255,255,0.04), 0 -20px 60px rgba(0,0,0,0.6), 0 0 80px ${activeColor}0d`,
|
|
3496
|
-
transition: 'box-shadow 1.2s ease',
|
|
3497
|
-
}}
|
|
3496
|
+
data-jp-ignore-select="true"
|
|
3497
|
+
className="sticky top-2 z-[65] rounded-md border border-zinc-700 bg-zinc-950/95 p-2 shadow-lg backdrop-blur"
|
|
3498
3498
|
>
|
|
3499
|
-
<div
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
<
|
|
3526
|
-
<
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
<
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
</div>
|
|
3569
|
-
</div>
|
|
3570
|
-
) : (
|
|
3571
|
-
<ElapsedTimer running={isRunning} />
|
|
3572
|
-
)}
|
|
3573
|
-
</div>
|
|
3574
|
-
</div>
|
|
3575
|
-
|
|
3576
|
-
<div className="jp-drawer-track-row">
|
|
3577
|
-
{DEPLOY_STEPS.map((step, i) => (
|
|
3578
|
-
<div key={step.id} style={{ display: 'flex', alignItems: 'center', flex: i < DEPLOY_STEPS.length - 1 ? 1 : 'none' }}>
|
|
3579
|
-
<DeployNode step={step} state={stepState(i)} />
|
|
3580
|
-
{i < DEPLOY_STEPS.length - 1 && (
|
|
3581
|
-
<DeployConnector fromState={stepState(i)} toState={stepState(i + 1)} color={DEPLOY_STEPS[i + 1].color} />
|
|
3582
|
-
)}
|
|
3583
|
-
</div>
|
|
3584
|
-
))}
|
|
3585
|
-
</div>
|
|
3586
|
-
|
|
3587
|
-
<div className="jp-drawer-bars-wrap">
|
|
3588
|
-
<BuildBars active={stepState(2) === 'active'} />
|
|
3589
|
-
</div>
|
|
3590
|
-
|
|
3591
|
-
<div className="jp-drawer-separator" />
|
|
3592
|
-
|
|
3593
|
-
<div className="jp-drawer-footer">
|
|
3594
|
-
<div className="jp-drawer-progress">
|
|
3595
|
-
<div
|
|
3596
|
-
className="jp-drawer-progress-indicator"
|
|
3597
|
-
style={{
|
|
3598
|
-
width: `${Math.max(0, Math.min(100, progress))}%`,
|
|
3599
|
-
background: `linear-gradient(90deg, ${DEPLOY_STEPS[0].color}, ${activeColor})`,
|
|
3600
|
-
}}
|
|
3601
|
-
/>
|
|
3602
|
-
</div>
|
|
3603
|
-
|
|
3604
|
-
<div className="jp-drawer-cta">
|
|
3605
|
-
{isDone && (
|
|
3606
|
-
<div className="jp-drawer-btn-row animate-fade-up">
|
|
3607
|
-
<button type="button" className="jp-drawer-btn jp-drawer-btn-secondary" onClick={onClose}>
|
|
3608
|
-
Chiudi
|
|
3609
|
-
</button>
|
|
3610
|
-
<button
|
|
3611
|
-
type="button"
|
|
3612
|
-
className="jp-drawer-btn jp-drawer-btn-emerald"
|
|
3613
|
-
onClick={() => {
|
|
3614
|
-
if (deployUrl) window.open(deployUrl, '_blank', 'noopener,noreferrer');
|
|
3615
|
-
}}
|
|
3616
|
-
disabled={!deployUrl}
|
|
3617
|
-
>
|
|
3618
|
-
<span aria-hidden>↗</span> Open site
|
|
3619
|
-
</button>
|
|
3620
|
-
</div>
|
|
3621
|
-
)}
|
|
3622
|
-
|
|
3623
|
-
{isError && (
|
|
3624
|
-
<div className="jp-drawer-btn-row animate-fade-up">
|
|
3625
|
-
<button type="button" className="jp-drawer-btn jp-drawer-btn-ghost" onClick={onClose}>
|
|
3626
|
-
Annulla
|
|
3627
|
-
</button>
|
|
3628
|
-
<button type="button" className="jp-drawer-btn jp-drawer-btn-destructive" onClick={onRetry}>
|
|
3629
|
-
Retry
|
|
3630
|
-
</button>
|
|
3631
|
-
</div>
|
|
3632
|
-
)}
|
|
3633
|
-
|
|
3634
|
-
{isRunning && (
|
|
3635
|
-
<span className="jp-drawer-running-step" aria-hidden>
|
|
3636
|
-
{doneSteps.length + 1} / {DEPLOY_STEPS.length}
|
|
3637
|
-
</span>
|
|
3638
|
-
)}
|
|
3639
|
-
</div>
|
|
3640
|
-
</div>
|
|
3499
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
3500
|
+
<ToolbarButton title="Undo" onClick={() => editor.chain().focus().undo().run()}>
|
|
3501
|
+
<Undo2 size={14} />
|
|
3502
|
+
</ToolbarButton>
|
|
3503
|
+
<ToolbarButton title="Redo" onClick={() => editor.chain().focus().redo().run()}>
|
|
3504
|
+
<Redo2 size={14} />
|
|
3505
|
+
</ToolbarButton>
|
|
3506
|
+
<ToolbarSeparator />
|
|
3507
|
+
|
|
3508
|
+
<ToolbarButton active={isActive('paragraph')} title="Paragraph" onClick={setParagraph}>
|
|
3509
|
+
P
|
|
3510
|
+
</ToolbarButton>
|
|
3511
|
+
<ToolbarButton active={isActive('heading', { level: 1 })} title="Heading 1" onClick={() => setHeading(1)}>
|
|
3512
|
+
H1
|
|
3513
|
+
</ToolbarButton>
|
|
3514
|
+
<ToolbarButton active={isActive('heading', { level: 2 })} title="Heading 2" onClick={() => setHeading(2)}>
|
|
3515
|
+
H2
|
|
3516
|
+
</ToolbarButton>
|
|
3517
|
+
<ToolbarButton active={isActive('heading', { level: 3 })} title="Heading 3" onClick={() => setHeading(3)}>
|
|
3518
|
+
H3
|
|
3519
|
+
</ToolbarButton>
|
|
3520
|
+
<ToolbarButton title="Headings" onClick={() => setHeading(2)}>
|
|
3521
|
+
<Heading size={14} />
|
|
3522
|
+
</ToolbarButton>
|
|
3523
|
+
<ToolbarSeparator />
|
|
3524
|
+
|
|
3525
|
+
<ToolbarButton active={isActive('bold')} title="Bold" onClick={() => editor.chain().focus().toggleBold().run()}>
|
|
3526
|
+
<Bold size={14} />
|
|
3527
|
+
</ToolbarButton>
|
|
3528
|
+
<ToolbarButton active={isActive('italic')} title="Italic" onClick={() => editor.chain().focus().toggleItalic().run()}>
|
|
3529
|
+
<Italic size={14} />
|
|
3530
|
+
</ToolbarButton>
|
|
3531
|
+
<ToolbarButton active={isActive('underline')} title="Underline" onClick={() => editor.chain().focus().toggleUnderline().run()}>
|
|
3532
|
+
<UnderlineIcon size={14} />
|
|
3533
|
+
</ToolbarButton>
|
|
3534
|
+
<ToolbarButton active={isActive('strike')} title="Strike" onClick={() => editor.chain().focus().toggleStrike().run()}>
|
|
3535
|
+
<Strikethrough size={14} />
|
|
3536
|
+
</ToolbarButton>
|
|
3537
|
+
<ToolbarButton active={isActive('code')} title="Inline code" onClick={() => editor.chain().focus().toggleCode().run()}>
|
|
3538
|
+
<Code2 size={14} />
|
|
3539
|
+
</ToolbarButton>
|
|
3540
|
+
<ToolbarSeparator />
|
|
3541
|
+
|
|
3542
|
+
<ToolbarButton active={isActive('bulletList')} title="Bullet list" onClick={() => editor.chain().focus().toggleBulletList().run()}>
|
|
3543
|
+
<List size={14} />
|
|
3544
|
+
</ToolbarButton>
|
|
3545
|
+
<ToolbarButton active={isActive('orderedList')} title="Ordered list" onClick={() => editor.chain().focus().toggleOrderedList().run()}>
|
|
3546
|
+
<ListOrdered size={14} />
|
|
3547
|
+
</ToolbarButton>
|
|
3548
|
+
<ToolbarButton active={isActive('blockquote')} title="Blockquote" onClick={() => editor.chain().focus().toggleBlockquote().run()}>
|
|
3549
|
+
<Quote size={14} />
|
|
3550
|
+
</ToolbarButton>
|
|
3551
|
+
<ToolbarButton active={isActive('codeBlock')} title="Code block" onClick={() => editor.chain().focus().toggleCodeBlock().run()}>
|
|
3552
|
+
<SquareCode size={14} />
|
|
3553
|
+
</ToolbarButton>
|
|
3554
|
+
<ToolbarSeparator />
|
|
3555
|
+
|
|
3556
|
+
<ToolbarButton active={isActive('link')} title="Set link" onClick={setLink}>
|
|
3557
|
+
<Link2 size={14} />
|
|
3558
|
+
</ToolbarButton>
|
|
3559
|
+
<ToolbarButton title="Remove link" onClick={() => editor.chain().focus().unsetLink().run()}>
|
|
3560
|
+
<Unlink2 size={14} />
|
|
3561
|
+
</ToolbarButton>
|
|
3562
|
+
<ToolbarButton title="Add image" onClick={onPickImageClick}>
|
|
3563
|
+
<ImagePlus size={14} />
|
|
3564
|
+
</ToolbarButton>
|
|
3565
|
+
<ToolbarButton title="Clear formatting" onClick={clearFormatting}>
|
|
3566
|
+
<Eraser size={14} />
|
|
3567
|
+
</ToolbarButton>
|
|
3641
3568
|
</div>
|
|
3569
|
+
<input
|
|
3570
|
+
ref={fileInputRef}
|
|
3571
|
+
type="file"
|
|
3572
|
+
accept="image/*"
|
|
3573
|
+
className="hidden"
|
|
3574
|
+
onChange={onFileSelected}
|
|
3575
|
+
/>
|
|
3642
3576
|
</div>
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
shadowMount
|
|
3646
|
-
);
|
|
3647
|
-
}
|
|
3648
|
-
|
|
3649
|
-
END_OF_FILE_CONTENT
|
|
3650
|
-
# SKIP: src/components/save-drawer/DopaDrawer.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
3651
|
-
# SKIP: src/components/save-drawer/DopaDrawer.tsx:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
3652
|
-
echo "Creating src/components/save-drawer/Visuals.tsx..."
|
|
3653
|
-
cat << 'END_OF_FILE_CONTENT' > "src/components/save-drawer/Visuals.tsx"
|
|
3654
|
-
import { useEffect, useRef, useState } from 'react';
|
|
3655
|
-
import type { CSSProperties } from 'react';
|
|
3656
|
-
|
|
3657
|
-
interface Particle {
|
|
3658
|
-
id: number;
|
|
3659
|
-
x: number;
|
|
3660
|
-
y: number;
|
|
3661
|
-
size: number;
|
|
3662
|
-
dur: number;
|
|
3663
|
-
delay: number;
|
|
3664
|
-
}
|
|
3665
|
-
|
|
3666
|
-
const PARTICLE_POOL: Particle[] = Array.from({ length: 44 }, (_, i) => ({
|
|
3667
|
-
id: i,
|
|
3668
|
-
x: 5 + Math.random() * 90,
|
|
3669
|
-
y: 15 + Math.random() * 70,
|
|
3670
|
-
size: 1.5 + Math.random() * 2.5,
|
|
3671
|
-
dur: 2.8 + Math.random() * 3.5,
|
|
3672
|
-
delay: Math.random() * 4,
|
|
3673
|
-
}));
|
|
3674
|
-
|
|
3675
|
-
interface ParticlesProps {
|
|
3676
|
-
count: number;
|
|
3677
|
-
color: string;
|
|
3678
|
-
}
|
|
3679
|
-
|
|
3680
|
-
export function Particles({ count, color }: ParticlesProps) {
|
|
3681
|
-
return (
|
|
3682
|
-
<div className="jp-drawer-particles" aria-hidden>
|
|
3683
|
-
{PARTICLE_POOL.slice(0, count).map((particle) => (
|
|
3684
|
-
<div
|
|
3685
|
-
key={particle.id}
|
|
3686
|
-
className="jp-drawer-particle"
|
|
3687
|
-
style={{
|
|
3688
|
-
left: `${particle.x}%`,
|
|
3689
|
-
bottom: `${particle.y}%`,
|
|
3690
|
-
width: particle.size,
|
|
3691
|
-
height: particle.size,
|
|
3692
|
-
background: color,
|
|
3693
|
-
boxShadow: `0 0 ${particle.size * 3}px ${color}`,
|
|
3694
|
-
opacity: 0,
|
|
3695
|
-
animation: `particle-float ${particle.dur}s ${particle.delay}s ease-out infinite`,
|
|
3696
|
-
}}
|
|
3697
|
-
/>
|
|
3698
|
-
))}
|
|
3577
|
+
)}
|
|
3578
|
+
<EditorContent editor={editor} className="jp-simple-editor" />
|
|
3699
3579
|
</div>
|
|
3700
3580
|
);
|
|
3701
|
-
}
|
|
3702
|
-
|
|
3703
|
-
const BAR_H = [0.45, 0.75, 0.55, 0.9, 0.65, 0.8, 0.5, 0.72, 0.6, 0.85, 0.42, 0.7];
|
|
3704
|
-
|
|
3705
|
-
interface BuildBarsProps {
|
|
3706
|
-
active: boolean;
|
|
3707
|
-
}
|
|
3708
|
-
|
|
3709
|
-
export function BuildBars({ active }: BuildBarsProps) {
|
|
3710
|
-
if (!active) return <div className="jp-drawer-bars-placeholder" />;
|
|
3581
|
+
};
|
|
3711
3582
|
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
height: `${height * 100}%`,
|
|
3720
|
-
animation: `bar-eq ${0.42 + i * 0.06}s ${i * 0.04}s ease-in-out infinite alternate`,
|
|
3721
|
-
}}
|
|
3722
|
-
/>
|
|
3723
|
-
))}
|
|
3724
|
-
</div>
|
|
3725
|
-
);
|
|
3726
|
-
}
|
|
3583
|
+
const PublicTiptapContent: React.FC<{ content: string }> = ({ content }) => (
|
|
3584
|
+
<article className="prose max-w-none prose-zinc" data-jp-field="content">
|
|
3585
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeSanitize]}>
|
|
3586
|
+
{content}
|
|
3587
|
+
</ReactMarkdown>
|
|
3588
|
+
</article>
|
|
3589
|
+
);
|
|
3727
3590
|
|
|
3728
|
-
const
|
|
3591
|
+
export const Tiptap: React.FC<{ data: TiptapData; settings?: TiptapSettings }> = ({ data }) => {
|
|
3592
|
+
const { mode } = useStudio();
|
|
3593
|
+
const isStudio = mode === 'studio';
|
|
3729
3594
|
|
|
3730
|
-
export function SuccessBurst() {
|
|
3731
3595
|
return (
|
|
3732
|
-
<
|
|
3733
|
-
{
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
/>
|
|
3748
|
-
))}
|
|
3749
|
-
</div>
|
|
3596
|
+
<section
|
|
3597
|
+
style={
|
|
3598
|
+
{
|
|
3599
|
+
'--local-bg': 'var(--background)',
|
|
3600
|
+
'--local-surface': 'var(--card)',
|
|
3601
|
+
'--local-text': 'var(--foreground)',
|
|
3602
|
+
'--local-border': 'var(--border)',
|
|
3603
|
+
} as React.CSSProperties
|
|
3604
|
+
}
|
|
3605
|
+
className="py-12 bg-[var(--local-bg)]"
|
|
3606
|
+
>
|
|
3607
|
+
<div className="container mx-auto px-6 max-w-4xl">
|
|
3608
|
+
{isStudio ? <StudioTiptapEditor data={data} /> : <PublicTiptapContent content={data.content ?? ''} />}
|
|
3609
|
+
</div>
|
|
3610
|
+
</section>
|
|
3750
3611
|
);
|
|
3751
|
-
}
|
|
3752
|
-
|
|
3753
|
-
interface ElapsedTimerProps {
|
|
3754
|
-
running: boolean;
|
|
3755
|
-
}
|
|
3756
|
-
|
|
3757
|
-
export function ElapsedTimer({ running }: ElapsedTimerProps) {
|
|
3758
|
-
const [elapsed, setElapsed] = useState(0);
|
|
3759
|
-
const startRef = useRef<number | null>(null);
|
|
3760
|
-
const rafRef = useRef<number | null>(null);
|
|
3761
|
-
|
|
3762
|
-
useEffect(() => {
|
|
3763
|
-
if (!running) return;
|
|
3764
|
-
if (!startRef.current) startRef.current = performance.now();
|
|
3765
|
-
|
|
3766
|
-
const tick = () => {
|
|
3767
|
-
if (!startRef.current) return;
|
|
3768
|
-
setElapsed(Math.floor((performance.now() - startRef.current) / 1000));
|
|
3769
|
-
rafRef.current = requestAnimationFrame(tick);
|
|
3770
|
-
};
|
|
3771
|
-
|
|
3772
|
-
rafRef.current = requestAnimationFrame(tick);
|
|
3773
|
-
return () => {
|
|
3774
|
-
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
3775
|
-
};
|
|
3776
|
-
}, [running]);
|
|
3777
|
-
|
|
3778
|
-
const sec = String(elapsed % 60).padStart(2, '0');
|
|
3779
|
-
const min = String(Math.floor(elapsed / 60)).padStart(2, '0');
|
|
3780
|
-
return <span className="jp-drawer-elapsed" aria-live="off">{min}:{sec}</span>;
|
|
3781
|
-
}
|
|
3612
|
+
};
|
|
3782
3613
|
|
|
3783
3614
|
END_OF_FILE_CONTENT
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
.jp-drawer-root {
|
|
3791
|
-
--background: 222 18% 6%;
|
|
3792
|
-
--foreground: 210 20% 96%;
|
|
3793
|
-
--card: 222 16% 8%;
|
|
3794
|
-
--card-foreground: 210 20% 96%;
|
|
3795
|
-
--primary: 0 0% 95%;
|
|
3796
|
-
--primary-foreground: 222 18% 6%;
|
|
3797
|
-
--secondary: 220 14% 13%;
|
|
3798
|
-
--secondary-foreground: 210 20% 96%;
|
|
3799
|
-
--destructive: 0 72% 51%;
|
|
3800
|
-
--destructive-foreground: 0 0% 98%;
|
|
3801
|
-
--border: 220 14% 13%;
|
|
3802
|
-
--radius: 0.6rem;
|
|
3803
|
-
font-family: 'Geist', system-ui, sans-serif;
|
|
3804
|
-
}
|
|
3805
|
-
|
|
3806
|
-
.jp-drawer-overlay {
|
|
3807
|
-
position: fixed;
|
|
3808
|
-
inset: 0;
|
|
3809
|
-
z-index: 2147483600;
|
|
3810
|
-
background: rgb(0 0 0 / 0.4);
|
|
3811
|
-
backdrop-filter: blur(2px);
|
|
3812
|
-
}
|
|
3813
|
-
|
|
3814
|
-
.jp-drawer-shell {
|
|
3815
|
-
position: fixed;
|
|
3816
|
-
left: 0;
|
|
3817
|
-
right: 0;
|
|
3818
|
-
z-index: 2147483601;
|
|
3819
|
-
display: flex;
|
|
3820
|
-
justify-content: center;
|
|
3821
|
-
padding: 0 1rem;
|
|
3822
|
-
}
|
|
3823
|
-
|
|
3824
|
-
.jp-drawer-card {
|
|
3825
|
-
position: relative;
|
|
3826
|
-
width: 100%;
|
|
3827
|
-
max-width: 31rem;
|
|
3828
|
-
overflow: hidden;
|
|
3829
|
-
border-radius: 1rem;
|
|
3830
|
-
border: 1px solid rgb(255 255 255 / 0.07);
|
|
3831
|
-
}
|
|
3832
|
-
|
|
3833
|
-
.jp-drawer-ambient {
|
|
3834
|
-
position: absolute;
|
|
3835
|
-
inset: 0;
|
|
3836
|
-
pointer-events: none;
|
|
3837
|
-
}
|
|
3838
|
-
|
|
3839
|
-
.jp-drawer-shimmer {
|
|
3840
|
-
position: absolute;
|
|
3841
|
-
inset: 0;
|
|
3842
|
-
overflow: hidden;
|
|
3843
|
-
pointer-events: none;
|
|
3844
|
-
}
|
|
3845
|
-
|
|
3846
|
-
.jp-drawer-shimmer-bar {
|
|
3847
|
-
position: absolute;
|
|
3848
|
-
inset-block: 0;
|
|
3849
|
-
width: 35%;
|
|
3850
|
-
}
|
|
3851
|
-
|
|
3852
|
-
.jp-drawer-content {
|
|
3853
|
-
position: relative;
|
|
3854
|
-
z-index: 10;
|
|
3855
|
-
padding: 2rem 2rem 1.75rem;
|
|
3856
|
-
}
|
|
3857
|
-
|
|
3858
|
-
.jp-drawer-header {
|
|
3859
|
-
margin-bottom: 1.5rem;
|
|
3860
|
-
display: flex;
|
|
3861
|
-
align-items: flex-start;
|
|
3862
|
-
justify-content: space-between;
|
|
3863
|
-
}
|
|
3864
|
-
|
|
3865
|
-
.jp-drawer-header-left {
|
|
3866
|
-
display: flex;
|
|
3867
|
-
flex-direction: column;
|
|
3868
|
-
gap: 0.625rem;
|
|
3869
|
-
}
|
|
3870
|
-
|
|
3871
|
-
.jp-drawer-status {
|
|
3872
|
-
display: flex;
|
|
3873
|
-
align-items: center;
|
|
3874
|
-
gap: 0.5rem;
|
|
3875
|
-
font-size: 0.75rem;
|
|
3876
|
-
font-weight: 700;
|
|
3877
|
-
text-transform: uppercase;
|
|
3878
|
-
letter-spacing: 0.1em;
|
|
3879
|
-
transition: color 0.5s;
|
|
3880
|
-
}
|
|
3881
|
-
|
|
3882
|
-
.jp-drawer-status-dot {
|
|
3883
|
-
width: 0.375rem;
|
|
3884
|
-
height: 0.375rem;
|
|
3885
|
-
border-radius: 9999px;
|
|
3886
|
-
display: inline-block;
|
|
3887
|
-
}
|
|
3888
|
-
|
|
3889
|
-
.jp-drawer-copy {
|
|
3890
|
-
min-height: 52px;
|
|
3891
|
-
}
|
|
3892
|
-
|
|
3893
|
-
.jp-drawer-copy-title {
|
|
3894
|
-
margin: 0;
|
|
3895
|
-
color: white;
|
|
3896
|
-
line-height: 1.25;
|
|
3897
|
-
font-weight: 600;
|
|
3898
|
-
}
|
|
3899
|
-
|
|
3900
|
-
.jp-drawer-copy-title-lg {
|
|
3901
|
-
font-size: 1.125rem;
|
|
3902
|
-
}
|
|
3903
|
-
|
|
3904
|
-
.jp-drawer-copy-title-md {
|
|
3905
|
-
font-size: 1rem;
|
|
3906
|
-
}
|
|
3907
|
-
|
|
3908
|
-
.jp-drawer-copy-sub {
|
|
3909
|
-
margin: 0.125rem 0 0;
|
|
3910
|
-
color: rgb(255 255 255 / 0.4);
|
|
3911
|
-
font-size: 0.875rem;
|
|
3912
|
-
}
|
|
3913
|
-
|
|
3914
|
-
.jp-drawer-copy-sub-error {
|
|
3915
|
-
color: rgb(255 255 255 / 0.35);
|
|
3916
|
-
}
|
|
3917
|
-
|
|
3918
|
-
.jp-drawer-poem-line {
|
|
3919
|
-
margin: 0;
|
|
3920
|
-
font-size: 0.875rem;
|
|
3921
|
-
font-weight: 300;
|
|
3922
|
-
line-height: 1.5;
|
|
3923
|
-
}
|
|
3924
|
-
|
|
3925
|
-
.jp-drawer-poem-line-1 {
|
|
3926
|
-
color: rgb(255 255 255 / 0.55);
|
|
3927
|
-
}
|
|
3928
|
-
|
|
3929
|
-
.jp-drawer-poem-line-2 {
|
|
3930
|
-
color: rgb(255 255 255 / 0.3);
|
|
3931
|
-
}
|
|
3932
|
-
|
|
3933
|
-
.jp-drawer-right {
|
|
3934
|
-
margin-left: 1.5rem;
|
|
3935
|
-
display: flex;
|
|
3936
|
-
flex-direction: column;
|
|
3937
|
-
align-items: flex-end;
|
|
3938
|
-
gap: 0.5rem;
|
|
3939
|
-
flex-shrink: 0;
|
|
3940
|
-
}
|
|
3941
|
-
|
|
3942
|
-
.jp-drawer-countdown-wrap {
|
|
3943
|
-
display: flex;
|
|
3944
|
-
flex-direction: column;
|
|
3945
|
-
align-items: flex-end;
|
|
3946
|
-
gap: 0.5rem;
|
|
3947
|
-
}
|
|
3948
|
-
|
|
3949
|
-
.jp-drawer-countdown-text {
|
|
3950
|
-
font-family: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
3951
|
-
font-size: 0.75rem;
|
|
3952
|
-
font-weight: 600;
|
|
3953
|
-
color: #34d399;
|
|
3954
|
-
}
|
|
3955
|
-
|
|
3956
|
-
.jp-drawer-countdown-track {
|
|
3957
|
-
width: 6rem;
|
|
3958
|
-
height: 0.125rem;
|
|
3959
|
-
border-radius: 9999px;
|
|
3960
|
-
overflow: hidden;
|
|
3961
|
-
background: rgb(255 255 255 / 0.1);
|
|
3962
|
-
}
|
|
3963
|
-
|
|
3964
|
-
.jp-drawer-countdown-bar {
|
|
3965
|
-
width: 100%;
|
|
3966
|
-
height: 100%;
|
|
3967
|
-
border-radius: 9999px;
|
|
3968
|
-
background: #34d399;
|
|
3969
|
-
}
|
|
3970
|
-
|
|
3971
|
-
.jp-drawer-track-row {
|
|
3972
|
-
margin-bottom: 1rem;
|
|
3973
|
-
display: flex;
|
|
3974
|
-
align-items: center;
|
|
3975
|
-
}
|
|
3976
|
-
|
|
3977
|
-
.jp-drawer-bars-wrap {
|
|
3978
|
-
margin-bottom: 1rem;
|
|
3979
|
-
display: flex;
|
|
3980
|
-
justify-content: center;
|
|
3981
|
-
}
|
|
3982
|
-
|
|
3983
|
-
.jp-drawer-separator {
|
|
3984
|
-
margin-bottom: 1rem;
|
|
3985
|
-
height: 1px;
|
|
3986
|
-
width: 100%;
|
|
3987
|
-
border: 0;
|
|
3988
|
-
background: rgb(255 255 255 / 0.06);
|
|
3989
|
-
}
|
|
3990
|
-
|
|
3991
|
-
.jp-drawer-footer {
|
|
3992
|
-
display: flex;
|
|
3993
|
-
align-items: center;
|
|
3994
|
-
gap: 1rem;
|
|
3995
|
-
}
|
|
3996
|
-
|
|
3997
|
-
.jp-drawer-progress {
|
|
3998
|
-
flex: 1;
|
|
3999
|
-
height: 2px;
|
|
4000
|
-
border-radius: 9999px;
|
|
4001
|
-
overflow: hidden;
|
|
4002
|
-
background: rgb(255 255 255 / 0.06);
|
|
4003
|
-
}
|
|
4004
|
-
|
|
4005
|
-
.jp-drawer-progress-indicator {
|
|
4006
|
-
height: 100%;
|
|
4007
|
-
}
|
|
4008
|
-
|
|
4009
|
-
.jp-drawer-cta {
|
|
4010
|
-
display: flex;
|
|
4011
|
-
align-items: center;
|
|
4012
|
-
gap: 0.5rem;
|
|
4013
|
-
flex-shrink: 0;
|
|
4014
|
-
}
|
|
4015
|
-
|
|
4016
|
-
.jp-drawer-running-step {
|
|
4017
|
-
font-family: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
4018
|
-
font-size: 0.75rem;
|
|
4019
|
-
color: rgb(255 255 255 / 0.2);
|
|
4020
|
-
}
|
|
4021
|
-
|
|
4022
|
-
.jp-drawer-btn-row {
|
|
4023
|
-
display: flex;
|
|
4024
|
-
gap: 0.5rem;
|
|
4025
|
-
}
|
|
4026
|
-
|
|
4027
|
-
.jp-drawer-btn {
|
|
4028
|
-
border: 1px solid transparent;
|
|
4029
|
-
border-radius: 0.375rem;
|
|
4030
|
-
font-size: 0.8125rem;
|
|
4031
|
-
font-weight: 500;
|
|
4032
|
-
line-height: 1;
|
|
4033
|
-
height: 2.25rem;
|
|
4034
|
-
padding: 0 0.75rem;
|
|
4035
|
-
cursor: pointer;
|
|
4036
|
-
transition: all 0.2s ease;
|
|
4037
|
-
display: inline-flex;
|
|
4038
|
-
align-items: center;
|
|
4039
|
-
justify-content: center;
|
|
4040
|
-
gap: 0.375rem;
|
|
4041
|
-
}
|
|
4042
|
-
|
|
4043
|
-
.jp-drawer-btn:disabled {
|
|
4044
|
-
opacity: 0.5;
|
|
4045
|
-
cursor: not-allowed;
|
|
4046
|
-
}
|
|
4047
|
-
|
|
4048
|
-
.jp-drawer-btn-secondary {
|
|
4049
|
-
background: hsl(var(--secondary));
|
|
4050
|
-
color: hsl(var(--secondary-foreground));
|
|
4051
|
-
}
|
|
4052
|
-
|
|
4053
|
-
.jp-drawer-btn-secondary:hover {
|
|
4054
|
-
filter: brightness(1.08);
|
|
4055
|
-
}
|
|
4056
|
-
|
|
4057
|
-
.jp-drawer-btn-emerald {
|
|
4058
|
-
background: #34d399;
|
|
4059
|
-
color: #18181b;
|
|
4060
|
-
font-weight: 600;
|
|
4061
|
-
}
|
|
4062
|
-
|
|
4063
|
-
.jp-drawer-btn-emerald:hover {
|
|
4064
|
-
background: #6ee7b7;
|
|
4065
|
-
}
|
|
4066
|
-
|
|
4067
|
-
.jp-drawer-btn-ghost {
|
|
4068
|
-
background: transparent;
|
|
4069
|
-
color: rgb(255 255 255 / 0.9);
|
|
4070
|
-
}
|
|
4071
|
-
|
|
4072
|
-
.jp-drawer-btn-ghost:hover {
|
|
4073
|
-
background: rgb(255 255 255 / 0.08);
|
|
4074
|
-
}
|
|
4075
|
-
|
|
4076
|
-
.jp-drawer-btn-destructive {
|
|
4077
|
-
background: hsl(var(--destructive));
|
|
4078
|
-
color: hsl(var(--destructive-foreground));
|
|
4079
|
-
}
|
|
4080
|
-
|
|
4081
|
-
.jp-drawer-btn-destructive:hover {
|
|
4082
|
-
filter: brightness(1.06);
|
|
4083
|
-
}
|
|
4084
|
-
|
|
4085
|
-
.jp-drawer-node-wrap {
|
|
4086
|
-
position: relative;
|
|
4087
|
-
z-index: 10;
|
|
4088
|
-
display: flex;
|
|
4089
|
-
flex-direction: column;
|
|
4090
|
-
align-items: center;
|
|
4091
|
-
gap: 0.625rem;
|
|
4092
|
-
}
|
|
4093
|
-
|
|
4094
|
-
.jp-drawer-node {
|
|
4095
|
-
position: relative;
|
|
4096
|
-
width: 3rem;
|
|
4097
|
-
height: 3rem;
|
|
4098
|
-
border-radius: 9999px;
|
|
4099
|
-
border: 1px solid transparent;
|
|
4100
|
-
display: flex;
|
|
4101
|
-
align-items: center;
|
|
4102
|
-
justify-content: center;
|
|
4103
|
-
transition: all 0.5s;
|
|
4104
|
-
}
|
|
4105
|
-
|
|
4106
|
-
.jp-drawer-node-pending {
|
|
4107
|
-
border-color: rgb(255 255 255 / 0.08);
|
|
4108
|
-
background: rgb(255 255 255 / 0.02);
|
|
4109
|
-
}
|
|
4110
|
-
|
|
4111
|
-
.jp-drawer-node-glyph {
|
|
4112
|
-
font-size: 1.125rem;
|
|
4113
|
-
line-height: 1;
|
|
4114
|
-
}
|
|
4115
|
-
|
|
4116
|
-
.jp-drawer-node-glyph-active {
|
|
4117
|
-
display: inline-block;
|
|
4118
|
-
}
|
|
4119
|
-
|
|
4120
|
-
.jp-drawer-node-glyph-pending {
|
|
4121
|
-
color: rgb(255 255 255 / 0.15);
|
|
4122
|
-
}
|
|
4123
|
-
|
|
4124
|
-
.jp-drawer-node-ring {
|
|
4125
|
-
position: absolute;
|
|
4126
|
-
border-radius: 9999px;
|
|
4127
|
-
border: 1px solid transparent;
|
|
4128
|
-
}
|
|
4129
|
-
|
|
4130
|
-
.jp-drawer-node-label {
|
|
4131
|
-
font-size: 10px;
|
|
4132
|
-
font-weight: 600;
|
|
4133
|
-
text-transform: uppercase;
|
|
4134
|
-
letter-spacing: 0.1em;
|
|
4135
|
-
transition: color 0.5s;
|
|
4136
|
-
}
|
|
4137
|
-
|
|
4138
|
-
.jp-drawer-connector {
|
|
4139
|
-
position: relative;
|
|
4140
|
-
z-index: 0;
|
|
4141
|
-
flex: 1;
|
|
4142
|
-
height: 2px;
|
|
4143
|
-
margin-top: -24px;
|
|
4144
|
-
}
|
|
4145
|
-
|
|
4146
|
-
.jp-drawer-connector-base {
|
|
4147
|
-
position: absolute;
|
|
4148
|
-
inset: 0;
|
|
4149
|
-
border-radius: 9999px;
|
|
4150
|
-
background: rgb(255 255 255 / 0.08);
|
|
4151
|
-
}
|
|
4152
|
-
|
|
4153
|
-
.jp-drawer-connector-fill {
|
|
4154
|
-
position: absolute;
|
|
4155
|
-
left: 0;
|
|
4156
|
-
right: auto;
|
|
4157
|
-
top: 0;
|
|
4158
|
-
bottom: 0;
|
|
4159
|
-
border-radius: 9999px;
|
|
4160
|
-
}
|
|
4161
|
-
|
|
4162
|
-
.jp-drawer-connector-orb {
|
|
4163
|
-
position: absolute;
|
|
4164
|
-
top: 50%;
|
|
4165
|
-
transform: translateY(-50%);
|
|
4166
|
-
width: 10px;
|
|
4167
|
-
height: 10px;
|
|
4168
|
-
border-radius: 9999px;
|
|
4169
|
-
}
|
|
4170
|
-
|
|
4171
|
-
.jp-drawer-particles {
|
|
4172
|
-
position: absolute;
|
|
4173
|
-
inset: 0;
|
|
4174
|
-
overflow: hidden;
|
|
4175
|
-
pointer-events: none;
|
|
4176
|
-
}
|
|
4177
|
-
|
|
4178
|
-
.jp-drawer-particle {
|
|
4179
|
-
position: absolute;
|
|
4180
|
-
border-radius: 9999px;
|
|
4181
|
-
}
|
|
4182
|
-
|
|
4183
|
-
.jp-drawer-bars {
|
|
4184
|
-
height: 1.75rem;
|
|
4185
|
-
display: flex;
|
|
4186
|
-
align-items: flex-end;
|
|
4187
|
-
gap: 3px;
|
|
4188
|
-
}
|
|
4189
|
-
|
|
4190
|
-
.jp-drawer-bars-placeholder {
|
|
4191
|
-
height: 1.75rem;
|
|
4192
|
-
}
|
|
4193
|
-
|
|
4194
|
-
.jp-drawer-bar {
|
|
4195
|
-
width: 3px;
|
|
4196
|
-
border-radius: 2px;
|
|
4197
|
-
background: #f59e0b;
|
|
4198
|
-
transform-origin: bottom;
|
|
4199
|
-
}
|
|
4200
|
-
|
|
4201
|
-
.jp-drawer-burst {
|
|
4202
|
-
position: absolute;
|
|
4203
|
-
inset: 0;
|
|
4204
|
-
display: flex;
|
|
4205
|
-
align-items: center;
|
|
4206
|
-
justify-content: center;
|
|
4207
|
-
pointer-events: none;
|
|
4208
|
-
}
|
|
4209
|
-
|
|
4210
|
-
.jp-drawer-burst-dot {
|
|
4211
|
-
position: absolute;
|
|
4212
|
-
width: 5px;
|
|
4213
|
-
height: 5px;
|
|
4214
|
-
border-radius: 9999px;
|
|
4215
|
-
}
|
|
4216
|
-
|
|
4217
|
-
.jp-drawer-elapsed {
|
|
4218
|
-
font-family: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
4219
|
-
font-size: 0.75rem;
|
|
4220
|
-
letter-spacing: 0.1em;
|
|
4221
|
-
color: rgb(255 255 255 / 0.25);
|
|
4222
|
-
}
|
|
4223
|
-
|
|
4224
|
-
/* Animation helper classes */
|
|
4225
|
-
.animate-drawer-up { animation: drawer-up 0.45s cubic-bezier(0.22, 1, 0.36, 1) forwards; }
|
|
4226
|
-
.animate-fade-in { animation: fade-in 0.25s ease forwards; }
|
|
4227
|
-
.animate-fade-up { animation: fade-up 0.35s ease forwards; }
|
|
4228
|
-
.animate-text-in { animation: text-in 0.3s ease forwards; }
|
|
4229
|
-
.animate-success-pop { animation: success-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }
|
|
4230
|
-
.countdown-bar { animation: countdown-drain 3s linear forwards; }
|
|
4231
|
-
|
|
4232
|
-
.stroke-dash-30 {
|
|
4233
|
-
stroke-dasharray: 30;
|
|
4234
|
-
stroke-dashoffset: 30;
|
|
4235
|
-
}
|
|
4236
|
-
|
|
4237
|
-
.animate-check-draw {
|
|
4238
|
-
animation: check-draw 0.4s 0.05s ease forwards;
|
|
4239
|
-
}
|
|
4240
|
-
|
|
4241
|
-
@keyframes check-draw {
|
|
4242
|
-
to { stroke-dashoffset: 0; }
|
|
4243
|
-
}
|
|
4244
|
-
|
|
4245
|
-
@keyframes drawer-up {
|
|
4246
|
-
from { transform: translateY(100%); opacity: 0; }
|
|
4247
|
-
to { transform: translateY(0); opacity: 1; }
|
|
4248
|
-
}
|
|
4249
|
-
|
|
4250
|
-
@keyframes fade-in {
|
|
4251
|
-
from { opacity: 0; }
|
|
4252
|
-
to { opacity: 1; }
|
|
4253
|
-
}
|
|
4254
|
-
|
|
4255
|
-
@keyframes fade-up {
|
|
4256
|
-
from { opacity: 0; transform: translateY(8px); }
|
|
4257
|
-
to { transform: translateY(0); opacity: 1; }
|
|
4258
|
-
}
|
|
4259
|
-
|
|
4260
|
-
@keyframes text-in {
|
|
4261
|
-
from { opacity: 0; transform: translateX(-6px); }
|
|
4262
|
-
to { opacity: 1; transform: translateX(0); }
|
|
4263
|
-
}
|
|
4264
|
-
|
|
4265
|
-
@keyframes success-pop {
|
|
4266
|
-
0% { transform: scale(0.88); opacity: 0; }
|
|
4267
|
-
60% { transform: scale(1.04); }
|
|
4268
|
-
100% { transform: scale(1); opacity: 1; }
|
|
4269
|
-
}
|
|
4270
|
-
|
|
4271
|
-
@keyframes ambient-pulse {
|
|
4272
|
-
0%, 100% { opacity: 0.3; }
|
|
4273
|
-
50% { opacity: 0.65; }
|
|
4274
|
-
}
|
|
4275
|
-
|
|
4276
|
-
@keyframes shimmer-sweep {
|
|
4277
|
-
from { transform: translateX(-100%); }
|
|
4278
|
-
to { transform: translateX(250%); }
|
|
4279
|
-
}
|
|
4280
|
-
|
|
4281
|
-
@keyframes node-glow {
|
|
4282
|
-
0%, 100% { box-shadow: 0 0 12px var(--glow-color,#60a5fa55); }
|
|
4283
|
-
50% { box-shadow: 0 0 28px var(--glow-color,#60a5fa88), 0 0 48px var(--glow-color,#60a5fa22); }
|
|
4284
|
-
}
|
|
4285
|
-
|
|
4286
|
-
@keyframes glyph-rotate {
|
|
4287
|
-
from { transform: rotate(0deg); }
|
|
4288
|
-
to { transform: rotate(360deg); }
|
|
4289
|
-
}
|
|
4290
|
-
|
|
4291
|
-
@keyframes ring-expand {
|
|
4292
|
-
from { transform: scale(1); opacity: 0.7; }
|
|
4293
|
-
to { transform: scale(2.1); opacity: 0; }
|
|
4294
|
-
}
|
|
3615
|
+
echo "Creating src/components/tiptap/index.ts..."
|
|
3616
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/tiptap/index.ts"
|
|
3617
|
+
export * from './View';
|
|
3618
|
+
export * from './schema';
|
|
3619
|
+
export * from './types';
|
|
4295
3620
|
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
}
|
|
3621
|
+
END_OF_FILE_CONTENT
|
|
3622
|
+
echo "Creating src/components/tiptap/schema.ts..."
|
|
3623
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/tiptap/schema.ts"
|
|
3624
|
+
import { z } from 'zod';
|
|
3625
|
+
import { BaseSectionData } from '@/lib/base-schemas';
|
|
4300
3626
|
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
100% { transform: translateY(-90px) scale(0.3); opacity: 0; }
|
|
4305
|
-
}
|
|
3627
|
+
export const TiptapSchema = BaseSectionData.extend({
|
|
3628
|
+
content: z.string().default('').describe('ui:editorial-markdown'),
|
|
3629
|
+
});
|
|
4306
3630
|
|
|
4307
|
-
|
|
4308
|
-
from { transform: scaleY(0.4); }
|
|
4309
|
-
to { transform: scaleY(1); }
|
|
4310
|
-
}
|
|
3631
|
+
export const TiptapSettingsSchema = z.object({});
|
|
4311
3632
|
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
}
|
|
3633
|
+
END_OF_FILE_CONTENT
|
|
3634
|
+
echo "Creating src/components/tiptap/types.ts..."
|
|
3635
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/tiptap/types.ts"
|
|
3636
|
+
import { z } from 'zod';
|
|
3637
|
+
import { TiptapSchema, TiptapSettingsSchema } from './schema';
|
|
4316
3638
|
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
to { width: 0%; }
|
|
4320
|
-
}
|
|
3639
|
+
export type TiptapData = z.infer<typeof TiptapSchema>;
|
|
3640
|
+
export type TiptapSettings = z.infer<typeof TiptapSettingsSchema>;
|
|
4321
3641
|
|
|
4322
3642
|
END_OF_FILE_CONTENT
|
|
4323
|
-
# SKIP: src/components/save-drawer/saverStyle.css:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
4324
|
-
# SKIP: src/components/save-drawer/saverStyle.css:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
4325
3643
|
mkdir -p "src/components/ui"
|
|
4326
3644
|
echo "Creating src/components/ui/checkbox.tsx..."
|
|
4327
3645
|
cat << 'END_OF_FILE_CONTENT' > "src/components/ui/checkbox.tsx"
|
|
@@ -5608,6 +4926,93 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
|
5608
4926
|
}
|
|
5609
4927
|
END_OF_FILE_CONTENT
|
|
5610
4928
|
# SKIP: src/data/pages/home.json:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
4929
|
+
echo "Creating src/data/pages/post.json..."
|
|
4930
|
+
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/post.json"
|
|
4931
|
+
{
|
|
4932
|
+
"id": "post-page",
|
|
4933
|
+
"slug": "post",
|
|
4934
|
+
"meta": {
|
|
4935
|
+
"title": "Post",
|
|
4936
|
+
"description": "Smoke test page for header + tiptap + footer flow."
|
|
4937
|
+
},
|
|
4938
|
+
"sections": [
|
|
4939
|
+
{
|
|
4940
|
+
"id": "post-editorial-main",
|
|
4941
|
+
"type": "tiptap",
|
|
4942
|
+
"data": {
|
|
4943
|
+
"content": "## qefqfqfqf**header / tiptap / footer** smoke flow.\n\nInline **editing** in Studio\n\n## `Markdown persistence on Save`\n\n- Safe render on public route"
|
|
4944
|
+
},
|
|
4945
|
+
"settings": {}
|
|
4946
|
+
}
|
|
4947
|
+
]
|
|
4948
|
+
}
|
|
4949
|
+
END_OF_FILE_CONTENT
|
|
4950
|
+
mkdir -p "src/data/pages/servizi"
|
|
4951
|
+
echo "Creating src/data/pages/servizi/trattamento.json..."
|
|
4952
|
+
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/servizi/trattamento.json"
|
|
4953
|
+
{
|
|
4954
|
+
"id": "servizi-trattamento-page",
|
|
4955
|
+
"slug": "servizi/trattamento",
|
|
4956
|
+
"meta": {
|
|
4957
|
+
"title": "Servizi - Trattamento",
|
|
4958
|
+
"description": "Pagina nested di smoke test per verificare routing visitor/admin/preview."
|
|
4959
|
+
},
|
|
4960
|
+
"sections": [
|
|
4961
|
+
{
|
|
4962
|
+
"id": "hero-servizi-trattamento",
|
|
4963
|
+
"type": "hero",
|
|
4964
|
+
"data": {
|
|
4965
|
+
"badge": "Smoke Test",
|
|
4966
|
+
"title": "Trattamento",
|
|
4967
|
+
"titleHighlight": "Pagina Nested",
|
|
4968
|
+
"description": "Questa pagina verifica il supporto ai nested slug su filesystem e router.",
|
|
4969
|
+
"ctas": [
|
|
4970
|
+
{
|
|
4971
|
+
"id": "cta-home",
|
|
4972
|
+
"label": "Torna Home",
|
|
4973
|
+
"href": "/",
|
|
4974
|
+
"variant": "primary"
|
|
4975
|
+
}
|
|
4976
|
+
]
|
|
4977
|
+
},
|
|
4978
|
+
"settings": {}
|
|
4979
|
+
}
|
|
4980
|
+
]
|
|
4981
|
+
}
|
|
4982
|
+
|
|
4983
|
+
END_OF_FILE_CONTENT
|
|
4984
|
+
echo "Creating src/data/pages/servizi_trattamento.json..."
|
|
4985
|
+
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/servizi_trattamento.json"
|
|
4986
|
+
{
|
|
4987
|
+
"id": "servizi-trattamento-page",
|
|
4988
|
+
"slug": "servizi/trattamento",
|
|
4989
|
+
"meta": {
|
|
4990
|
+
"title": "Servizi - Trattamento",
|
|
4991
|
+
"description": "Pagina nested di smoke test per verificare routing visitor/admin/preview."
|
|
4992
|
+
},
|
|
4993
|
+
"sections": [
|
|
4994
|
+
{
|
|
4995
|
+
"id": "hero-servizi-trattamento",
|
|
4996
|
+
"type": "hero",
|
|
4997
|
+
"data": {
|
|
4998
|
+
"badge": "Smoke Test",
|
|
4999
|
+
"title": "Trattamentos",
|
|
5000
|
+
"titleHighlight": "Pagina Nested",
|
|
5001
|
+
"description": "Questa pagina verifica il supporto ai nested slug su filesystem e router.",
|
|
5002
|
+
"ctas": [
|
|
5003
|
+
{
|
|
5004
|
+
"id": "cta-home",
|
|
5005
|
+
"label": "Torna Home",
|
|
5006
|
+
"href": "/",
|
|
5007
|
+
"variant": "primary"
|
|
5008
|
+
}
|
|
5009
|
+
]
|
|
5010
|
+
},
|
|
5011
|
+
"settings": {}
|
|
5012
|
+
}
|
|
5013
|
+
]
|
|
5014
|
+
}
|
|
5015
|
+
END_OF_FILE_CONTENT
|
|
5611
5016
|
echo "Creating src/fonts.css..."
|
|
5612
5017
|
cat << 'END_OF_FILE_CONTENT' > "src/fonts.css"
|
|
5613
5018
|
@import url('https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,600;12..96,700;12..96,800&family=JetBrains+Mono:wght@400;500;600;700&family=Instrument+Sans:wght@400;500;600&display=swap');
|
|
@@ -5815,6 +5220,40 @@ html {
|
|
|
5815
5220
|
opacity: 0;
|
|
5816
5221
|
}
|
|
5817
5222
|
|
|
5223
|
+
/* Editorial focus lock: avoid section reselection while selecting text in inline editor. */
|
|
5224
|
+
[data-section-id].jp-editorial-focus [data-jp-section-overlay] {
|
|
5225
|
+
border-color: transparent !important;
|
|
5226
|
+
background: transparent !important;
|
|
5227
|
+
}
|
|
5228
|
+
|
|
5229
|
+
[data-section-id].jp-editorial-focus [data-jp-section-overlay] > div {
|
|
5230
|
+
opacity: 0 !important;
|
|
5231
|
+
pointer-events: none;
|
|
5232
|
+
}
|
|
5233
|
+
|
|
5234
|
+
/* Tiptap simple-editor-like behavior in Studio */
|
|
5235
|
+
.jp-simple-editor .ProseMirror:focus {
|
|
5236
|
+
outline: none;
|
|
5237
|
+
}
|
|
5238
|
+
|
|
5239
|
+
.jp-simple-editor .ProseMirror img {
|
|
5240
|
+
max-width: 100%;
|
|
5241
|
+
height: auto;
|
|
5242
|
+
border-radius: 0.5rem;
|
|
5243
|
+
}
|
|
5244
|
+
|
|
5245
|
+
.jp-simple-editor .ProseMirror img[data-uploading="true"] {
|
|
5246
|
+
opacity: 0.6;
|
|
5247
|
+
filter: grayscale(0.25);
|
|
5248
|
+
outline: 2px dashed rgb(59 130 246 / 0.7);
|
|
5249
|
+
outline-offset: 2px;
|
|
5250
|
+
}
|
|
5251
|
+
|
|
5252
|
+
.jp-simple-editor .ProseMirror img[data-upload-error="true"] {
|
|
5253
|
+
outline: 2px solid rgb(239 68 68 / 0.8);
|
|
5254
|
+
outline-offset: 2px;
|
|
5255
|
+
}
|
|
5256
|
+
|
|
5818
5257
|
|
|
5819
5258
|
|
|
5820
5259
|
END_OF_FILE_CONTENT
|
|
@@ -5840,6 +5279,7 @@ import { GitSection } from '@/components/git-section';
|
|
|
5840
5279
|
import { Devex } from '@/components/devex';
|
|
5841
5280
|
import { CliSection } from '@/components/cli-section';
|
|
5842
5281
|
import { DocsLayout } from '@/components/docs-layout';
|
|
5282
|
+
import { Tiptap } from '@/components/tiptap';
|
|
5843
5283
|
|
|
5844
5284
|
import type { SectionType } from '@jsonpages/core';
|
|
5845
5285
|
import type { SectionComponentPropsMap } from '@/types';
|
|
@@ -5865,6 +5305,7 @@ export const ComponentRegistry: {
|
|
|
5865
5305
|
'devex': Devex,
|
|
5866
5306
|
'cli-section': CliSection,
|
|
5867
5307
|
'docs-layout': DocsLayout,
|
|
5308
|
+
'tiptap': Tiptap,
|
|
5868
5309
|
};
|
|
5869
5310
|
|
|
5870
5311
|
END_OF_FILE_CONTENT
|
|
@@ -5936,7 +5377,7 @@ const addableSectionTypes = [
|
|
|
5936
5377
|
'hero', 'feature-grid', 'code-block', 'problem-statement',
|
|
5937
5378
|
'pillars-grid', 'arch-layers', 'product-triad', 'pa-section',
|
|
5938
5379
|
'philosophy', 'cta-banner', 'image-break',
|
|
5939
|
-
'cms-ice', 'git-section', 'devex', 'cli-section', 'docs-layout',
|
|
5380
|
+
'cms-ice', 'git-section', 'devex', 'cli-section', 'docs-layout', 'tiptap',
|
|
5940
5381
|
] as const;
|
|
5941
5382
|
|
|
5942
5383
|
const sectionTypeLabels: Record<string, string> = {
|
|
@@ -5956,6 +5397,7 @@ const sectionTypeLabels: Record<string, string> = {
|
|
|
5956
5397
|
'devex': 'Developer Experience',
|
|
5957
5398
|
'cli-section': 'CLI Tool',
|
|
5958
5399
|
'docs-layout': 'Documentation Layout',
|
|
5400
|
+
'tiptap': 'Tiptap Editorial',
|
|
5959
5401
|
};
|
|
5960
5402
|
|
|
5961
5403
|
function getDefaultSectionData(type: string): Record<string, unknown> {
|
|
@@ -5976,6 +5418,7 @@ function getDefaultSectionData(type: string): Record<string, unknown> {
|
|
|
5976
5418
|
case 'devex': return { title: 'Developer Experience', description: '', features: [] };
|
|
5977
5419
|
case 'cli-section': return { title: 'CLI Tool', description: '', steps: [] };
|
|
5978
5420
|
case 'docs-layout': return { pageTitle: 'Documentation', pageSubtitle: '', version: 'v1.0', groups: [] };
|
|
5421
|
+
case 'tiptap': return { content: '# Post title\n\nStart writing in Markdown...' };
|
|
5979
5422
|
default: return {};
|
|
5980
5423
|
}
|
|
5981
5424
|
}
|
|
@@ -6031,176 +5474,8 @@ export const CtaSchema = z.object({
|
|
|
6031
5474
|
|
|
6032
5475
|
END_OF_FILE_CONTENT
|
|
6033
5476
|
# SKIP: src/lib/base-schemas.ts:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
6034
|
-
echo "Creating src/lib/cloudSaveStream.ts..."
|
|
6035
|
-
cat << 'END_OF_FILE_CONTENT' > "src/lib/cloudSaveStream.ts"
|
|
6036
|
-
import type { StepId } from '@/types/deploy';
|
|
6037
|
-
|
|
6038
|
-
interface SaveStreamStepEvent {
|
|
6039
|
-
id: StepId;
|
|
6040
|
-
status: 'running' | 'done';
|
|
6041
|
-
label?: string;
|
|
6042
|
-
}
|
|
6043
|
-
|
|
6044
|
-
interface SaveStreamLogEvent {
|
|
6045
|
-
stepId: StepId;
|
|
6046
|
-
message: string;
|
|
6047
|
-
}
|
|
6048
|
-
|
|
6049
|
-
interface SaveStreamDoneEvent {
|
|
6050
|
-
deployUrl?: string;
|
|
6051
|
-
commitSha?: string;
|
|
6052
|
-
}
|
|
6053
|
-
|
|
6054
|
-
interface SaveStreamErrorEvent {
|
|
6055
|
-
message?: string;
|
|
6056
|
-
}
|
|
6057
|
-
|
|
6058
|
-
interface StartCloudSaveStreamInput {
|
|
6059
|
-
apiBaseUrl: string;
|
|
6060
|
-
apiKey: string;
|
|
6061
|
-
path: string;
|
|
6062
|
-
content: unknown;
|
|
6063
|
-
message?: string;
|
|
6064
|
-
signal?: AbortSignal;
|
|
6065
|
-
onStep: (event: SaveStreamStepEvent) => void;
|
|
6066
|
-
onLog?: (event: SaveStreamLogEvent) => void;
|
|
6067
|
-
onDone: (event: SaveStreamDoneEvent) => void;
|
|
6068
|
-
}
|
|
6069
|
-
|
|
6070
|
-
function parseSseEventBlock(rawBlock: string): { event: string; data: string } | null {
|
|
6071
|
-
const lines = rawBlock
|
|
6072
|
-
.split('\n')
|
|
6073
|
-
.map((line) => line.trimEnd())
|
|
6074
|
-
.filter((line) => line.length > 0);
|
|
6075
|
-
|
|
6076
|
-
if (lines.length === 0) return null;
|
|
6077
|
-
|
|
6078
|
-
let eventName = 'message';
|
|
6079
|
-
const dataLines: string[] = [];
|
|
6080
|
-
for (const line of lines) {
|
|
6081
|
-
if (line.startsWith('event:')) {
|
|
6082
|
-
eventName = line.slice(6).trim();
|
|
6083
|
-
continue;
|
|
6084
|
-
}
|
|
6085
|
-
if (line.startsWith('data:')) {
|
|
6086
|
-
dataLines.push(line.slice(5).trim());
|
|
6087
|
-
}
|
|
6088
|
-
}
|
|
6089
|
-
return { event: eventName, data: dataLines.join('\n') };
|
|
6090
|
-
}
|
|
6091
|
-
|
|
6092
|
-
export async function startCloudSaveStream(input: StartCloudSaveStreamInput): Promise<void> {
|
|
6093
|
-
const response = await fetch(`${input.apiBaseUrl}/save-stream`, {
|
|
6094
|
-
method: 'POST',
|
|
6095
|
-
headers: {
|
|
6096
|
-
'Content-Type': 'application/json',
|
|
6097
|
-
Authorization: `Bearer ${input.apiKey}`,
|
|
6098
|
-
},
|
|
6099
|
-
body: JSON.stringify({
|
|
6100
|
-
path: input.path,
|
|
6101
|
-
content: input.content,
|
|
6102
|
-
message: input.message,
|
|
6103
|
-
}),
|
|
6104
|
-
signal: input.signal,
|
|
6105
|
-
});
|
|
6106
|
-
|
|
6107
|
-
if (!response.ok || !response.body) {
|
|
6108
|
-
const body = (await response.json().catch(() => ({}))) as SaveStreamErrorEvent;
|
|
6109
|
-
throw new Error(body.message ?? `Cloud save stream failed: ${response.status}`);
|
|
6110
|
-
}
|
|
6111
|
-
|
|
6112
|
-
const reader = response.body.getReader();
|
|
6113
|
-
const decoder = new TextDecoder();
|
|
6114
|
-
let buffer = '';
|
|
6115
|
-
let receivedDone = false;
|
|
6116
|
-
|
|
6117
|
-
while (true) {
|
|
6118
|
-
const { value, done } = await reader.read();
|
|
6119
|
-
if (!done) {
|
|
6120
|
-
buffer += decoder.decode(value, { stream: true });
|
|
6121
|
-
} else {
|
|
6122
|
-
buffer += decoder.decode();
|
|
6123
|
-
}
|
|
6124
|
-
|
|
6125
|
-
const chunks = buffer.split('\n\n');
|
|
6126
|
-
buffer = done ? '' : (chunks.pop() ?? '');
|
|
6127
|
-
|
|
6128
|
-
for (const chunk of chunks) {
|
|
6129
|
-
const parsed = parseSseEventBlock(chunk);
|
|
6130
|
-
if (!parsed) continue;
|
|
6131
|
-
if (!parsed.data) continue;
|
|
6132
|
-
|
|
6133
|
-
if (parsed.event === 'step') {
|
|
6134
|
-
const payload = JSON.parse(parsed.data) as SaveStreamStepEvent;
|
|
6135
|
-
input.onStep(payload);
|
|
6136
|
-
} else if (parsed.event === 'log') {
|
|
6137
|
-
const payload = JSON.parse(parsed.data) as SaveStreamLogEvent;
|
|
6138
|
-
input.onLog?.(payload);
|
|
6139
|
-
} else if (parsed.event === 'error') {
|
|
6140
|
-
const payload = JSON.parse(parsed.data) as SaveStreamErrorEvent;
|
|
6141
|
-
throw new Error(payload.message ?? 'Cloud save failed.');
|
|
6142
|
-
} else if (parsed.event === 'done') {
|
|
6143
|
-
const payload = JSON.parse(parsed.data) as SaveStreamDoneEvent;
|
|
6144
|
-
input.onDone(payload);
|
|
6145
|
-
receivedDone = true;
|
|
6146
|
-
}
|
|
6147
|
-
}
|
|
6148
|
-
|
|
6149
|
-
if (done) break;
|
|
6150
|
-
}
|
|
6151
|
-
|
|
6152
|
-
if (!receivedDone) {
|
|
6153
|
-
throw new Error('Cloud save stream ended before completion.');
|
|
6154
|
-
}
|
|
6155
|
-
}
|
|
6156
|
-
|
|
6157
|
-
END_OF_FILE_CONTENT
|
|
6158
5477
|
# SKIP: src/lib/cloudSaveStream.ts:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
6159
5478
|
# SKIP: src/lib/cloudSaveStream.ts:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
6160
|
-
echo "Creating src/lib/deploySteps.ts..."
|
|
6161
|
-
cat << 'END_OF_FILE_CONTENT' > "src/lib/deploySteps.ts"
|
|
6162
|
-
import type { DeployStep } from '@/types/deploy';
|
|
6163
|
-
|
|
6164
|
-
export const DEPLOY_STEPS: readonly DeployStep[] = [
|
|
6165
|
-
{
|
|
6166
|
-
id: 'commit',
|
|
6167
|
-
label: 'Commit',
|
|
6168
|
-
verb: 'Committing',
|
|
6169
|
-
poem: ['Crystallizing your edit', 'into permanent history.'],
|
|
6170
|
-
color: '#60a5fa',
|
|
6171
|
-
glyph: '◈',
|
|
6172
|
-
duration: 2200,
|
|
6173
|
-
},
|
|
6174
|
-
{
|
|
6175
|
-
id: 'push',
|
|
6176
|
-
label: 'Push',
|
|
6177
|
-
verb: 'Pushing',
|
|
6178
|
-
poem: ['Sending your vision', 'across the wire.'],
|
|
6179
|
-
color: '#a78bfa',
|
|
6180
|
-
glyph: '◎',
|
|
6181
|
-
duration: 2800,
|
|
6182
|
-
},
|
|
6183
|
-
{
|
|
6184
|
-
id: 'build',
|
|
6185
|
-
label: 'Build',
|
|
6186
|
-
verb: 'Building',
|
|
6187
|
-
poem: ['Assembling the pieces,', 'brick by digital brick.'],
|
|
6188
|
-
color: '#f59e0b',
|
|
6189
|
-
glyph: '⬡',
|
|
6190
|
-
duration: 7500,
|
|
6191
|
-
},
|
|
6192
|
-
{
|
|
6193
|
-
id: 'live',
|
|
6194
|
-
label: 'Live',
|
|
6195
|
-
verb: 'Going live',
|
|
6196
|
-
poem: ['Your content', 'is now breathing.'],
|
|
6197
|
-
color: '#34d399',
|
|
6198
|
-
glyph: '✦',
|
|
6199
|
-
duration: 1600,
|
|
6200
|
-
},
|
|
6201
|
-
] as const;
|
|
6202
|
-
|
|
6203
|
-
END_OF_FILE_CONTENT
|
|
6204
5479
|
# SKIP: src/lib/deploySteps.ts:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
6205
5480
|
# SKIP: src/lib/deploySteps.ts:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
6206
5481
|
echo "Creating src/lib/draftStorage.ts..."
|
|
@@ -6235,24 +5510,37 @@ END_OF_FILE_CONTENT
|
|
|
6235
5510
|
echo "Creating src/lib/getFilePages.ts..."
|
|
6236
5511
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/getFilePages.ts"
|
|
6237
5512
|
/**
|
|
6238
|
-
* Page registry from src/data/pages
|
|
6239
|
-
* Add a
|
|
5513
|
+
* Page registry loaded from nested JSON files under src/data/pages.
|
|
5514
|
+
* Add a JSON file in that directory tree to register a page; no manual list in App.tsx.
|
|
6240
5515
|
*/
|
|
6241
5516
|
import type { PageConfig } from '@/types';
|
|
6242
5517
|
|
|
6243
5518
|
function slugFromPath(filePath: string): string {
|
|
6244
|
-
const
|
|
6245
|
-
const
|
|
6246
|
-
|
|
5519
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
5520
|
+
const match = normalizedPath.match(/\/data\/pages\/(.+)\.json$/i);
|
|
5521
|
+
const rawSlug = match?.[1] ?? normalizedPath.split('/').pop()?.replace(/\.json$/i, '') ?? '';
|
|
5522
|
+
const canonical = rawSlug
|
|
5523
|
+
.split('/')
|
|
5524
|
+
.map((segment) => segment.trim())
|
|
5525
|
+
.filter(Boolean)
|
|
5526
|
+
.join('/');
|
|
5527
|
+
return canonical || 'home';
|
|
6247
5528
|
}
|
|
6248
5529
|
|
|
6249
5530
|
export function getFilePages(): Record<string, PageConfig> {
|
|
6250
|
-
const glob = import.meta.glob<{ default: unknown }>('@/data/pages
|
|
5531
|
+
const glob = import.meta.glob<{ default: unknown }>('@/data/pages/**/*.json', { eager: true });
|
|
6251
5532
|
const bySlug = new Map<string, PageConfig>();
|
|
6252
|
-
|
|
5533
|
+
const entries = Object.entries(glob).sort(([a], [b]) => a.localeCompare(b));
|
|
5534
|
+
for (const [path, mod] of entries) {
|
|
6253
5535
|
const slug = slugFromPath(path);
|
|
6254
5536
|
const raw = mod?.default;
|
|
6255
|
-
if (raw == null || typeof raw !== 'object')
|
|
5537
|
+
if (raw == null || typeof raw !== 'object') {
|
|
5538
|
+
console.warn(`[tenant-alpha:getFilePages] Ignoring invalid page module at "${path}".`);
|
|
5539
|
+
continue;
|
|
5540
|
+
}
|
|
5541
|
+
if (bySlug.has(slug)) {
|
|
5542
|
+
console.warn(`[tenant-alpha:getFilePages] Duplicate slug "${slug}" at "${path}". Keeping latest match.`);
|
|
5543
|
+
}
|
|
6256
5544
|
bySlug.set(slug, raw as PageConfig);
|
|
6257
5545
|
}
|
|
6258
5546
|
const slugs = Array.from(bySlug.keys()).sort((a, b) =>
|
|
@@ -6290,6 +5578,7 @@ import { GitSectionSchema } from '@/components/git-section';
|
|
|
6290
5578
|
import { DevexSchema } from '@/components/devex';
|
|
6291
5579
|
import { CliSectionSchema } from '@/components/cli-section';
|
|
6292
5580
|
import { DocsLayoutSchema } from '@/components/docs-layout';
|
|
5581
|
+
import { TiptapSchema } from '@/components/tiptap';
|
|
6293
5582
|
|
|
6294
5583
|
export const SECTION_SCHEMAS = {
|
|
6295
5584
|
'header': HeaderSchema,
|
|
@@ -6310,6 +5599,7 @@ export const SECTION_SCHEMAS = {
|
|
|
6310
5599
|
'devex': DevexSchema,
|
|
6311
5600
|
'cli-section': CliSectionSchema,
|
|
6312
5601
|
'docs-layout': DocsLayoutSchema,
|
|
5602
|
+
'tiptap': TiptapSchema,
|
|
6313
5603
|
} as const;
|
|
6314
5604
|
|
|
6315
5605
|
export type SectionType = keyof typeof SECTION_SCHEMAS;
|
|
@@ -6346,6 +5636,24 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
6346
5636
|
|
|
6347
5637
|
END_OF_FILE_CONTENT
|
|
6348
5638
|
# SKIP: src/main.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
5639
|
+
echo "Creating src/main_.tsx..."
|
|
5640
|
+
cat << 'END_OF_FILE_CONTENT' > "src/main_.tsx"
|
|
5641
|
+
import '@/types'; // TBP: load type augmentation from capsule-driven types
|
|
5642
|
+
import React from 'react';
|
|
5643
|
+
import ReactDOM from 'react-dom/client';
|
|
5644
|
+
import App from './App';
|
|
5645
|
+
// ... resto del file
|
|
5646
|
+
|
|
5647
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
5648
|
+
<React.StrictMode>
|
|
5649
|
+
<App />
|
|
5650
|
+
</React.StrictMode>
|
|
5651
|
+
);
|
|
5652
|
+
|
|
5653
|
+
|
|
5654
|
+
|
|
5655
|
+
|
|
5656
|
+
END_OF_FILE_CONTENT
|
|
6349
5657
|
# SKIP: src/registry-types.ts è un file binario e non può essere convertito in testo.
|
|
6350
5658
|
mkdir -p "src/server"
|
|
6351
5659
|
mkdir -p "src/types"
|
|
@@ -6370,6 +5678,7 @@ import type { GitSectionData, GitSectionSettings } from '@/component
|
|
|
6370
5678
|
import type { DevexData, DevexSettings } from '@/components/devex';
|
|
6371
5679
|
import type { CliSectionData, CliSectionSettings } from '@/components/cli-section';
|
|
6372
5680
|
import type { DocsLayoutData, DocsLayoutSettings } from '@/components/docs-layout';
|
|
5681
|
+
import type { TiptapData, TiptapSettings } from '@/components/tiptap';
|
|
6373
5682
|
|
|
6374
5683
|
export type SectionComponentPropsMap = {
|
|
6375
5684
|
'header': { data: HeaderData; settings?: HeaderSettings; menu: MenuItem[] };
|
|
@@ -6390,6 +5699,7 @@ export type SectionComponentPropsMap = {
|
|
|
6390
5699
|
'devex': { data: DevexData; settings?: DevexSettings };
|
|
6391
5700
|
'cli-section': { data: CliSectionData; settings?: CliSectionSettings };
|
|
6392
5701
|
'docs-layout': { data: DocsLayoutData; settings?: DocsLayoutSettings };
|
|
5702
|
+
'tiptap': { data: TiptapData; settings?: TiptapSettings };
|
|
6393
5703
|
};
|
|
6394
5704
|
|
|
6395
5705
|
declare module '@jsonpages/core' {
|
|
@@ -6412,6 +5722,7 @@ declare module '@jsonpages/core' {
|
|
|
6412
5722
|
'devex': DevexData;
|
|
6413
5723
|
'cli-section': CliSectionData;
|
|
6414
5724
|
'docs-layout': DocsLayoutData;
|
|
5725
|
+
'tiptap': TiptapData;
|
|
6415
5726
|
}
|
|
6416
5727
|
export interface SectionSettingsRegistry {
|
|
6417
5728
|
'header': HeaderSettings;
|
|
@@ -6432,30 +5743,12 @@ declare module '@jsonpages/core' {
|
|
|
6432
5743
|
'devex': DevexSettings;
|
|
6433
5744
|
'cli-section': CliSectionSettings;
|
|
6434
5745
|
'docs-layout': DocsLayoutSettings;
|
|
5746
|
+
'tiptap': TiptapSettings;
|
|
6435
5747
|
}
|
|
6436
5748
|
}
|
|
6437
5749
|
|
|
6438
5750
|
export * from '@jsonpages/core';
|
|
6439
5751
|
|
|
6440
|
-
END_OF_FILE_CONTENT
|
|
6441
|
-
echo "Creating src/types/deploy.ts..."
|
|
6442
|
-
cat << 'END_OF_FILE_CONTENT' > "src/types/deploy.ts"
|
|
6443
|
-
export type StepId = 'commit' | 'push' | 'build' | 'live';
|
|
6444
|
-
|
|
6445
|
-
export type StepState = 'pending' | 'active' | 'done';
|
|
6446
|
-
|
|
6447
|
-
export type DeployPhase = 'idle' | 'running' | 'done' | 'error';
|
|
6448
|
-
|
|
6449
|
-
export interface DeployStep {
|
|
6450
|
-
id: StepId;
|
|
6451
|
-
label: string;
|
|
6452
|
-
verb: string;
|
|
6453
|
-
poem: [string, string];
|
|
6454
|
-
color: string;
|
|
6455
|
-
glyph: string;
|
|
6456
|
-
duration: number;
|
|
6457
|
-
}
|
|
6458
|
-
|
|
6459
5752
|
END_OF_FILE_CONTENT
|
|
6460
5753
|
# SKIP: src/types/deploy.ts:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
6461
5754
|
echo "Creating src/vite-env.d.ts..."
|
|
@@ -6469,26 +5762,4 @@ declare module '*?inline' {
|
|
|
6469
5762
|
|
|
6470
5763
|
|
|
6471
5764
|
|
|
6472
|
-
END_OF_FILE_CONTENT
|
|
6473
|
-
echo "Creating vercel.json..."
|
|
6474
|
-
cat << 'END_OF_FILE_CONTENT' > "vercel.json"
|
|
6475
|
-
{
|
|
6476
|
-
"rewrites": [
|
|
6477
|
-
{
|
|
6478
|
-
"source": "/(.*)",
|
|
6479
|
-
"destination": "/index.html"
|
|
6480
|
-
}
|
|
6481
|
-
],
|
|
6482
|
-
"headers": [
|
|
6483
|
-
{
|
|
6484
|
-
"source": "/assets/(.*)",
|
|
6485
|
-
"headers": [
|
|
6486
|
-
{
|
|
6487
|
-
"key": "Cache-Control",
|
|
6488
|
-
"value": "public, max-age=31536000, immutable"
|
|
6489
|
-
}
|
|
6490
|
-
]
|
|
6491
|
-
}
|
|
6492
|
-
]
|
|
6493
|
-
}
|
|
6494
5765
|
END_OF_FILE_CONTENT
|