@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.
@@ -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 { useCallback, useEffect, useRef, useState } from 'react';
35
+ import { useState, useEffect } from 'react';
36
36
  import { JsonPagesEngine } from '@jsonpages/core';
37
- import type { JsonPagesConfig, LibraryImageEntry, ProjectState } from '@jsonpages/core';
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
- import type { DeployPhase, StepId } from '@/types/deploy';
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
- useEffect(() => {
113
- return () => {
114
- activeCloudSaveController.current?.abort();
115
- };
116
- }, []);
117
-
118
- const runCloudSave = useCallback(
119
- async (
120
- payload: { state: ProjectState; slug: string },
121
- rejectOnError: boolean
122
- ): Promise<void> => {
123
- if (!CLOUD_API_URL || !CLOUD_API_KEY) {
124
- const noCloudError = new Error('Cloud mode is not configured.');
125
- if (rejectOnError) throw noCloudError;
126
- return;
127
- }
128
-
129
- pendingCloudSave.current = payload;
130
- activeCloudSaveController.current?.abort();
131
- const controller = new AbortController();
132
- activeCloudSaveController.current = controller;
133
-
134
- setCloudSaveUi({
135
- isOpen: true,
136
- phase: 'running',
137
- currentStepId: null,
138
- doneSteps: [],
139
- progress: 0,
140
- });
141
-
142
- try {
143
- await startCloudSaveStream({
144
- apiBaseUrl: CLOUD_API_URL,
145
- apiKey: CLOUD_API_KEY,
146
- path: `src/data/pages/${payload.slug}.json`,
147
- content: payload.state.page,
148
- message: `Content update for ${payload.slug} via Visual Editor`,
149
- signal: controller.signal,
150
- onStep: (event) => {
151
- setCloudSaveUi((prev) => {
152
- if (event.status === 'running') {
153
- return {
154
- ...prev,
155
- isOpen: true,
156
- phase: 'running',
157
- currentStepId: event.id,
158
- errorMessage: undefined,
159
- };
160
- }
161
-
162
- if (prev.doneSteps.includes(event.id)) {
163
- return prev;
164
- }
165
-
166
- const nextDone = [...prev.doneSteps, event.id];
167
- return {
168
- ...prev,
169
- isOpen: true,
170
- phase: 'running',
171
- currentStepId: event.id,
172
- doneSteps: nextDone,
173
- progress: stepProgress(nextDone),
174
- };
175
- });
176
- },
177
- onDone: (event) => {
178
- const completed = DEPLOY_STEPS.map((step) => step.id);
179
- setCloudSaveUi({
180
- isOpen: true,
181
- phase: 'done',
182
- currentStepId: 'live',
183
- doneSteps: completed,
184
- progress: 100,
185
- deployUrl: event.deployUrl,
186
- });
187
- },
188
- });
189
- } catch (error: unknown) {
190
- const message = error instanceof Error ? error.message : 'Cloud save failed.';
191
- setCloudSaveUi((prev) => ({
192
- ...prev,
193
- isOpen: true,
194
- phase: 'error',
195
- errorMessage: message,
196
- }));
197
- if (rejectOnError) throw new Error(message);
198
- } finally {
199
- if (activeCloudSaveController.current === controller) {
200
- activeCloudSaveController.current = null;
201
- }
202
- }
203
- },
204
- []
205
- );
206
-
207
- const closeCloudDrawer = useCallback(() => {
208
- setCloudSaveUi(getInitialCloudSaveUiState());
209
- }, []);
210
-
211
- const retryCloudSave = useCallback(() => {
212
- if (!pendingCloudSave.current) return;
213
- void runCloudSave(pendingCloudSave.current, false);
214
- }, [runCloudSave]);
215
-
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 (isCloudMode) {
230
- await runCloudSave({ state, slug }, true);
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/_App.tsx..."
299
- cat << 'END_OF_FILE_CONTENT' > "src/_App.tsx"
300
- /**
301
- * Thin Entry Point (Tenant).
302
- * Data from getHydratedData (file-backed or draft); assets from public/assets/images.
303
- * Supports Hybrid Persistence: Local Filesystem (Dev) or Cloud Bridge (Prod).
304
- */
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: SECTION_SCHEMAS as unknown as JsonPagesConfig['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
- echo "Creating src/components/save-drawer/DeployNode.tsx..."
3284
- cat << 'END_OF_FILE_CONTENT' > "src/components/save-drawer/DeployNode.tsx"
3285
- import type { CSSProperties } from 'react';
3286
- import type { DeployStep, StepState } from '@/types/deploy';
3287
-
3288
- interface DeployNodeProps {
3289
- step: DeployStep;
3290
- state: StepState;
3291
- }
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
- export function DeployNode({ step, state }: DeployNodeProps) {
3294
- const isActive = state === 'active';
3295
- const isDone = state === 'done';
3296
- const isPending = state === 'pending';
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
- return (
3299
- <div className="jp-drawer-node-wrap">
3300
- <div
3301
- className={`jp-drawer-node ${isPending ? 'jp-drawer-node-pending' : ''}`}
3302
- style={
3303
- {
3304
- background: isDone ? step.color : isActive ? 'rgba(0,0,0,0.5)' : undefined,
3305
- borderWidth: isDone ? 0 : 1,
3306
- borderColor: isActive ? `${step.color}80` : undefined,
3307
- boxShadow: isDone
3308
- ? `0 0 20px ${step.color}55, 0 0 40px ${step.color}22`
3309
- : isActive
3310
- ? `0 0 14px ${step.color}33`
3311
- : undefined,
3312
- animation: isActive ? 'node-glow 2s ease infinite' : undefined,
3313
- ['--glow-color' as string]: step.color,
3314
- } as CSSProperties
3315
- }
3316
- >
3317
- {isDone && (
3318
- <svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" aria-label="Done">
3319
- <path
3320
- className="stroke-dash-30 animate-check-draw"
3321
- d="M5 13l4 4L19 7"
3322
- stroke="#0a0f1a"
3323
- strokeWidth="2.5"
3324
- strokeLinecap="round"
3325
- strokeLinejoin="round"
3326
- />
3327
- </svg>
3328
- )}
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
- {isActive && (
3331
- <span
3332
- className="jp-drawer-node-glyph jp-drawer-node-glyph-active"
3333
- style={{ color: step.color, animation: 'glyph-rotate 9s linear infinite' }}
3334
- aria-hidden
3335
- >
3336
- {step.glyph}
3337
- </span>
3338
- )}
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
- {isPending && (
3341
- <span className="jp-drawer-node-glyph jp-drawer-node-glyph-pending" aria-hidden>
3342
- {step.glyph}
3343
- </span>
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
- {isActive && (
3347
- <span
3348
- className="jp-drawer-node-ring"
3349
- style={{
3350
- inset: -7,
3351
- borderColor: `${step.color}50`,
3352
- animation: 'ring-expand 2s ease-out infinite',
3353
- }}
3354
- />
3355
- )}
3356
- </div>
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
- <span
3359
- className="jp-drawer-node-label"
3360
- style={{ color: isDone ? step.color : isActive ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.18)' }}
3361
- >
3362
- {step.label}
3363
- </span>
3364
- </div>
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
- END_OF_FILE_CONTENT
3369
- # SKIP: src/components/save-drawer/DeployNode.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
3370
- # SKIP: src/components/save-drawer/DeployNode.tsx:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
3371
- echo "Creating src/components/save-drawer/DopaDrawer.tsx..."
3372
- cat << 'END_OF_FILE_CONTENT' > "src/components/save-drawer/DopaDrawer.tsx"
3373
- import { useEffect, useMemo, useState } from 'react';
3374
- import { createPortal } from 'react-dom';
3375
- import type { StepId, StepState } from '@/types/deploy';
3376
- import { DEPLOY_STEPS } from '@/lib/deploySteps';
3377
- import fontsCss from '@/fonts.css?inline';
3378
- import saverStyleCss from './saverStyle.css?inline';
3379
- import { DeployNode } from './DeployNode';
3380
- import { DeployConnector } from './DeployConnector';
3381
- import { BuildBars, ElapsedTimer, Particles, SuccessBurst } from './Visuals';
3382
-
3383
- interface DopaDrawerProps {
3384
- isOpen: boolean;
3385
- phase: 'idle' | 'running' | 'done' | 'error';
3386
- currentStepId: StepId | null;
3387
- doneSteps: StepId[];
3388
- progress: number;
3389
- errorMessage?: string;
3390
- deployUrl?: string;
3391
- onClose: () => void;
3392
- onRetry: () => void;
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
- export function DopaDrawer({
3396
- isOpen,
3397
- phase,
3398
- currentStepId,
3399
- doneSteps,
3400
- progress,
3401
- errorMessage,
3402
- deployUrl,
3403
- onClose,
3404
- onRetry,
3405
- }: DopaDrawerProps) {
3406
- const [shadowMount, setShadowMount] = useState<HTMLElement | null>(null);
3407
- const [burst, setBurst] = useState(false);
3408
- const [countdown, setCountdown] = useState(3);
3409
-
3410
- const isRunning = phase === 'running';
3411
- const isDone = phase === 'done';
3412
- const isError = phase === 'error';
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
- useEffect(() => {
3415
- const host = document.createElement('div');
3416
- host.setAttribute('data-jp-drawer-shadow-host', '');
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
- const shadowRoot = host.attachShadow({ mode: 'open' });
3419
- const style = document.createElement('style');
3420
- style.textContent = `${fontsCss}\n${saverStyleCss}`;
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
- const mount = document.createElement('div');
3423
- shadowRoot.append(style, mount);
3396
+ React.useEffect(() => {
3397
+ const sectionEl = editorHostRef.current?.closest('[data-section-id]') as HTMLElement | null;
3398
+ rootRef.current = sectionEl;
3399
+ }, []);
3424
3400
 
3425
- document.body.appendChild(host);
3426
- setShadowMount(mount);
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
- setShadowMount(null);
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
- useEffect(() => {
3435
- if (!isOpen) {
3436
- setBurst(false);
3437
- setCountdown(3);
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
- if (isDone) setBurst(true);
3441
- }, [isDone, isOpen]);
3442
-
3443
- useEffect(() => {
3444
- if (!isOpen || !isDone) return;
3445
- setCountdown(3);
3446
- const interval = window.setInterval(() => {
3447
- setCountdown((prev) => {
3448
- if (prev <= 1) {
3449
- window.clearInterval(interval);
3450
- onClose();
3451
- return 0;
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
- return prev - 1;
3454
- });
3455
- }, 1000);
3456
- return () => window.clearInterval(interval);
3457
- }, [isDone, isOpen, onClose]);
3458
-
3459
- const currentStep = useMemo(
3460
- () => DEPLOY_STEPS.find((step) => step.id === currentStepId) ?? null,
3461
- [currentStepId]
3462
- );
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
- if (!shadowMount || !isOpen || phase === 'idle') return null;
3475
-
3476
- return createPortal(
3477
- <div className="jp-drawer-root">
3478
- <div
3479
- className="jp-drawer-overlay animate-fade-in"
3480
- onClick={isDone || isError ? onClose : undefined}
3481
- aria-hidden
3482
- />
3483
-
3484
- <div
3485
- role="status"
3486
- aria-live="polite"
3487
- aria-label={isDone ? 'Deploy completed' : isError ? 'Deploy failed' : 'Deploying'}
3488
- className="jp-drawer-shell animate-drawer-up"
3489
- style={{ bottom: 'max(2.25rem, env(safe-area-inset-bottom))' }}
3490
- >
3488
+ return (
3489
+ <div
3490
+ ref={editorHostRef}
3491
+ data-jp-field="content"
3492
+ className="space-y-3"
3493
+ >
3494
+ {editor && (
3491
3495
  <div
3492
- className="jp-drawer-card"
3493
- style={{
3494
- backgroundColor: 'hsl(222 18% 7%)',
3495
- boxShadow: `0 0 0 1px rgba(255,255,255,0.04), 0 -20px 60px rgba(0,0,0,0.6), 0 0 80px ${activeColor}0d`,
3496
- transition: 'box-shadow 1.2s ease',
3497
- }}
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
- className="jp-drawer-ambient"
3501
- style={{
3502
- background: `radial-gradient(ellipse 70% 60% at 50% 110%, ${activeColor}12 0%, transparent 65%)`,
3503
- transition: 'background 1.5s ease',
3504
- animation: 'ambient-pulse 3.5s ease infinite',
3505
- }}
3506
- aria-hidden
3507
- />
3508
-
3509
- {isDone && (
3510
- <div className="jp-drawer-shimmer" aria-hidden>
3511
- <div
3512
- className="jp-drawer-shimmer-bar"
3513
- style={{
3514
- background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.04), transparent)',
3515
- animation: 'shimmer-sweep 1.4s 0.1s ease forwards',
3516
- }}
3517
- />
3518
- </div>
3519
- )}
3520
-
3521
- <Particles count={particleCount} color={activeColor} />
3522
- {burst && <SuccessBurst />}
3523
-
3524
- <div className="jp-drawer-content">
3525
- <div className="jp-drawer-header">
3526
- <div className="jp-drawer-header-left">
3527
- <div className="jp-drawer-status" style={{ color: activeColor }}>
3528
- <span
3529
- className="jp-drawer-status-dot"
3530
- style={{
3531
- background: activeColor,
3532
- boxShadow: `0 0 6px ${activeColor}`,
3533
- animation: isRunning ? 'ambient-pulse 1.5s ease infinite' : 'none',
3534
- }}
3535
- aria-hidden
3536
- />
3537
- {isDone ? 'Live' : isError ? 'Build failed' : currentStep?.verb ?? 'Saving'}
3538
- </div>
3539
-
3540
- <div key={currentStep?.id ?? phase} className="jp-drawer-copy animate-text-in">
3541
- {isDone ? (
3542
- <div className="animate-success-pop">
3543
- <p className="jp-drawer-copy-title jp-drawer-copy-title-lg">Your content is live.</p>
3544
- <p className="jp-drawer-copy-sub">Deployed to production successfully</p>
3545
- </div>
3546
- ) : isError ? (
3547
- <>
3548
- <p className="jp-drawer-copy-title jp-drawer-copy-title-md">Deploy failed at build.</p>
3549
- <p className="jp-drawer-copy-sub jp-drawer-copy-sub-error">{errorMessage ?? 'Check your Vercel logs or retry below'}</p>
3550
- </>
3551
- ) : currentStep ? (
3552
- <>
3553
- <p className="jp-drawer-poem-line jp-drawer-poem-line-1">{currentStep.poem[0]}</p>
3554
- <p className="jp-drawer-poem-line jp-drawer-poem-line-2">{currentStep.poem[1]}</p>
3555
- </>
3556
- ) : null}
3557
- </div>
3558
- </div>
3559
-
3560
- <div className="jp-drawer-right">
3561
- {isDone ? (
3562
- <div className="jp-drawer-countdown-wrap animate-fade-up">
3563
- <span className="jp-drawer-countdown-text" aria-live="polite">
3564
- Chiusura in {countdown}s
3565
- </span>
3566
- <div className="jp-drawer-countdown-track">
3567
- <div className="jp-drawer-countdown-bar countdown-bar" style={{ boxShadow: '0 0 6px #34d39988' }} />
3568
- </div>
3569
- </div>
3570
- ) : (
3571
- <ElapsedTimer running={isRunning} />
3572
- )}
3573
- </div>
3574
- </div>
3575
-
3576
- <div className="jp-drawer-track-row">
3577
- {DEPLOY_STEPS.map((step, i) => (
3578
- <div key={step.id} style={{ display: 'flex', alignItems: 'center', flex: i < DEPLOY_STEPS.length - 1 ? 1 : 'none' }}>
3579
- <DeployNode step={step} state={stepState(i)} />
3580
- {i < DEPLOY_STEPS.length - 1 && (
3581
- <DeployConnector fromState={stepState(i)} toState={stepState(i + 1)} color={DEPLOY_STEPS[i + 1].color} />
3582
- )}
3583
- </div>
3584
- ))}
3585
- </div>
3586
-
3587
- <div className="jp-drawer-bars-wrap">
3588
- <BuildBars active={stepState(2) === 'active'} />
3589
- </div>
3590
-
3591
- <div className="jp-drawer-separator" />
3592
-
3593
- <div className="jp-drawer-footer">
3594
- <div className="jp-drawer-progress">
3595
- <div
3596
- className="jp-drawer-progress-indicator"
3597
- style={{
3598
- width: `${Math.max(0, Math.min(100, progress))}%`,
3599
- background: `linear-gradient(90deg, ${DEPLOY_STEPS[0].color}, ${activeColor})`,
3600
- }}
3601
- />
3602
- </div>
3603
-
3604
- <div className="jp-drawer-cta">
3605
- {isDone && (
3606
- <div className="jp-drawer-btn-row animate-fade-up">
3607
- <button type="button" className="jp-drawer-btn jp-drawer-btn-secondary" onClick={onClose}>
3608
- Chiudi
3609
- </button>
3610
- <button
3611
- type="button"
3612
- className="jp-drawer-btn jp-drawer-btn-emerald"
3613
- onClick={() => {
3614
- if (deployUrl) window.open(deployUrl, '_blank', 'noopener,noreferrer');
3615
- }}
3616
- disabled={!deployUrl}
3617
- >
3618
- <span aria-hidden>↗</span> Open site
3619
- </button>
3620
- </div>
3621
- )}
3622
-
3623
- {isError && (
3624
- <div className="jp-drawer-btn-row animate-fade-up">
3625
- <button type="button" className="jp-drawer-btn jp-drawer-btn-ghost" onClick={onClose}>
3626
- Annulla
3627
- </button>
3628
- <button type="button" className="jp-drawer-btn jp-drawer-btn-destructive" onClick={onRetry}>
3629
- Retry
3630
- </button>
3631
- </div>
3632
- )}
3633
-
3634
- {isRunning && (
3635
- <span className="jp-drawer-running-step" aria-hidden>
3636
- {doneSteps.length + 1} / {DEPLOY_STEPS.length}
3637
- </span>
3638
- )}
3639
- </div>
3640
- </div>
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
- </div>
3644
- </div>,
3645
- shadowMount
3646
- );
3647
- }
3648
-
3649
- END_OF_FILE_CONTENT
3650
- # SKIP: src/components/save-drawer/DopaDrawer.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
3651
- # SKIP: src/components/save-drawer/DopaDrawer.tsx:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
3652
- echo "Creating src/components/save-drawer/Visuals.tsx..."
3653
- cat << 'END_OF_FILE_CONTENT' > "src/components/save-drawer/Visuals.tsx"
3654
- import { useEffect, useRef, useState } from 'react';
3655
- import type { CSSProperties } from 'react';
3656
-
3657
- interface Particle {
3658
- id: number;
3659
- x: number;
3660
- y: number;
3661
- size: number;
3662
- dur: number;
3663
- delay: number;
3664
- }
3665
-
3666
- const PARTICLE_POOL: Particle[] = Array.from({ length: 44 }, (_, i) => ({
3667
- id: i,
3668
- x: 5 + Math.random() * 90,
3669
- y: 15 + Math.random() * 70,
3670
- size: 1.5 + Math.random() * 2.5,
3671
- dur: 2.8 + Math.random() * 3.5,
3672
- delay: Math.random() * 4,
3673
- }));
3674
-
3675
- interface ParticlesProps {
3676
- count: number;
3677
- color: string;
3678
- }
3679
-
3680
- export function Particles({ count, color }: ParticlesProps) {
3681
- return (
3682
- <div className="jp-drawer-particles" aria-hidden>
3683
- {PARTICLE_POOL.slice(0, count).map((particle) => (
3684
- <div
3685
- key={particle.id}
3686
- className="jp-drawer-particle"
3687
- style={{
3688
- left: `${particle.x}%`,
3689
- bottom: `${particle.y}%`,
3690
- width: particle.size,
3691
- height: particle.size,
3692
- background: color,
3693
- boxShadow: `0 0 ${particle.size * 3}px ${color}`,
3694
- opacity: 0,
3695
- animation: `particle-float ${particle.dur}s ${particle.delay}s ease-out infinite`,
3696
- }}
3697
- />
3698
- ))}
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
- return (
3713
- <div className="jp-drawer-bars" aria-hidden>
3714
- {BAR_H.map((height, i) => (
3715
- <div
3716
- key={i}
3717
- className="jp-drawer-bar"
3718
- style={{
3719
- height: `${height * 100}%`,
3720
- animation: `bar-eq ${0.42 + i * 0.06}s ${i * 0.04}s ease-in-out infinite alternate`,
3721
- }}
3722
- />
3723
- ))}
3724
- </div>
3725
- );
3726
- }
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 BURST_COLORS = ['#34d399', '#60a5fa', '#a78bfa', '#f59e0b', '#f472b6'];
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
- <div className="jp-drawer-burst" aria-hidden>
3733
- {Array.from({ length: 16 }).map((_, i) => (
3734
- <div
3735
- key={i}
3736
- className="jp-drawer-burst-dot"
3737
- style={
3738
- {
3739
- background: BURST_COLORS[i % BURST_COLORS.length],
3740
- ['--r' as string]: `${i * 22.5}deg`,
3741
- animation: `burst-ray 0.85s ${i * 0.03}s cubic-bezier(0,0.6,0.5,1) forwards`,
3742
- transform: `rotate(${i * 22.5}deg)`,
3743
- transformOrigin: '50% 50%',
3744
- opacity: 0,
3745
- } as CSSProperties
3746
- }
3747
- />
3748
- ))}
3749
- </div>
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
- # SKIP: src/components/save-drawer/Visuals.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
3785
- # SKIP: src/components/save-drawer/Visuals.tsx:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
3786
- echo "Creating src/components/save-drawer/saverStyle.css..."
3787
- cat << 'END_OF_FILE_CONTENT' > "src/components/save-drawer/saverStyle.css"
3788
- /* Save Drawer strict_full isolated stylesheet */
3789
-
3790
- .jp-drawer-root {
3791
- --background: 222 18% 6%;
3792
- --foreground: 210 20% 96%;
3793
- --card: 222 16% 8%;
3794
- --card-foreground: 210 20% 96%;
3795
- --primary: 0 0% 95%;
3796
- --primary-foreground: 222 18% 6%;
3797
- --secondary: 220 14% 13%;
3798
- --secondary-foreground: 210 20% 96%;
3799
- --destructive: 0 72% 51%;
3800
- --destructive-foreground: 0 0% 98%;
3801
- --border: 220 14% 13%;
3802
- --radius: 0.6rem;
3803
- font-family: 'Geist', system-ui, sans-serif;
3804
- }
3805
-
3806
- .jp-drawer-overlay {
3807
- position: fixed;
3808
- inset: 0;
3809
- z-index: 2147483600;
3810
- background: rgb(0 0 0 / 0.4);
3811
- backdrop-filter: blur(2px);
3812
- }
3813
-
3814
- .jp-drawer-shell {
3815
- position: fixed;
3816
- left: 0;
3817
- right: 0;
3818
- z-index: 2147483601;
3819
- display: flex;
3820
- justify-content: center;
3821
- padding: 0 1rem;
3822
- }
3823
-
3824
- .jp-drawer-card {
3825
- position: relative;
3826
- width: 100%;
3827
- max-width: 31rem;
3828
- overflow: hidden;
3829
- border-radius: 1rem;
3830
- border: 1px solid rgb(255 255 255 / 0.07);
3831
- }
3832
-
3833
- .jp-drawer-ambient {
3834
- position: absolute;
3835
- inset: 0;
3836
- pointer-events: none;
3837
- }
3838
-
3839
- .jp-drawer-shimmer {
3840
- position: absolute;
3841
- inset: 0;
3842
- overflow: hidden;
3843
- pointer-events: none;
3844
- }
3845
-
3846
- .jp-drawer-shimmer-bar {
3847
- position: absolute;
3848
- inset-block: 0;
3849
- width: 35%;
3850
- }
3851
-
3852
- .jp-drawer-content {
3853
- position: relative;
3854
- z-index: 10;
3855
- padding: 2rem 2rem 1.75rem;
3856
- }
3857
-
3858
- .jp-drawer-header {
3859
- margin-bottom: 1.5rem;
3860
- display: flex;
3861
- align-items: flex-start;
3862
- justify-content: space-between;
3863
- }
3864
-
3865
- .jp-drawer-header-left {
3866
- display: flex;
3867
- flex-direction: column;
3868
- gap: 0.625rem;
3869
- }
3870
-
3871
- .jp-drawer-status {
3872
- display: flex;
3873
- align-items: center;
3874
- gap: 0.5rem;
3875
- font-size: 0.75rem;
3876
- font-weight: 700;
3877
- text-transform: uppercase;
3878
- letter-spacing: 0.1em;
3879
- transition: color 0.5s;
3880
- }
3881
-
3882
- .jp-drawer-status-dot {
3883
- width: 0.375rem;
3884
- height: 0.375rem;
3885
- border-radius: 9999px;
3886
- display: inline-block;
3887
- }
3888
-
3889
- .jp-drawer-copy {
3890
- min-height: 52px;
3891
- }
3892
-
3893
- .jp-drawer-copy-title {
3894
- margin: 0;
3895
- color: white;
3896
- line-height: 1.25;
3897
- font-weight: 600;
3898
- }
3899
-
3900
- .jp-drawer-copy-title-lg {
3901
- font-size: 1.125rem;
3902
- }
3903
-
3904
- .jp-drawer-copy-title-md {
3905
- font-size: 1rem;
3906
- }
3907
-
3908
- .jp-drawer-copy-sub {
3909
- margin: 0.125rem 0 0;
3910
- color: rgb(255 255 255 / 0.4);
3911
- font-size: 0.875rem;
3912
- }
3913
-
3914
- .jp-drawer-copy-sub-error {
3915
- color: rgb(255 255 255 / 0.35);
3916
- }
3917
-
3918
- .jp-drawer-poem-line {
3919
- margin: 0;
3920
- font-size: 0.875rem;
3921
- font-weight: 300;
3922
- line-height: 1.5;
3923
- }
3924
-
3925
- .jp-drawer-poem-line-1 {
3926
- color: rgb(255 255 255 / 0.55);
3927
- }
3928
-
3929
- .jp-drawer-poem-line-2 {
3930
- color: rgb(255 255 255 / 0.3);
3931
- }
3932
-
3933
- .jp-drawer-right {
3934
- margin-left: 1.5rem;
3935
- display: flex;
3936
- flex-direction: column;
3937
- align-items: flex-end;
3938
- gap: 0.5rem;
3939
- flex-shrink: 0;
3940
- }
3941
-
3942
- .jp-drawer-countdown-wrap {
3943
- display: flex;
3944
- flex-direction: column;
3945
- align-items: flex-end;
3946
- gap: 0.5rem;
3947
- }
3948
-
3949
- .jp-drawer-countdown-text {
3950
- font-family: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
3951
- font-size: 0.75rem;
3952
- font-weight: 600;
3953
- color: #34d399;
3954
- }
3955
-
3956
- .jp-drawer-countdown-track {
3957
- width: 6rem;
3958
- height: 0.125rem;
3959
- border-radius: 9999px;
3960
- overflow: hidden;
3961
- background: rgb(255 255 255 / 0.1);
3962
- }
3963
-
3964
- .jp-drawer-countdown-bar {
3965
- width: 100%;
3966
- height: 100%;
3967
- border-radius: 9999px;
3968
- background: #34d399;
3969
- }
3970
-
3971
- .jp-drawer-track-row {
3972
- margin-bottom: 1rem;
3973
- display: flex;
3974
- align-items: center;
3975
- }
3976
-
3977
- .jp-drawer-bars-wrap {
3978
- margin-bottom: 1rem;
3979
- display: flex;
3980
- justify-content: center;
3981
- }
3982
-
3983
- .jp-drawer-separator {
3984
- margin-bottom: 1rem;
3985
- height: 1px;
3986
- width: 100%;
3987
- border: 0;
3988
- background: rgb(255 255 255 / 0.06);
3989
- }
3990
-
3991
- .jp-drawer-footer {
3992
- display: flex;
3993
- align-items: center;
3994
- gap: 1rem;
3995
- }
3996
-
3997
- .jp-drawer-progress {
3998
- flex: 1;
3999
- height: 2px;
4000
- border-radius: 9999px;
4001
- overflow: hidden;
4002
- background: rgb(255 255 255 / 0.06);
4003
- }
4004
-
4005
- .jp-drawer-progress-indicator {
4006
- height: 100%;
4007
- }
4008
-
4009
- .jp-drawer-cta {
4010
- display: flex;
4011
- align-items: center;
4012
- gap: 0.5rem;
4013
- flex-shrink: 0;
4014
- }
4015
-
4016
- .jp-drawer-running-step {
4017
- font-family: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
4018
- font-size: 0.75rem;
4019
- color: rgb(255 255 255 / 0.2);
4020
- }
4021
-
4022
- .jp-drawer-btn-row {
4023
- display: flex;
4024
- gap: 0.5rem;
4025
- }
4026
-
4027
- .jp-drawer-btn {
4028
- border: 1px solid transparent;
4029
- border-radius: 0.375rem;
4030
- font-size: 0.8125rem;
4031
- font-weight: 500;
4032
- line-height: 1;
4033
- height: 2.25rem;
4034
- padding: 0 0.75rem;
4035
- cursor: pointer;
4036
- transition: all 0.2s ease;
4037
- display: inline-flex;
4038
- align-items: center;
4039
- justify-content: center;
4040
- gap: 0.375rem;
4041
- }
4042
-
4043
- .jp-drawer-btn:disabled {
4044
- opacity: 0.5;
4045
- cursor: not-allowed;
4046
- }
4047
-
4048
- .jp-drawer-btn-secondary {
4049
- background: hsl(var(--secondary));
4050
- color: hsl(var(--secondary-foreground));
4051
- }
4052
-
4053
- .jp-drawer-btn-secondary:hover {
4054
- filter: brightness(1.08);
4055
- }
4056
-
4057
- .jp-drawer-btn-emerald {
4058
- background: #34d399;
4059
- color: #18181b;
4060
- font-weight: 600;
4061
- }
4062
-
4063
- .jp-drawer-btn-emerald:hover {
4064
- background: #6ee7b7;
4065
- }
4066
-
4067
- .jp-drawer-btn-ghost {
4068
- background: transparent;
4069
- color: rgb(255 255 255 / 0.9);
4070
- }
4071
-
4072
- .jp-drawer-btn-ghost:hover {
4073
- background: rgb(255 255 255 / 0.08);
4074
- }
4075
-
4076
- .jp-drawer-btn-destructive {
4077
- background: hsl(var(--destructive));
4078
- color: hsl(var(--destructive-foreground));
4079
- }
4080
-
4081
- .jp-drawer-btn-destructive:hover {
4082
- filter: brightness(1.06);
4083
- }
4084
-
4085
- .jp-drawer-node-wrap {
4086
- position: relative;
4087
- z-index: 10;
4088
- display: flex;
4089
- flex-direction: column;
4090
- align-items: center;
4091
- gap: 0.625rem;
4092
- }
4093
-
4094
- .jp-drawer-node {
4095
- position: relative;
4096
- width: 3rem;
4097
- height: 3rem;
4098
- border-radius: 9999px;
4099
- border: 1px solid transparent;
4100
- display: flex;
4101
- align-items: center;
4102
- justify-content: center;
4103
- transition: all 0.5s;
4104
- }
4105
-
4106
- .jp-drawer-node-pending {
4107
- border-color: rgb(255 255 255 / 0.08);
4108
- background: rgb(255 255 255 / 0.02);
4109
- }
4110
-
4111
- .jp-drawer-node-glyph {
4112
- font-size: 1.125rem;
4113
- line-height: 1;
4114
- }
4115
-
4116
- .jp-drawer-node-glyph-active {
4117
- display: inline-block;
4118
- }
4119
-
4120
- .jp-drawer-node-glyph-pending {
4121
- color: rgb(255 255 255 / 0.15);
4122
- }
4123
-
4124
- .jp-drawer-node-ring {
4125
- position: absolute;
4126
- border-radius: 9999px;
4127
- border: 1px solid transparent;
4128
- }
4129
-
4130
- .jp-drawer-node-label {
4131
- font-size: 10px;
4132
- font-weight: 600;
4133
- text-transform: uppercase;
4134
- letter-spacing: 0.1em;
4135
- transition: color 0.5s;
4136
- }
4137
-
4138
- .jp-drawer-connector {
4139
- position: relative;
4140
- z-index: 0;
4141
- flex: 1;
4142
- height: 2px;
4143
- margin-top: -24px;
4144
- }
4145
-
4146
- .jp-drawer-connector-base {
4147
- position: absolute;
4148
- inset: 0;
4149
- border-radius: 9999px;
4150
- background: rgb(255 255 255 / 0.08);
4151
- }
4152
-
4153
- .jp-drawer-connector-fill {
4154
- position: absolute;
4155
- left: 0;
4156
- right: auto;
4157
- top: 0;
4158
- bottom: 0;
4159
- border-radius: 9999px;
4160
- }
4161
-
4162
- .jp-drawer-connector-orb {
4163
- position: absolute;
4164
- top: 50%;
4165
- transform: translateY(-50%);
4166
- width: 10px;
4167
- height: 10px;
4168
- border-radius: 9999px;
4169
- }
4170
-
4171
- .jp-drawer-particles {
4172
- position: absolute;
4173
- inset: 0;
4174
- overflow: hidden;
4175
- pointer-events: none;
4176
- }
4177
-
4178
- .jp-drawer-particle {
4179
- position: absolute;
4180
- border-radius: 9999px;
4181
- }
4182
-
4183
- .jp-drawer-bars {
4184
- height: 1.75rem;
4185
- display: flex;
4186
- align-items: flex-end;
4187
- gap: 3px;
4188
- }
4189
-
4190
- .jp-drawer-bars-placeholder {
4191
- height: 1.75rem;
4192
- }
4193
-
4194
- .jp-drawer-bar {
4195
- width: 3px;
4196
- border-radius: 2px;
4197
- background: #f59e0b;
4198
- transform-origin: bottom;
4199
- }
4200
-
4201
- .jp-drawer-burst {
4202
- position: absolute;
4203
- inset: 0;
4204
- display: flex;
4205
- align-items: center;
4206
- justify-content: center;
4207
- pointer-events: none;
4208
- }
4209
-
4210
- .jp-drawer-burst-dot {
4211
- position: absolute;
4212
- width: 5px;
4213
- height: 5px;
4214
- border-radius: 9999px;
4215
- }
4216
-
4217
- .jp-drawer-elapsed {
4218
- font-family: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
4219
- font-size: 0.75rem;
4220
- letter-spacing: 0.1em;
4221
- color: rgb(255 255 255 / 0.25);
4222
- }
4223
-
4224
- /* Animation helper classes */
4225
- .animate-drawer-up { animation: drawer-up 0.45s cubic-bezier(0.22, 1, 0.36, 1) forwards; }
4226
- .animate-fade-in { animation: fade-in 0.25s ease forwards; }
4227
- .animate-fade-up { animation: fade-up 0.35s ease forwards; }
4228
- .animate-text-in { animation: text-in 0.3s ease forwards; }
4229
- .animate-success-pop { animation: success-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }
4230
- .countdown-bar { animation: countdown-drain 3s linear forwards; }
4231
-
4232
- .stroke-dash-30 {
4233
- stroke-dasharray: 30;
4234
- stroke-dashoffset: 30;
4235
- }
4236
-
4237
- .animate-check-draw {
4238
- animation: check-draw 0.4s 0.05s ease forwards;
4239
- }
4240
-
4241
- @keyframes check-draw {
4242
- to { stroke-dashoffset: 0; }
4243
- }
4244
-
4245
- @keyframes drawer-up {
4246
- from { transform: translateY(100%); opacity: 0; }
4247
- to { transform: translateY(0); opacity: 1; }
4248
- }
4249
-
4250
- @keyframes fade-in {
4251
- from { opacity: 0; }
4252
- to { opacity: 1; }
4253
- }
4254
-
4255
- @keyframes fade-up {
4256
- from { opacity: 0; transform: translateY(8px); }
4257
- to { transform: translateY(0); opacity: 1; }
4258
- }
4259
-
4260
- @keyframes text-in {
4261
- from { opacity: 0; transform: translateX(-6px); }
4262
- to { opacity: 1; transform: translateX(0); }
4263
- }
4264
-
4265
- @keyframes success-pop {
4266
- 0% { transform: scale(0.88); opacity: 0; }
4267
- 60% { transform: scale(1.04); }
4268
- 100% { transform: scale(1); opacity: 1; }
4269
- }
4270
-
4271
- @keyframes ambient-pulse {
4272
- 0%, 100% { opacity: 0.3; }
4273
- 50% { opacity: 0.65; }
4274
- }
4275
-
4276
- @keyframes shimmer-sweep {
4277
- from { transform: translateX(-100%); }
4278
- to { transform: translateX(250%); }
4279
- }
4280
-
4281
- @keyframes node-glow {
4282
- 0%, 100% { box-shadow: 0 0 12px var(--glow-color,#60a5fa55); }
4283
- 50% { box-shadow: 0 0 28px var(--glow-color,#60a5fa88), 0 0 48px var(--glow-color,#60a5fa22); }
4284
- }
4285
-
4286
- @keyframes glyph-rotate {
4287
- from { transform: rotate(0deg); }
4288
- to { transform: rotate(360deg); }
4289
- }
4290
-
4291
- @keyframes ring-expand {
4292
- from { transform: scale(1); opacity: 0.7; }
4293
- to { transform: scale(2.1); opacity: 0; }
4294
- }
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
- @keyframes orb-travel {
4297
- from { left: 0%; }
4298
- to { left: calc(100% - 10px); }
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
- @keyframes particle-float {
4302
- 0% { transform: translateY(0) scale(1); opacity: 0; }
4303
- 15% { opacity: 1; }
4304
- 100% { transform: translateY(-90px) scale(0.3); opacity: 0; }
4305
- }
3627
+ export const TiptapSchema = BaseSectionData.extend({
3628
+ content: z.string().default('').describe('ui:editorial-markdown'),
3629
+ });
4306
3630
 
4307
- @keyframes bar-eq {
4308
- from { transform: scaleY(0.4); }
4309
- to { transform: scaleY(1); }
4310
- }
3631
+ export const TiptapSettingsSchema = z.object({});
4311
3632
 
4312
- @keyframes burst-ray {
4313
- 0% { transform: rotate(var(--r, 0deg)) translateX(0); opacity: 1; }
4314
- 100% { transform: rotate(var(--r, 0deg)) translateX(56px); opacity: 0; }
4315
- }
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
- @keyframes countdown-drain {
4318
- from { width: 100%; }
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": "![Screenshot 2026-03-05 at 14-45-10 JsonPages.png](/assets/images/1772721027290-Screenshot_2026-03-05_at_14-45-10_JsonPages.png \"Screenshot 2026-03-05 at 14-45-10 JsonPages.png\")## 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/*.json.
6239
- * Add a .json file in that directory to add a page; no manual list in App.tsx.
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 base = filePath.split('/').pop() ?? filePath;
6245
- const name = base.replace(/\.json$/i, '').trim();
6246
- return name || 'page';
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/*.json', { eager: true });
5531
+ const glob = import.meta.glob<{ default: unknown }>('@/data/pages/**/*.json', { eager: true });
6251
5532
  const bySlug = new Map<string, PageConfig>();
6252
- for (const [path, mod] of Object.entries(glob)) {
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') continue;
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