@jsonpages/cli 3.0.63 → 3.0.65
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 +1534 -41
- package/package.json +1 -1
|
@@ -3,6 +3,555 @@ set -e # Termina se c'è un errore
|
|
|
3
3
|
|
|
4
4
|
echo "Inizio ricostruzione progetto..."
|
|
5
5
|
|
|
6
|
+
mkdir -p "docs"
|
|
7
|
+
echo "Creating docs/01-Onboarding_Client_completo_aggiornato.md..."
|
|
8
|
+
cat << 'END_OF_FILE_CONTENT' > "docs/01-Onboarding_Client_completo_aggiornato.md"
|
|
9
|
+
# Onboarding — Percorso Client (senza CMS) — Versione completa
|
|
10
|
+
|
|
11
|
+
**Per chi:** Sviluppo grafico e dati quando **non** usi il CMS (Studio/ICE). Il sito è un **client**: i dati arrivano da JSON locali, da API o da un CMS esterno; tu ti occupi di layout, design e rendering.
|
|
12
|
+
|
|
13
|
+
**Riferimento spec:** JSONPAGES Architecture v1.2 — solo le parti che riguardano struttura sito, componenti e dati. Ignori: Studio, ICE, Form Factory, IDAC, TOCC, AddSectionConfig, schema obbligatori per l'editor.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. Cosa fai tu (in sintesi)
|
|
18
|
+
|
|
19
|
+
- **Grafico:** Layout e stili delle section (View + CSS / design tokens se vuoi).
|
|
20
|
+
- **Dati:** Da dove prendono i dati le pagine (file JSON, API, altro CMS) e come vengono passati al motore (config + pages).
|
|
21
|
+
|
|
22
|
+
Non devi: tipizzare tutto per l'editor, esporre schema Zod al Form Factory, gestire overlay Studio, Add Section, ecc. Puoi usare tipi minimi o anche `unknown`/`any` sui dati se non ti serve type-safety forte.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 2. Struttura progetto (minima)
|
|
27
|
+
|
|
28
|
+
- **`src/data/config/site.json`** — Identità, header, footer (blocchi con `id`, `type`, `data`, `settings`).
|
|
29
|
+
- **`src/data/config/menu.json`** — Menu (es. `{ main: [{ label, href }] }`).
|
|
30
|
+
- **`src/data/config/theme.json`** — (Opzionale) Token tema (colori, font, radius).
|
|
31
|
+
- **`src/data/pages/<slug>.json`** — Una pagina = `slug`, `meta`, `sections[]` (array di blocchi `id`, `type`, `data`, `settings`). **Per creare una nuova pagina** basta aggiungere un file `<slug>.json` in `src/data/pages/`; lo slug del nome file diventa il path della pagina (es. `chi-siamo.json` → `/chi-siamo`).
|
|
32
|
+
- **`src/components/<sectionType>/`** — Una cartella per tipo di blocco (hero, header, footer, feature-grid, …).
|
|
33
|
+
- **`src/App.tsx`** — Carica site, menu, theme, pages; costruisce la config; renderizza **`<JsonPagesEngine config={config} />`**.
|
|
34
|
+
|
|
35
|
+
Il motore (Core) si aspetta comunque un **registry** (mappa tipo → componente) e le **pagine** nel formato previsto (slug → page con `sections`). Come popoli i JSON (a mano, da script, da altro CMS) è fuori dall'editor.
|
|
36
|
+
|
|
37
|
+
**Perché servono (struttura):** Path e forma (site, menu, theme, pages con sections) sono il contratto minimo che il Core usa per routing e rendering; rispettarli permette di cambiare in seguito fonte dati (JSON → API) senza riscrivere la logica. Vedi spec §2 (JSP), Appendix A.4.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 3. Componenti (solo View)
|
|
42
|
+
|
|
43
|
+
- Ogni **section type** ha almeno una **View**: riceve `data` e, se serve, `settings`. L'header riceve anche `menu` (array di `{ label, href }`).
|
|
44
|
+
- **Niente obbligo di capsule "piene":** puoi avere solo `View.tsx` (e magari un `index.ts` che esporta la View). Schema Zod e types servono solo se vuoi type-safety in sviluppo o se in futuro attivi il CMS.
|
|
45
|
+
- **Stili:** Puoi usare classi Tailwind "libere" o un set di variabili CSS (es. `--local-bg`, `--local-text`) per coerenza. Le spec CIP (solo variabili, niente utility nude) sono per il percorso governance; qui puoi adattare alle tue convenzioni.
|
|
46
|
+
- **Asset:** Se il Core espone `resolveAssetUrl(path, tenantId)`, usalo per le immagini; altrimenti path relativi o URL assoluti.
|
|
47
|
+
|
|
48
|
+
**Perché servono (componenti):** Il registry deve avere un componente per ogni `type` usato nei JSON; la View deve ricevere `data` (e `settings`/`menu` dove previsto) così il Core può renderizzare senza conoscere i dettagli. Senza registry coerente con i dati, il motore non saprebbe cosa montare. Vedi spec §3 (TBP), §4 (CIP) per il percorso completo.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 3.1 Image e campi immagine (se usi schema in seguito)
|
|
53
|
+
|
|
54
|
+
Se più avanti aggiungi schema Zod per type-safety o per attivare Studio, i **campi immagine** vanno modellati così:
|
|
55
|
+
|
|
56
|
+
- **Schema:** Il campo immagine è un **oggetto** (non una stringa) con almeno `url` e opzionalmente `alt`. Lo schema di questo oggetto va marcato con **`.describe('ui:image-picker')`** così il Form Factory (Inspector) mostra il widget Image Picker. Esempio: uno sub-schema `ImageSelectionSchema = z.object({ url: z.string(), alt: z.string().optional() }).describe('ui:image-picker')` usato come `image: ImageSelectionSchema.default({ url: '', alt: '' })`.
|
|
57
|
+
- **View:** Per il `src` dell'immagine usa **`resolveAssetUrl(data.image.url, tenantId)`**; sul nodo che rappresenta l'immagine imposta **`data-jp-field="image"`** (così l'Inspector lega correttamente il campo).
|
|
58
|
+
|
|
59
|
+
**Riferimento:** componente `image-break` in `apps/tenant-alpha/src/components/image-break/` (schema.ts, View.tsx) come esempio completo.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 4. Dati: da dove arrivano
|
|
64
|
+
|
|
65
|
+
- **Solo JSON locali:** Leggi `site.json`, `menu.json`, `theme.json`, `pages/*.json` e li passi in `config` (siteConfig, menuConfig, themeConfig, pages). Nessun CMS.
|
|
66
|
+
- **CMS esterno / API:** Invece di importare i JSON, fai fetch (o SSR) e costruisci gli stessi oggetti (siteConfig, menuConfig, pages) e li passi a `JsonPagesEngine`. La forma delle pagine resta: `{ slug, meta?, sections[] }`; ogni section: `{ id, type, data, settings? }`.
|
|
67
|
+
- **Ibrido:** Header/footer da `site.json`, body da API o da altro CMS: costruisci un unico `pages[slug]` con `sections` che rispettano i tipi di blocco che hai nel registry.
|
|
68
|
+
|
|
69
|
+
Non devi registrare schema o AddSectionConfig a meno che non attivi Studio.
|
|
70
|
+
|
|
71
|
+
**Perché servono (dati):** La forma `sections[]` con `id`, `type`, `data`, `settings?` è ciò che il SectionRenderer e il Core si aspettano; mantenere quella forma anche quando i dati arrivano da API o altro CMS evita adattatori fragili e permette di attivare Studio in seguito senza rifare i dati. Vedi spec Appendix A.2 (PageConfig, SiteConfig, MenuConfig).
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 5. Registry e config (minimo)
|
|
76
|
+
|
|
77
|
+
- **Registry:** Un oggetto che mappa ogni `sectionType` (stringa) al componente React che renderizza quel tipo. Es.: `{ header: Header, footer: Footer, hero: Hero, ... }`. Se non usi Studio, puoi tipizzare in modo lasco (es. `Record<string, React.FC<any>>` o comunque compatibile con quanto si aspetta `JsonPagesConfig['registry']`).
|
|
78
|
+
- **Config da passare a JsonPagesEngine:**
|
|
79
|
+
`tenantId`, `registry`, `pages`, `siteConfig`, `menuConfig`, `themeConfig` (o oggetto vuoto), `themeCss: { tenant: cssString }`.
|
|
80
|
+
Se **non** usi Studio, **schemas** e **addSection** possono essere placeholder (oggetto vuoto / no-op) se il Core lo permette; altrimenti fornisci il minimo (es. schemas = `{}`, addSection = `{ addableSectionTypes: [], sectionTypeLabels: {}, getDefaultSectionData: () => ({}) }`) per non rompere l'engine.
|
|
81
|
+
|
|
82
|
+
Verifica nella doc o nel tipo `JsonPagesConfig` se `schemas` e `addSection` sono opzionali quando Studio non è in uso.
|
|
83
|
+
|
|
84
|
+
**Perché servono (registry e config):** Il Core deve risolvere ogni section a un componente (registry) e avere pagine, site, menu, theme e CSS tenant per renderizzare e, se serve, iniettare lo Stage in iframe; i campi obbligatori di config sono il minimo per far funzionare l'engine. Placeholder per schemas/addSection evitano errori quando Studio non è usato. Vedi spec §10 (JEB), Appendix A.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 6. Checklist rapida (sviluppo grafico e dati, senza CMS)
|
|
89
|
+
|
|
90
|
+
| Cosa | Azione |
|
|
91
|
+
|------|--------|
|
|
92
|
+
| **Layout / grafico** | Implementare le View (una per section type) e gli stili (CSS / Tailwind / variabili). |
|
|
93
|
+
| **Dati** | Decidere fonte (JSON locali, API, altro CMS); costruire `siteConfig`, `menuConfig`, `pages` nella forma attesa e passarli in `config`. |
|
|
94
|
+
| **Registry** | Mappare ogni tipo di blocco usato nei JSON al componente corrispondente. |
|
|
95
|
+
| **Header / menu** | Header component riceve `data`, `settings`, `menu`; `menu` viene da `menuConfig` (es. `menuConfig.main`). |
|
|
96
|
+
| **Pagine** | Ogni pagina = un entry in `pages` con `sections[]`; ogni section ha `id`, `type`, `data`, `settings?`. |
|
|
97
|
+
| **Nuova pagina** | Aggiungere un file `<slug>.json` in `src/data/pages/` (lo slug diventa il path della pagina). |
|
|
98
|
+
| **Image (se schema)** | Campo immagine = oggetto `{ url, alt? }` con schema `.describe('ui:image-picker')`; View usa `resolveAssetUrl` e `data-jp-field="image"`. |
|
|
99
|
+
| **Studio / ICE** | Non usati: niente schema obbligatori, niente data-jp-*, niente overlay CSS, niente Add Section. |
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 7. Quando passi al percorso "Governance"
|
|
104
|
+
|
|
105
|
+
Se più avanti vuoi l'editor (Studio) e la governance (tipi, schema, Add Section, overlay): usa l'onboarding **02-Onboarding_Governance_completo.md** e allinea il progetto a tipi, capsule piene (View + schema + types), IDAC, TOCC, AddSectionConfig e Appendix A delle spec v1.2.
|
|
106
|
+
|
|
107
|
+
END_OF_FILE_CONTENT
|
|
108
|
+
echo "Creating docs/01-Onboarding_Governance_naked.md..."
|
|
109
|
+
cat << 'END_OF_FILE_CONTENT' > "docs/01-Onboarding_Governance_naked.md"
|
|
110
|
+
### 📄 File 1: Client Path (No CMS)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# Onboarding — Client Path (No CMS) — Complete Version
|
|
115
|
+
|
|
116
|
+
**Target:** Frontend Developers & Data Entry staff who **do not** use the CMS (Studio/ICE). The site acts as a **client**: data comes from local JSON, APIs, or an external CMS; you are responsible for layout, design, and rendering.
|
|
117
|
+
|
|
118
|
+
**Spec Reference:** JSONPAGES Architecture v1.2 — only the parts regarding site structure, components, and data. You ignore: Studio, ICE, Form Factory, IDAC, TOCC, AddSectionConfig, and mandatory schemas for the editor.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## 1. Your Role (Summary)
|
|
123
|
+
|
|
124
|
+
- **Visuals:** Layout and styling of sections (View + CSS / design tokens).
|
|
125
|
+
- **Data:** Where pages get their data (JSON files, API, external CMS) and how they are passed to the engine (config + pages).
|
|
126
|
+
|
|
127
|
+
You **do not** need to: type everything for the editor, expose Zod schemas to the Form Factory, handle Studio overlays, Add Section logic, etc. You can use minimal types or even `unknown`/`any` on data if strong type-safety is not required.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 2. Project Structure (Minimal)
|
|
132
|
+
|
|
133
|
+
- **`src/data/config/site.json`** — Identity, header, footer (blocks with `id`, `type`, `data`, `settings`).
|
|
134
|
+
- **`src/data/config/menu.json`** — Menu (e.g., `{ main: [{ label, href }] }`).
|
|
135
|
+
- **`src/data/config/theme.json`** — (Optional) Theme tokens (colors, fonts, radius).
|
|
136
|
+
- **`src/data/pages/<slug>.json`** — One page = `slug`, `meta`, `sections[]` (array of blocks `id`, `type`, `data`, `settings`). **To create a new page**, simply add a `<slug>.json` file in `src/data/pages/`; the filename slug becomes the page path (e.g., `about-us.json` → `/about-us`).
|
|
137
|
+
- **`src/components/<sectionType>/`** — One folder per block type (hero, header, footer, feature-grid, …).
|
|
138
|
+
- **`src/App.tsx`** — Loads site, menu, theme, pages; builds the config; renders **`<JsonPagesEngine config={config} />`**.
|
|
139
|
+
|
|
140
|
+
The Engine (Core) still expects a **registry** (type → component map) and **pages** in the expected format (slug → page with `sections`). How you populate the JSONs (manually, via script, from another CMS) is outside the editor's scope.
|
|
141
|
+
|
|
142
|
+
**Why this matters (Structure):** Paths and shape (site, menu, theme, pages with sections) are the minimal contract the Core uses for routing and rendering; respecting them allows you to switch data sources (JSON → API) later without rewriting logic. See Spec §2 (JSP), Appendix A.4.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 3. Components (View Only)
|
|
147
|
+
|
|
148
|
+
- Every **section type** has at least one **View**: it receives `data` and, if needed, `settings`. The header also receives `menu` (array of `{ label, href }`).
|
|
149
|
+
- **No "Full Capsule" requirement:** you can have just `View.tsx` (and maybe an `index.ts` exporting the View). Zod schemas and types are only needed if you want dev-time type-safety or plan to activate the CMS later.
|
|
150
|
+
- **Styles:** You can use "free" Tailwind classes or a set of CSS variables (e.g., `--local-bg`, `--local-text`) for consistency. CIP specs (variables only, no naked utilities) are for the Governance path; here you can adapt to your conventions.
|
|
151
|
+
- **Assets:** If the Core exposes `resolveAssetUrl(path, tenantId)`, use it for images; otherwise, use relative paths or absolute URLs.
|
|
152
|
+
|
|
153
|
+
**Why this matters (Components):** The registry must have a component for every `type` used in the JSONs; the View must receive `data` (and `settings`/`menu` where expected) so the Core can render without knowing details. Without a registry consistent with data, the engine wouldn't know what to mount. See Spec §3 (TBP), §4 (CIP) for the full path.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 3.1 Images and Image Fields (If using Schema later)
|
|
158
|
+
|
|
159
|
+
If you later add Zod schemas for type-safety or to activate Studio, **image fields** must be modeled as follows:
|
|
160
|
+
|
|
161
|
+
- **Schema:** The image field is an **object** (not a string) with at least `url` and optionally `alt`. This object's schema must be marked with **`.describe('ui:image-picker')`** so the Form Factory (Inspector) shows the Image Picker widget. Example: a sub-schema `ImageSelectionSchema = z.object({ url: z.string(), alt: z.string().optional() }).describe('ui:image-picker')` used as `image: ImageSelectionSchema.default({ url: '', alt: '' })`.
|
|
162
|
+
- **View:** For the image `src`, use **`resolveAssetUrl(data.image.url, tenantId)`**; on the node representing the image, set **`data-jp-field="image"`** (so the Inspector binds the field correctly).
|
|
163
|
+
|
|
164
|
+
**Reference:** See the `image-break` component in `apps/tenant-alpha/src/components/image-break/` (schema.ts, View.tsx) for a complete example.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 4. Data: Where it comes from
|
|
169
|
+
|
|
170
|
+
- **Local JSONs only:** Read `site.json`, `menu.json`, `theme.json`, `pages/*.json` and pass them into `config` (siteConfig, menuConfig, themeConfig, pages). No CMS.
|
|
171
|
+
- **External CMS / API:** Instead of importing JSONs, fetch (or SSR) and build the same objects (siteConfig, menuConfig, pages) and pass them to `JsonPagesEngine`. The page shape remains: `{ slug, meta?, sections[] }`; each section: `{ id, type, data, settings? }`.
|
|
172
|
+
- **Hybrid:** Header/footer from `site.json`, body from API or another CMS: build a single `pages[slug]` with `sections` that respect the block types you have in the registry.
|
|
173
|
+
|
|
174
|
+
You do not need to register schemas or AddSectionConfig unless you activate Studio.
|
|
175
|
+
|
|
176
|
+
**Why this matters (Data):** The `sections[]` shape with `id`, `type`, `data`, `settings?` is what the SectionRenderer and Core expect; maintaining this shape even when data comes from an API or another CMS avoids fragile adapters and allows activating Studio later without redoing data. See Spec Appendix A.2 (PageConfig, SiteConfig, MenuConfig).
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## 5. Registry and Config (Minimal)
|
|
181
|
+
|
|
182
|
+
- **Registry:** An object mapping every `sectionType` (string) to the React component rendering that type. E.g.: `{ header: Header, footer: Footer, hero: Hero, ... }`. If not using Studio, you can type loosely (e.g., `Record<string, React.FC<any>>` or whatever is compatible with `JsonPagesConfig['registry']`).
|
|
183
|
+
- **Config passed to JsonPagesEngine:**
|
|
184
|
+
`tenantId`, `registry`, `pages`, `siteConfig`, `menuConfig`, `themeConfig` (or empty object), `themeCss: { tenant: cssString }`.
|
|
185
|
+
If you are **not** using Studio, **schemas** and **addSection** can be placeholders (empty object / no-op) if the Core allows it; otherwise, provide the minimum (e.g., schemas = `{}`, addSection = `{ addableSectionTypes: [], sectionTypeLabels: {}, getDefaultSectionData: () => ({}) }`) to prevent engine errors.
|
|
186
|
+
|
|
187
|
+
Check docs or `JsonPagesConfig` type to see if `schemas` and `addSection` are optional when Studio is unused.
|
|
188
|
+
|
|
189
|
+
**Why this matters (Registry & Config):** The Core must resolve every section to a component (registry) and have pages, site, menu, theme, and tenant CSS to render and, if needed, inject the Stage iframe; mandatory config fields are the minimum to make the engine work. Placeholders for schemas/addSection avoid errors when Studio is not used. See Spec §10 (JEB), Appendix A.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## 6. Quick Checklist (Visual Dev & Data, No CMS)
|
|
194
|
+
|
|
195
|
+
| Item | Action |
|
|
196
|
+
|------|--------|
|
|
197
|
+
| **Layout / Visuals** | Implement Views (one per section type) and styles (CSS / Tailwind / variables). |
|
|
198
|
+
| **Data** | Decide source (Local JSON, API, other CMS); build `siteConfig`, `menuConfig`, `pages` in the expected shape and pass to `config`. |
|
|
199
|
+
| **Registry** | Map every block type used in JSONs to the corresponding component. |
|
|
200
|
+
| **Header / Menu** | Header component receives `data`, `settings`, `menu`; `menu` comes from `menuConfig` (e.g., `menuConfig.main`). |
|
|
201
|
+
| **Pages** | Each page = one entry in `pages` with `sections[]`; each section has `id`, `type`, `data`, `settings?`. |
|
|
202
|
+
| **New Page** | Add a `<slug>.json` file in `src/data/pages/` (slug becomes page path). |
|
|
203
|
+
| **Image (if schema)** | Image field = object `{ url, alt? }` with schema `.describe('ui:image-picker')`; View uses `resolveAssetUrl` and `data-jp-field="image"`. |
|
|
204
|
+
| **Studio / ICE** | Not used: no mandatory schemas, no data-jp-*, no overlay CSS, no Add Section. |
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## 7. Switching to the "Governance" Path
|
|
209
|
+
|
|
210
|
+
If you later want the editor (Studio) and governance (types, schema, Add Section, overlay): use the onboarding guide **02-Onboarding_Governance.md** and align the project with types, full capsules (View + schema + types), IDAC, TOCC, AddSectionConfig, and Appendix A of Spec v1.2.
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
END_OF_FILE_CONTENT
|
|
214
|
+
echo "Creating docs/02-Onboarding_Governance_CMS.md..."
|
|
215
|
+
cat << 'END_OF_FILE_CONTENT' > "docs/02-Onboarding_Governance_CMS.md"
|
|
216
|
+
|
|
217
|
+
# Onboarding — Governance Path (With CMS) — Complete Version
|
|
218
|
+
|
|
219
|
+
**Target:** Lead Developers & Architects setting up the **CMS** (Studio, ICE, Form Factory): in-app authoring, strong typing, content and component governance.
|
|
220
|
+
|
|
221
|
+
**Spec Reference:** JSONPAGES Architecture Specifications v1.2 (full) + Appendix A — Tenant Type & Code-Generation Annex.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## 1. What "Governance" Implies
|
|
226
|
+
|
|
227
|
+
- **Types:** Every section type is declared in `SectionDataRegistry` / `SectionSettingsRegistry` (module augmentation) and in `SectionComponentPropsMap`. Registry and config are strictly typed.
|
|
228
|
+
- **Schema:** Every section type has a Zod schema (data, and optionally settings) used by the Form Factory to generate the editor in the Inspector. Schemas are aggregated in `SECTION_SCHEMAS`.
|
|
229
|
+
- **Studio/ICE:** The editor (Inspector) hooks into the DOM via **data-jp-field** and **data-jp-item-id** / **data-jp-item-field**. The selection overlay in the iframe requires the **tenant** to provide the CSS (TOCC).
|
|
230
|
+
- **Add Section:** The tenant exposes **AddSectionConfig** (addable types, labels, default data) so the user can add sections from the library in Studio.
|
|
231
|
+
- **Design Tokens:** Views use CSS variables (`--local-*`) and no "naked" utilities (CIP) for consistency and compatibility with themes and overlays.
|
|
232
|
+
|
|
233
|
+
**Why this matters (Summary):** Types and schemas allow the Core and Form Factory to operate without knowing Tenant details; IDAC allows the Inspector to link Stage clicks to the active row in the sidebar (including active/inactive opacity); TOCC makes the overlay visible; AddSectionConfig defines the "Add Section" library; tokens and z-index avoid conflicts with the editing UI. Detailed "Whys" for each spec: see Spec v1.2 (§1–§10, JAP, Appendix A).
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## 1.1 The Value of Typing: Governance vs. CMS UX
|
|
238
|
+
|
|
239
|
+
Typing (TypeScript types + Zod schema) serves **two levels**: Governance (Developer/Architecture) and **CMS UX** (Author using Studio).
|
|
240
|
+
|
|
241
|
+
**Governance:** Typed registry, SectionComponentPropsMap, SiteConfig/PageConfig shape, audits, code-generation → consistency across tenants, no drift, safe refactoring, spec-based tooling.
|
|
242
|
+
|
|
243
|
+
**CMS UX:** The Zod schema drives the **Form Factory** (which widget for which field: text, textarea, select, list, icon-picker, **image-picker**); **data-jp-field** and **data-jp-item-id/field** bind Stage clicks to Inspector forms; **AddSectionConfig** provides addable types, labels, and defaults. Result for the author: consistent forms, "Add Section" with sensible names and initial data, correct selection (click → right form), validation with clear errors. Without schema and typed contracts, the Inspector wouldn't know which fields to show or how to validate. Thus: for governance, typing guarantees **contracts**; for CMS UX, it defines the **editing experience**. Both must be specified.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## 2. Project Structure (Complete)
|
|
248
|
+
|
|
249
|
+
- **`src/data/config/site.json`** — SiteConfig (identity, pages[], header block, footer block).
|
|
250
|
+
- **`src/data/config/menu.json`** — MenuConfig (e.g., `main: MenuItem[]`).
|
|
251
|
+
- **`src/data/config/theme.json`** — ThemeConfig (tokens).
|
|
252
|
+
- **`src/data/pages/<slug>.json`** — PageConfig (slug, meta, sections[]). **To create a new page**, simply add a `<slug>.json` file in `src/data/pages/`; the filename slug becomes the page path (e.g., `about-us.json` → `/about-us`).
|
|
253
|
+
- **`src/components/<sectionType>/`** — **Full Capsule:** View.tsx, schema.ts, types.ts, index.ts.
|
|
254
|
+
- **`src/lib/base-schemas.ts`** — BaseSectionData, BaseArrayItem, BaseSectionSettingsSchema.
|
|
255
|
+
- **`src/lib/schemas.ts`** — SECTION_SCHEMAS (aggregate of data schemas per type) + export SectionType.
|
|
256
|
+
- **`src/lib/ComponentRegistry.tsx`** — Typed Registry: `{ [K in SectionType]: React.FC<SectionComponentPropsMap[K]> }`.
|
|
257
|
+
- **`src/lib/addSectionConfig.ts`** — AddSectionConfig (addableSectionTypes, sectionTypeLabels, getDefaultSectionData).
|
|
258
|
+
- **`src/types.ts`** — SectionComponentPropsMap, PageConfig, SiteConfig, MenuConfig, ThemeConfig; **module augmentation** for SectionDataRegistry and SectionSettingsRegistry; re-export from `@jsonpages/core`.
|
|
259
|
+
- **`src/App.tsx`** — Bootstrap: config (tenantId, registry, schemas, pages, siteConfig, themeConfig, menuConfig, themeCss, addSection); `<JsonPagesEngine config={config} />`.
|
|
260
|
+
- **Global CSS** — Includes TOCC selectors for overlay (hover/selected/type label).
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## 3. Components (Capsules + IDAC + Tokens)
|
|
265
|
+
|
|
266
|
+
- **Capsule:** Every section type has View, schema (Zod), types (inferred), index. The **data** schema extends BaseSectionData; array items extend BaseArrayItem.
|
|
267
|
+
- **View:** Receives `data` and `settings` (and `menu` for header). Does not import Zod. Uses **only** CSS variables for colors/radii (e.g., `bg-[var(--local-bg)]`), root section with `z-index` ≤ 1.
|
|
268
|
+
- **IDAC (ICE):** On every editable scalar field: **`data-jp-field="<fieldKey>"`**. On every editable array item: **`data-jp-item-id="<stableId>"`** and **`data-jp-item-field="<arrayKey>"`**. This allows the Inspector to bind selection and forms to the correct paths.
|
|
269
|
+
- **Schema:** Use UI vocabulary (ECIP): `.describe('ui:text')`, `ui:textarea`, `ui:select`, `ui:number`, `ui:list`, `ui:icon-picker`, **`ui:image-picker`** (see §3.1). Editable object arrays: every object must have an `id` (BaseArrayItem).
|
|
270
|
+
|
|
271
|
+
**Why this matters (Components):** **data-jp-field** and **data-jp-item-*** are needed because the Stage is in an iframe, and the Core needs to know which field/item corresponds to a click without knowing the Tenant's DOM: this allows the sidebar to highlight the active row (even with active/inactive opacity), open the form on the right field, and handle lists (reorder, delete). Without IDAC, clicks on the canvas are not reflected in the sidebar. Schema with `ui:*` and BaseArrayItem are needed for the Form Factory to generate the right widgets and maintain stable keys (reorder/delete). Tokens and z-index prevent content from covering the overlay. See Spec §6 (IDAC), §5 (ECIP), §4 (CIP).
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 3.1 Image Picker: Correct Usage in Schema (Example `image-break`)
|
|
276
|
+
|
|
277
|
+
For **image fields**, the Form Factory exposes the **Image Picker** widget only if the schema is modeled correctly.
|
|
278
|
+
|
|
279
|
+
### Rule
|
|
280
|
+
|
|
281
|
+
- The image field is not a **string** (`z.string()`), but an **object** with at least `url` and, optionally, `alt`.
|
|
282
|
+
- The **schema of this object** (the sub-schema) must be marked with **`.describe('ui:image-picker')`**. The Form Factory recognizes `ui:image-picker` only on **ZodObject** (object schema), not on string fields.
|
|
283
|
+
|
|
284
|
+
### Example (`image-break` capsule)
|
|
285
|
+
|
|
286
|
+
**Schema (`schema.ts`):**
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
import { z } from 'zod';
|
|
290
|
+
import { BaseSectionData } from '@/lib/base-schemas';
|
|
291
|
+
|
|
292
|
+
const ImageSelectionSchema = z
|
|
293
|
+
.object({
|
|
294
|
+
url: z.string(),
|
|
295
|
+
alt: z.string().optional(),
|
|
296
|
+
})
|
|
297
|
+
.describe('ui:image-picker');
|
|
298
|
+
|
|
299
|
+
export const ImageBreakSchema = BaseSectionData.extend({
|
|
300
|
+
label: z.string().optional().describe('ui:text'),
|
|
301
|
+
image: ImageSelectionSchema.default({ url: '', alt: '' }),
|
|
302
|
+
caption: z.string().optional().describe('ui:textarea'),
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
- **ImageSelectionSchema** is a `z.object({ url, alt })` with **`.describe('ui:image-picker')`** on the object.
|
|
307
|
+
- The **`image`** field in the section data uses that schema (with default), so the Inspector shows the Image Picker widget for `image`.
|
|
308
|
+
|
|
309
|
+
**View (`View.tsx`):**
|
|
310
|
+
|
|
311
|
+
- For the image `src`: **`resolveAssetUrl(data.image.url, tenantId)`** (multi-tenant and relative paths).
|
|
312
|
+
- On the node representing the image (e.g., the `<img>` or a wrapper): **`data-jp-field="image"`** so the Stage click binds the Inspector to the `image` field.
|
|
313
|
+
- Other editable fields (caption, label) with **`data-jp-field="caption"`** and **`data-jp-field="label"`** where appropriate.
|
|
314
|
+
|
|
315
|
+
**Full Reference:** `apps/tenant-alpha/src/components/image-break/` (schema.ts, types.ts, View.tsx, index.ts).
|
|
316
|
+
|
|
317
|
+
### What to Avoid
|
|
318
|
+
|
|
319
|
+
- **Do not** use `.describe('ui:image-picker')` on a **string** field (e.g., `imageUrl: z.string().describe('ui:image-picker')`): the Image Picker widget expects an object `{ url, alt? }`.
|
|
320
|
+
- **Do not** forget `data-jp-field="image"` on the corresponding DOM node, otherwise Inspector ↔ Stage binding won't work for that field.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## 4. Data: Shape and Responsibility
|
|
325
|
+
|
|
326
|
+
- **site.json / menu.json / theme.json / pages/*.json** — Exact shape as in Appendix A (SiteConfig, MenuConfig, ThemeConfig, PageConfig). These are the Source of Truth when the user saves from Studio (Working Draft → persist to these files or API generating them).
|
|
327
|
+
- **Studio** updates the Working Draft; sync with the iframe and "Bake" use the same structure. Therefore, data passed to JsonPagesEngine (siteConfig, menuConfig, pages) must be compatible with what the editor modifies.
|
|
328
|
+
|
|
329
|
+
If data comes from an external CMS, you must synchronize: e.g., export from Studio → push to CMS, or CMS as source and Studio in read-only; in any case, the **shape** of pages (sections with id, type, data, settings) remains that of the spec.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## 5. Registry, Schemas, Types, AddSection
|
|
334
|
+
|
|
335
|
+
- **types.ts:** Single point of **module augmentation** and definition of SectionComponentPropsMap, PageConfig, SiteConfig, MenuConfig, ThemeConfig. Header: `{ data, settings?, menu: MenuItem[] }`; all others: `{ data, settings? }`.
|
|
336
|
+
- **ComponentRegistry:** Every SectionType key has the corresponding component; type: `{ [K in SectionType]: React.FC<SectionComponentPropsMap[K]> }`.
|
|
337
|
+
- **SECTION_SCHEMAS:** Every SectionType key has the **data Zod schema** (same order as registry). Base schemas re-exported from base-schemas.ts.
|
|
338
|
+
- **addSectionConfig:** addableSectionTypes (only types the user can add from the library), sectionTypeLabels, getDefaultSectionData(type) returning valid `data` for that schema.
|
|
339
|
+
|
|
340
|
+
**Why this matters (registry, schemas, types, addSection):** A single augmentation point (types.ts) and a single SECTION_SCHEMAS avoid duplication and ensure registry, Form Factory, and config use the same types. AddSectionConfig is the single source of truth for "which sections can be added" and "with what defaults"; without it, the "Add Section" modal wouldn't have valid names or initial data. See Spec §9 (ASC), Appendix A.2–A.3.
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## 6. Overlay and CSS (TOCC)
|
|
345
|
+
|
|
346
|
+
- The Core injects the overlay markup (wrapper with `data-section-id`, sibling with `data-jp-section-overlay`). The **tenant** must provide the CSS so that:
|
|
347
|
+
- `[data-jp-section-overlay]` covers the section, `pointer-events: none`, high z-index (e.g., 9999).
|
|
348
|
+
- Hover and selected states are visible (dashed/solid border, optional tint).
|
|
349
|
+
- The type label (e.g., `[data-jp-section-overlay] > div`) is positioned and visible on hover/selected.
|
|
350
|
+
|
|
351
|
+
Without this, the overlay is invisible in the Studio iframe.
|
|
352
|
+
|
|
353
|
+
**Why this matters (TOCC):** The Stage iframe loads only Tenant CSS; the Core injects overlay markup but not styles. Without TOCC selectors in Tenant CSS, hover/selected borders and type labels are invisible: the author cannot see which section is selected. See Spec §7 (TOCC).
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## 7. Quick Checklist (Visual Dev & Data, With CMS)
|
|
358
|
+
|
|
359
|
+
| Item | Action |
|
|
360
|
+
|------|--------|
|
|
361
|
+
| **Layout / Visuals** | View with `--local-*` variables, z-index ≤ 1, no naked utilities. |
|
|
362
|
+
| **Data (Shape)** | SiteConfig, MenuConfig, ThemeConfig, PageConfig as in Appendix A; JSON in `data/config` and `data/pages`. |
|
|
363
|
+
| **Capsules** | View + schema (with `ui:*`) + types + index; data schema extends BaseSectionData; array item with id. |
|
|
364
|
+
| **IDAC** | `data-jp-field` on editable scalar fields; `data-jp-item-id` and `data-jp-item-field` on array items. |
|
|
365
|
+
| **types.ts** | SectionComponentPropsMap (header with menu), augmentation, PageConfig, SiteConfig, MenuConfig, ThemeConfig. |
|
|
366
|
+
| **Registry** | All types mapped to component; registry type as in Appendix A. |
|
|
367
|
+
| **SECTION_SCHEMAS** | One entry per type (data schema); re-export base schemas. |
|
|
368
|
+
| **addSectionConfig** | addableSectionTypes, sectionTypeLabels, getDefaultSectionData. |
|
|
369
|
+
| **Config** | tenantId, registry, schemas, pages, siteConfig, themeConfig, menuConfig, themeCss, addSection. |
|
|
370
|
+
| **TOCC** | CSS overlay for `[data-jp-section-overlay]`, hover, selected, type label. |
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## 8. Spec References
|
|
375
|
+
|
|
376
|
+
- **Architecture and ICE:** §1–§10 (MTRP, JSP, TBP, CIP, ECIP, IDAC, TOCC, BSDS, ASC, JEB).
|
|
377
|
+
- **Types and Code-Generation:** Appendix A (Core types, Tenant types, Schema contract, File paths, Integration checklist).
|
|
378
|
+
- **Admin:** JAP (Studio topology, Working Draft, Bake, overlay, Green Build).
|
|
379
|
+
|
|
380
|
+
Using this path gives you full **governance**: types, schema, editor, Add Section, and overlay aligned with Spec v1.2. For versions with all "Why this matters" explanations, use the file **JSONPAGES_Specs_v1.2_completo.md**.
|
|
381
|
+
|
|
382
|
+
--- END OF FILE docs/02-Onboarding_Governance.md ---
|
|
383
|
+
END_OF_FILE_CONTENT
|
|
384
|
+
echo "Creating docs/02-Onboarding_Governance_completo_aggiornato.md..."
|
|
385
|
+
cat << 'END_OF_FILE_CONTENT' > "docs/02-Onboarding_Governance_completo_aggiornato.md"
|
|
386
|
+
# Onboarding — Percorso Governance (con CMS) — Versione completa
|
|
387
|
+
|
|
388
|
+
**Per chi:** Sviluppo grafico e dati quando vuoi il **CMS** (Studio, ICE, Form Factory): authoring in-app, tipizzazione forte, governance dei contenuti e dei componenti.
|
|
389
|
+
|
|
390
|
+
**Riferimento spec:** JSONPAGES Architecture Specifications v1.2 (full) + Appendix A — Tenant Type & Code-Generation Annex.
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## 1. Cosa implica "governance"
|
|
395
|
+
|
|
396
|
+
- **Tipi:** Ogni section type è dichiarato in `SectionDataRegistry` / `SectionSettingsRegistry` (module augmentation) e in `SectionComponentPropsMap`. Registry e config sono tipizzati.
|
|
397
|
+
- **Schema:** Ogni section type ha uno schema Zod (data, e opzionalmente settings) usato dal Form Factory per generare l'editor nell'Inspector. Gli schema sono aggregati in `SECTION_SCHEMAS`.
|
|
398
|
+
- **Studio/ICE:** L'editor (Inspector) si aggancia al DOM tramite **data-jp-field** e **data-jp-item-id** / **data-jp-item-field**. L'overlay di selezione in iframe richiede che il **tenant** fornisca il CSS (TOCC).
|
|
399
|
+
- **Add Section:** Il tenant espone **AddSectionConfig** (tipi addabili, label, default data) così in Studio l'utente può aggiungere section dalla libreria.
|
|
400
|
+
- **Design tokens:** Le View usano variabili CSS (`--local-*`) e nessuna utility "nuda" (CIP) per coerenza e compatibilità con tema e overlay.
|
|
401
|
+
|
|
402
|
+
**Perché servono (in sintesi):** Tipi e schema permettono al Core e al Form Factory di operare senza conoscere i dettagli del Tenant; IDAC permette all'Inspector di legare click in Stage e riga attiva nella sidebar (inclusa opacità attivo/inattivo); TOCC rende visibile l'overlay; AddSectionConfig definisce la libreria "Aggiungi sezione"; token e z-index evitano conflitti con l'UI di editing. Dettaglio sui "perché" per ogni specifica: spec v1.2 (§1–§10, JAP, Appendix A), dove ogni sezione ha un paragrafo **Perché servono**.
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## 1.1 Valore della tipizzazione: governance e CMS UX
|
|
407
|
+
|
|
408
|
+
La tipizzazione (tipi TypeScript + schema Zod) serve a **due livelli**: governance (sviluppatore/architettura) e **UX del CMS** (autore che usa Studio). Spesso si menziona solo il primo.
|
|
409
|
+
|
|
410
|
+
**Governance:** registry tipizzato, SectionComponentPropsMap, forma di SiteConfig/PageConfig, audit, code-generation → coerenza tra tenant, niente drift, refactor sicuro, tooling basato su spec.
|
|
411
|
+
|
|
412
|
+
**CMS UX:** lo schema Zod guida il **Form Factory** (quali widget per ogni campo: text, textarea, select, list, icon-picker, **image-picker**); **data-jp-field** e **data-jp-item-id/field** legano click in Stage e form nell'Inspector; **AddSectionConfig** dà tipi addabili, label e default. Risultato per l'autore: form coerenti, "Aggiungi sezione" con nomi e dati iniziali sensati, selezione corretta (click → form giusto), validazione con errori chiari. Senza schema e contratto tipizzato l'Inspector non saprebbe quali campi mostrare né come validare. Quindi: per la governance la tipizzazione garantisce contratti; per la **CMS UX** definisce l'**esperienza di editing** (controlli, label, default, binding). Va specificato entrambi.
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## 2. Struttura progetto (completa)
|
|
417
|
+
|
|
418
|
+
- **`src/data/config/site.json`** — SiteConfig (identity, pages[], header block, footer block).
|
|
419
|
+
- **`src/data/config/menu.json`** — MenuConfig (es. `main: MenuItem[]`).
|
|
420
|
+
- **`src/data/config/theme.json`** — ThemeConfig (tokens).
|
|
421
|
+
- **`src/data/pages/<slug>.json`** — PageConfig (slug, meta, sections[]). **Per creare una nuova pagina** basta aggiungere un file `<slug>.json` in `src/data/pages/`; lo slug del nome file diventa il path della pagina (es. `chi-siamo.json` → `/chi-siamo`).
|
|
422
|
+
- **`src/components/<sectionType>/`** — **Capsula piena:** View.tsx, schema.ts, types.ts, index.ts.
|
|
423
|
+
- **`src/lib/base-schemas.ts`** — BaseSectionData, BaseArrayItem, BaseSectionSettingsSchema.
|
|
424
|
+
- **`src/lib/schemas.ts`** — SECTION_SCHEMAS (aggregato degli schema data per tipo) + export SectionType.
|
|
425
|
+
- **`src/lib/ComponentRegistry.tsx`** — Registry tipizzato: `{ [K in SectionType]: React.FC<SectionComponentPropsMap[K]> }`.
|
|
426
|
+
- **`src/lib/addSectionConfig.ts`** — AddSectionConfig (addableSectionTypes, sectionTypeLabels, getDefaultSectionData).
|
|
427
|
+
- **`src/types.ts`** — SectionComponentPropsMap, PageConfig, SiteConfig, MenuConfig, ThemeConfig; **module augmentation** per SectionDataRegistry e SectionSettingsRegistry; re-export da `@jsonpages/core`.
|
|
428
|
+
- **`src/App.tsx`** — Bootstrap: config (tenantId, registry, schemas, pages, siteConfig, themeConfig, menuConfig, themeCss, addSection); `<JsonPagesEngine config={config} />`.
|
|
429
|
+
- **CSS globale** — Include i selettori TOCC per overlay (hover/selected/type label).
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## 3. Componenti (capsule + IDAC + token)
|
|
434
|
+
|
|
435
|
+
- **Capsula:** Ogni section type ha View, schema (Zod), types (inferiti), index. Lo schema **data** estende BaseSectionData; gli item degli array estendono BaseArrayItem.
|
|
436
|
+
- **View:** Riceve `data` e `settings` (e `menu` per header). Non importa Zod. Usa **solo** variabili CSS per colori/raggi (es. `bg-[var(--local-bg)]`), sezione root con `z-index` ≤ 1.
|
|
437
|
+
- **IDAC (ICE):** Su ogni campo editabile in modo scalare: **`data-jp-field="<fieldKey>"`**. Su ogni item di array editabile: **`data-jp-item-id="<stableId>"`** e **`data-jp-item-field="<arrayKey>"`**. Così l'Inspector può legare selezione e form ai path corretti.
|
|
438
|
+
- **Schema:** Usa il vocabolario UI (ECIP): `.describe('ui:text')`, `ui:textarea`, `ui:select`, `ui:number`, `ui:list`, `ui:icon-picker`, **`ui:image-picker`** (vedi §3.1). Array di oggetti editabili: ogni oggetto con `id` (BaseArrayItem).
|
|
439
|
+
|
|
440
|
+
**Perché servono (componenti):** **data-jp-field** e **data-jp-item-*** servono perché lo Stage è in un iframe e il Core deve sapere quale campo/item corrisponde al click senza conoscere il DOM del Tenant: così la sidebar può evidenziare la riga attiva (anche con opacità diversa per attivo/inattivo), aprire il form sul campo giusto e gestire liste (reorder, delete). Senza IDAC, click sul canvas non si riflette nella sidebar. Schema con `ui:*` e BaseArrayItem servono al Form Factory per generare i widget giusti e mantenere chiavi stabili (reorder/delete). Token e z-index evitano che il contenuto copra l'overlay. Vedi spec §6 (IDAC), §5 (ECIP), §4 (CIP).
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## 3.1 Image Picker: uso corretto nello schema (esempio `image-break`)
|
|
445
|
+
|
|
446
|
+
Per i **campi immagine** il Form Factory espone il widget **Image Picker** solo se lo schema è modellato correttamente.
|
|
447
|
+
|
|
448
|
+
### Regola
|
|
449
|
+
|
|
450
|
+
- Il campo immagine non è una **stringa** (`z.string()`), ma un **oggetto** con almeno `url` e, opzionalmente, `alt`.
|
|
451
|
+
- Lo **schema di questo oggetto** (il sub-schema) va marcato con **`.describe('ui:image-picker')`**. Il Form Factory riconosce `ui:image-picker` solo su **ZodObject** (schema oggetto), non su campi stringa.
|
|
452
|
+
|
|
453
|
+
### Esempio (capsula `image-break`)
|
|
454
|
+
|
|
455
|
+
**Schema (`schema.ts`):**
|
|
456
|
+
|
|
457
|
+
```ts
|
|
458
|
+
import { z } from 'zod';
|
|
459
|
+
import { BaseSectionData } from '@/lib/base-schemas';
|
|
460
|
+
|
|
461
|
+
const ImageSelectionSchema = z
|
|
462
|
+
.object({
|
|
463
|
+
url: z.string(),
|
|
464
|
+
alt: z.string().optional(),
|
|
465
|
+
})
|
|
466
|
+
.describe('ui:image-picker');
|
|
467
|
+
|
|
468
|
+
export const ImageBreakSchema = BaseSectionData.extend({
|
|
469
|
+
label: z.string().optional().describe('ui:text'),
|
|
470
|
+
image: ImageSelectionSchema.default({ url: '', alt: '' }),
|
|
471
|
+
caption: z.string().optional().describe('ui:textarea'),
|
|
472
|
+
});
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
- **ImageSelectionSchema** è un `z.object({ url, alt })` con **`.describe('ui:image-picker')`** sull'oggetto.
|
|
476
|
+
- Il campo **`image`** nella section data usa quel schema (con default) così l'Inspector mostra il widget Image Picker per `image`.
|
|
477
|
+
|
|
478
|
+
**View (`View.tsx`):**
|
|
479
|
+
|
|
480
|
+
- Per il `src` dell'immagine: **`resolveAssetUrl(data.image.url, tenantId)`** (multi-tenant e path relativi).
|
|
481
|
+
- Sul nodo che rappresenta l'immagine (es. il `<img>` o un wrapper): **`data-jp-field="image"`** così il click in Stage lega l'Inspector al campo `image`.
|
|
482
|
+
- Altri campi editabili (caption, label) con **`data-jp-field="caption"`** e **`data-jp-field="label"`** dove appropriato.
|
|
483
|
+
|
|
484
|
+
**Riferimento completo:** `apps/tenant-alpha/src/components/image-break/` (schema.ts, types.ts, View.tsx, index.ts).
|
|
485
|
+
|
|
486
|
+
### Cosa evitare
|
|
487
|
+
|
|
488
|
+
- **Non** usare `.describe('ui:image-picker')` su un campo **stringa** (es. `imageUrl: z.string().describe('ui:image-picker')`): il widget Image Picker si aspetta un oggetto `{ url, alt? }`.
|
|
489
|
+
- **Non** dimenticare `data-jp-field="image"` sul nodo corrispondente nel DOM, altrimenti il binding Inspector ↔ Stage non funziona per quel campo.
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## 4. Dati: forma e responsabilità
|
|
494
|
+
|
|
495
|
+
- **site.json / menu.json / theme.json / pages/*.json** — Forma esatta come in Appendix A (SiteConfig, MenuConfig, ThemeConfig, PageConfig). Sono la source of truth quando l'utente salva da Studio (Working Draft → persist su questi file o su API che li generano).
|
|
496
|
+
- **Studio** aggiorna il Working Draft; il sync con l'iframe e il "Bake" usano la stessa struttura. Quindi i dati che passi a JsonPagesEngine (siteConfig, menuConfig, pages) devono essere compatibili con ciò che l'editor modifica.
|
|
497
|
+
|
|
498
|
+
Se i dati arrivano da un CMS esterno, tocca a te sincronizzare: es. export da Studio → push su CMS, oppure CMS come source e Studio in read-only; in ogni caso la **forma** delle pagine (sections con id, type, data, settings) resta quella della spec.
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
## 5. Registry, schemas, types, addSection
|
|
503
|
+
|
|
504
|
+
- **types.ts:** Unico punto di **module augmentation** e definizione di SectionComponentPropsMap, PageConfig, SiteConfig, MenuConfig, ThemeConfig. Header: `{ data, settings?, menu: MenuItem[] }`; tutti gli altri: `{ data, settings? }`.
|
|
505
|
+
- **ComponentRegistry:** Ogni chiave di SectionType ha il componente corrispondente; tipo: `{ [K in SectionType]: React.FC<SectionComponentPropsMap[K]> }`.
|
|
506
|
+
- **SECTION_SCHEMAS:** Ogni chiave di SectionType ha lo **schema Zod della data** (stesso ordine del registry). Base schemas re-exportati da base-schemas.ts.
|
|
507
|
+
- **addSectionConfig:** addableSectionTypes (solo i tipi che l'utente può aggiungere dalla libreria), sectionTypeLabels, getDefaultSectionData(type) che restituisce `data` valido per quello schema.
|
|
508
|
+
|
|
509
|
+
**Perché servono (registry, schemas, types, addSection):** Un solo punto di augmentation (types.ts) e un solo SECTION_SCHEMAS evita duplicazioni e garantisce che registry, Form Factory e config usino gli stessi tipi. AddSectionConfig è l'unica fonte di verità per "quali section si possono aggiungere" e "con quali default"; senza, il modal "Aggiungi sezione" non avrebbe nomi né dati iniziali validi. Vedi spec §9 (ASC), Appendix A.2–A.3.
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
## 6. Overlay e CSS (TOCC)
|
|
514
|
+
|
|
515
|
+
- Il Core inietta il markup dell'overlay (wrapper con `data-section-id`, sibling con `data-jp-section-overlay`). Il **tenant** deve fornire il CSS in modo che:
|
|
516
|
+
- `[data-jp-section-overlay]` copra la section, `pointer-events: none`, z-index alto (es. 9999).
|
|
517
|
+
- Hover e selected siano visibili (bordo tratteggiato / pieno, eventuale tint).
|
|
518
|
+
- Il type label (es. `[data-jp-section-overlay] > div`) sia posizionato e visibile su hover/selected.
|
|
519
|
+
|
|
520
|
+
Senza questo, in Studio l'overlay non si vede nell'iframe.
|
|
521
|
+
|
|
522
|
+
**Perché servono (TOCC):** L'iframe dello Stage carica solo il CSS del Tenant; il Core inietta il markup dell'overlay ma non gli stili. Senza i selettori TOCC nel CSS tenant, bordo hover/selected e type label non sono visibili: l'autore non vede quale section è selezionata. Vedi spec §7 (TOCC).
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## 7. Checklist rapida (sviluppo grafico e dati, con CMS)
|
|
527
|
+
|
|
528
|
+
| Cosa | Azione |
|
|
529
|
+
|------|--------|
|
|
530
|
+
| **Layout / grafico** | View con variabili `--local-*`, z-index ≤ 1, nessuna utility naked. |
|
|
531
|
+
| **Dati (forma)** | SiteConfig, MenuConfig, ThemeConfig, PageConfig come in Appendix A; JSON in `data/config` e `data/pages`. |
|
|
532
|
+
| **Nuova pagina** | Aggiungere un file `<slug>.json` in `src/data/pages/` (lo slug diventa il path della pagina). |
|
|
533
|
+
| **Capsule** | View + schema (con ui:*) + types + index; data schema estende BaseSectionData; array item con id. |
|
|
534
|
+
| **IDAC** | data-jp-field su campi scalari editabili; data-jp-item-id e data-jp-item-field su item di array. |
|
|
535
|
+
| **Image Picker** | Campo immagine = oggetto `{ url, alt? }` con sub-schema `.describe('ui:image-picker')`; View con `resolveAssetUrl` e `data-jp-field="image"`. Esempio: `image-break`. |
|
|
536
|
+
| **types.ts** | SectionComponentPropsMap (header con menu), augmentation, PageConfig, SiteConfig, MenuConfig, ThemeConfig. |
|
|
537
|
+
| **Registry** | Tutti i tipi mappati al componente; tipo registry come in Appendix A. |
|
|
538
|
+
| **SECTION_SCHEMAS** | Un entry per tipo (schema data); re-export base schemas. |
|
|
539
|
+
| **addSectionConfig** | addableSectionTypes, sectionTypeLabels, getDefaultSectionData. |
|
|
540
|
+
| **Config** | tenantId, registry, schemas, pages, siteConfig, themeConfig, menuConfig, themeCss, addSection. |
|
|
541
|
+
| **TOCC** | CSS overlay per [data-jp-section-overlay], hover, selected, type label. |
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
## 8. Riferimenti spec
|
|
546
|
+
|
|
547
|
+
- **Architettura e ICE:** §1–§10 (MTRP, JSP, TBP, CIP, ECIP, IDAC, TOCC, BSDS, ASC, JEB).
|
|
548
|
+
- **Tipi e code-generation:** Appendix A (Core types, Tenant types, Schema contract, File paths, Integration checklist).
|
|
549
|
+
- **Admin:** JAP (Studio topology, Working Draft, Bake, overlay, Green Build).
|
|
550
|
+
|
|
551
|
+
Usando questo percorso hai **governance** piena: tipi, schema, editor, Add Section e overlay allineati alle spec v1.2. Per le versioni con tutti i "Perché servono" usa il file **JSONPAGES_Specs_v1.2_completo.md**.
|
|
552
|
+
|
|
553
|
+
END_OF_FILE_CONTENT
|
|
554
|
+
mkdir -p "docs/ver"
|
|
6
555
|
echo "Creating index.html..."
|
|
7
556
|
cat << 'END_OF_FILE_CONTENT' > "index.html"
|
|
8
557
|
<!DOCTYPE html>
|
|
@@ -35,15 +584,17 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
35
584
|
"dev": "vite",
|
|
36
585
|
"dev:clean": "vite --force",
|
|
37
586
|
"build": "tsc && vite build",
|
|
38
|
-
"dist": "bash ./src2Code.sh src index.html package.json",
|
|
39
|
-
"preview": "vite preview"
|
|
587
|
+
"dist": "bash ./src2Code.sh src index.html scripts docs package.json",
|
|
588
|
+
"preview": "vite preview",
|
|
589
|
+
"bake:email": "tsx scripts/bake-email.tsx",
|
|
590
|
+
"bakemail": "npm run bake:email --"
|
|
40
591
|
},
|
|
41
592
|
"dependencies": {
|
|
42
593
|
"@tiptap/extension-image": "^2.11.5",
|
|
43
594
|
"@tiptap/extension-link": "^2.11.5",
|
|
44
595
|
"@tiptap/react": "^2.11.5",
|
|
45
596
|
"@tiptap/starter-kit": "^2.11.5",
|
|
46
|
-
"@jsonpages/core": "^1.0.
|
|
597
|
+
"@jsonpages/core": "^1.0.52",
|
|
47
598
|
"clsx": "^2.1.1",
|
|
48
599
|
"lucide-react": "^0.474.0",
|
|
49
600
|
"react": "^19.0.0",
|
|
@@ -62,10 +613,283 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
62
613
|
"@types/react-dom": "^19.0.3",
|
|
63
614
|
"@vitejs/plugin-react": "^4.3.4",
|
|
64
615
|
"typescript": "^5.7.3",
|
|
65
|
-
"vite": "^6.0.11"
|
|
616
|
+
"vite": "^6.0.11",
|
|
617
|
+
"@react-email/components": "^0.0.41",
|
|
618
|
+
"@react-email/render": "^1.0.5",
|
|
619
|
+
"tsx": "^4.20.5"
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
END_OF_FILE_CONTENT
|
|
624
|
+
mkdir -p "scripts"
|
|
625
|
+
echo "Creating scripts/bake-email.tsx..."
|
|
626
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/bake-email.tsx"
|
|
627
|
+
import fs from "node:fs/promises";
|
|
628
|
+
import path from "node:path";
|
|
629
|
+
import { pathToFileURL } from "node:url";
|
|
630
|
+
import { render } from "@react-email/render";
|
|
631
|
+
import React from "react";
|
|
632
|
+
|
|
633
|
+
type Args = {
|
|
634
|
+
entry?: string;
|
|
635
|
+
out?: string;
|
|
636
|
+
outDir?: string;
|
|
637
|
+
exportName?: string;
|
|
638
|
+
propsFile?: string;
|
|
639
|
+
siteConfig?: string;
|
|
640
|
+
themeConfig?: string;
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
type BakeTarget = {
|
|
644
|
+
entryAbs: string;
|
|
645
|
+
outAbs: string;
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
type SiteConfig = {
|
|
649
|
+
identity?: {
|
|
650
|
+
title?: string;
|
|
651
|
+
logoUrl?: string;
|
|
652
|
+
};
|
|
653
|
+
header?: {
|
|
654
|
+
data?: {
|
|
655
|
+
logoImageUrl?: {
|
|
656
|
+
url?: string;
|
|
657
|
+
alt?: string;
|
|
658
|
+
};
|
|
659
|
+
};
|
|
660
|
+
};
|
|
661
|
+
footer?: {
|
|
662
|
+
data?: {
|
|
663
|
+
brandText?: string;
|
|
664
|
+
brandHighlight?: string;
|
|
665
|
+
tagline?: string;
|
|
666
|
+
logoImageUrl?: {
|
|
667
|
+
url?: string;
|
|
668
|
+
alt?: string;
|
|
669
|
+
};
|
|
670
|
+
};
|
|
671
|
+
};
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
type ThemeConfig = {
|
|
675
|
+
tokens?: {
|
|
676
|
+
colors?: Record<string, string>;
|
|
677
|
+
typography?: {
|
|
678
|
+
fontFamily?: Record<string, string>;
|
|
679
|
+
};
|
|
680
|
+
borderRadius?: Record<string, string>;
|
|
681
|
+
};
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
const DEFAULT_EMAIL_DIR = "src/emails";
|
|
685
|
+
const DEFAULT_OUT_DIR = "email-templates";
|
|
686
|
+
const DEFAULT_SITE_CONFIG = "src/data/config/site.json";
|
|
687
|
+
const DEFAULT_THEME_CONFIG = "src/data/config/theme.json";
|
|
688
|
+
|
|
689
|
+
function parseArgs(argv: string[]): Args {
|
|
690
|
+
const args: Args = {};
|
|
691
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
692
|
+
const key = argv[i];
|
|
693
|
+
const next = argv[i + 1];
|
|
694
|
+
if (!next) continue;
|
|
695
|
+
if (key === "--entry") {
|
|
696
|
+
args.entry = next;
|
|
697
|
+
i += 1;
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
if (key === "--out") {
|
|
701
|
+
args.out = next;
|
|
702
|
+
i += 1;
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (key === "--out-dir") {
|
|
706
|
+
args.outDir = next;
|
|
707
|
+
i += 1;
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
if (key === "--export") {
|
|
711
|
+
args.exportName = next;
|
|
712
|
+
i += 1;
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
if (key === "--props") {
|
|
716
|
+
args.propsFile = next;
|
|
717
|
+
i += 1;
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (key === "--site-config") {
|
|
721
|
+
args.siteConfig = next;
|
|
722
|
+
i += 1;
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
if (key === "--theme-config") {
|
|
726
|
+
args.themeConfig = next;
|
|
727
|
+
i += 1;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return args;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function isComponentExport(value: unknown): value is React.ComponentType<any> {
|
|
734
|
+
return typeof value === "function";
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function toKebabCase(value: string): string {
|
|
738
|
+
return value
|
|
739
|
+
.replace(/Email$/i, "")
|
|
740
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
741
|
+
.replace(/[_\s]+/g, "-")
|
|
742
|
+
.toLowerCase();
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function deriveOutAbs(entryAbs: string, outDirAbs: string): string {
|
|
746
|
+
const base = path.basename(entryAbs).replace(/\.[^.]+$/, "");
|
|
747
|
+
const fileName = `${toKebabCase(base)}.html`;
|
|
748
|
+
return path.join(outDirAbs, fileName);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async function discoverEmailEntries(rootDir: string): Promise<string[]> {
|
|
752
|
+
const abs = path.resolve(process.cwd(), rootDir);
|
|
753
|
+
const dirEntries = await fs.readdir(abs, { withFileTypes: true }).catch(() => []);
|
|
754
|
+
return dirEntries
|
|
755
|
+
.filter((entry) => entry.isFile())
|
|
756
|
+
.map((entry) => entry.name)
|
|
757
|
+
.filter((name) => /\.(tsx|jsx)$/i.test(name))
|
|
758
|
+
.map((name) => path.join(abs, name));
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function normalizeUrl(value: string | undefined): string | undefined {
|
|
762
|
+
if (!value) return undefined;
|
|
763
|
+
const trimmed = value.trim();
|
|
764
|
+
return trimmed || undefined;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async function readJsonObject<T>(filePath: string): Promise<T | null> {
|
|
768
|
+
const abs = path.resolve(process.cwd(), filePath);
|
|
769
|
+
const raw = await fs.readFile(abs, "utf8").catch(() => null);
|
|
770
|
+
if (!raw) return null;
|
|
771
|
+
const parsed = JSON.parse(raw);
|
|
772
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
773
|
+
return parsed as T;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function buildDefaultProps(site: SiteConfig | null, theme: ThemeConfig | null): Record<string, unknown> {
|
|
777
|
+
const footerBrandText = site?.footer?.data?.brandText?.trim() ?? "";
|
|
778
|
+
const footerBrandHighlight = site?.footer?.data?.brandHighlight?.trim() ?? "";
|
|
779
|
+
const brandName = `${footerBrandText}${footerBrandHighlight}`.trim() || "{{tenantName}}";
|
|
780
|
+
|
|
781
|
+
const tenantName = site?.identity?.title?.trim() || brandName;
|
|
782
|
+
const logoUrl =
|
|
783
|
+
normalizeUrl(site?.header?.data?.logoImageUrl?.url) ||
|
|
784
|
+
normalizeUrl(site?.footer?.data?.logoImageUrl?.url) ||
|
|
785
|
+
normalizeUrl(site?.identity?.logoUrl) ||
|
|
786
|
+
"";
|
|
787
|
+
const logoAlt =
|
|
788
|
+
site?.header?.data?.logoImageUrl?.alt?.trim() ||
|
|
789
|
+
site?.footer?.data?.logoImageUrl?.alt?.trim() ||
|
|
790
|
+
brandName;
|
|
791
|
+
const tagline = site?.footer?.data?.tagline?.trim() || "";
|
|
792
|
+
|
|
793
|
+
return {
|
|
794
|
+
tenantName,
|
|
795
|
+
brandName,
|
|
796
|
+
logoUrl,
|
|
797
|
+
logoAlt,
|
|
798
|
+
tagline,
|
|
799
|
+
theme: theme?.tokens ?? {},
|
|
800
|
+
correlationId: "{{correlationId}}",
|
|
801
|
+
replyTo: "{{replyTo}}",
|
|
802
|
+
leadData: {
|
|
803
|
+
name: "{{lead.name}}",
|
|
804
|
+
email: "{{lead.email}}",
|
|
805
|
+
phone: "{{lead.phone}}",
|
|
806
|
+
checkin: "{{lead.checkin}}",
|
|
807
|
+
checkout: "{{lead.checkout}}",
|
|
808
|
+
guests: "{{lead.guests}}",
|
|
809
|
+
notes: "{{lead.notes}}",
|
|
810
|
+
},
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
async function readProps(
|
|
815
|
+
propsFile: string | undefined,
|
|
816
|
+
siteConfigPath: string,
|
|
817
|
+
themeConfigPath: string
|
|
818
|
+
): Promise<Record<string, unknown>> {
|
|
819
|
+
if (propsFile) {
|
|
820
|
+
const raw = await fs.readFile(path.resolve(process.cwd(), propsFile), "utf8");
|
|
821
|
+
const parsed = JSON.parse(raw);
|
|
822
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
823
|
+
throw new Error("--props must point to a JSON object file");
|
|
824
|
+
}
|
|
825
|
+
return parsed as Record<string, unknown>;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const site = await readJsonObject<SiteConfig>(siteConfigPath);
|
|
829
|
+
const theme = await readJsonObject<ThemeConfig>(themeConfigPath);
|
|
830
|
+
return buildDefaultProps(site, theme);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
async function buildTargets(args: Args): Promise<BakeTarget[]> {
|
|
834
|
+
const outDirAbs = path.resolve(process.cwd(), args.outDir ?? DEFAULT_OUT_DIR);
|
|
835
|
+
|
|
836
|
+
if (args.entry) {
|
|
837
|
+
const entryAbs = path.resolve(process.cwd(), args.entry);
|
|
838
|
+
const outAbs = args.out ? path.resolve(process.cwd(), args.out) : deriveOutAbs(entryAbs, outDirAbs);
|
|
839
|
+
return [{ entryAbs, outAbs }];
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const discovered = await discoverEmailEntries(DEFAULT_EMAIL_DIR);
|
|
843
|
+
if (discovered.length === 0) {
|
|
844
|
+
throw new Error(`No templates found in ${DEFAULT_EMAIL_DIR}`);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return discovered.map((entryAbs) => ({
|
|
848
|
+
entryAbs,
|
|
849
|
+
outAbs: deriveOutAbs(entryAbs, outDirAbs),
|
|
850
|
+
}));
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
async function bakeTemplate(target: BakeTarget, args: Args, props: Record<string, unknown>) {
|
|
854
|
+
const moduleUrl = pathToFileURL(target.entryAbs).href;
|
|
855
|
+
const mod = (await import(moduleUrl)) as Record<string, unknown>;
|
|
856
|
+
const picked = args.exportName ? mod[args.exportName] : mod.default;
|
|
857
|
+
|
|
858
|
+
if (!isComponentExport(picked)) {
|
|
859
|
+
const available = Object.keys(mod).join(", ") || "(none)";
|
|
860
|
+
throw new Error(
|
|
861
|
+
`Template export not found or not a component. Requested: ${args.exportName ?? "default"}. Available exports: ${available}. Entry: ${target.entryAbs}`
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const element = React.createElement(picked, props);
|
|
866
|
+
const html = await render(element, { pretty: true });
|
|
867
|
+
|
|
868
|
+
await fs.mkdir(path.dirname(target.outAbs), { recursive: true });
|
|
869
|
+
await fs.writeFile(target.outAbs, html, "utf8");
|
|
870
|
+
|
|
871
|
+
console.log(`Baked email template: ${path.relative(process.cwd(), target.outAbs)}`);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
async function main() {
|
|
875
|
+
const args = parseArgs(process.argv.slice(2));
|
|
876
|
+
const targets = await buildTargets(args);
|
|
877
|
+
const props = await readProps(
|
|
878
|
+
args.propsFile,
|
|
879
|
+
args.siteConfig ?? DEFAULT_SITE_CONFIG,
|
|
880
|
+
args.themeConfig ?? DEFAULT_THEME_CONFIG
|
|
881
|
+
);
|
|
882
|
+
|
|
883
|
+
for (const target of targets) {
|
|
884
|
+
await bakeTemplate(target, args, props);
|
|
66
885
|
}
|
|
67
886
|
}
|
|
68
887
|
|
|
888
|
+
main().catch((error) => {
|
|
889
|
+
console.error(error instanceof Error ? error.message : error);
|
|
890
|
+
process.exit(1);
|
|
891
|
+
});
|
|
892
|
+
|
|
69
893
|
END_OF_FILE_CONTENT
|
|
70
894
|
mkdir -p "src"
|
|
71
895
|
echo "Creating src/App.tsx..."
|
|
@@ -213,7 +1037,6 @@ console.log("🔍 DEBUG ENV:", {
|
|
|
213
1037
|
export default App;
|
|
214
1038
|
|
|
215
1039
|
END_OF_FILE_CONTENT
|
|
216
|
-
# SKIP: src/App.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
217
1040
|
echo "Creating src/App_.tsx..."
|
|
218
1041
|
cat << 'END_OF_FILE_CONTENT' > "src/App_.tsx"
|
|
219
1042
|
import { useState, useEffect } from 'react';
|
|
@@ -3085,17 +3908,248 @@ export type ProductTriadSettings = z.infer<typeof BaseSectionSettingsSchema>;
|
|
|
3085
3908
|
|
|
3086
3909
|
END_OF_FILE_CONTENT
|
|
3087
3910
|
mkdir -p "src/components/save-drawer"
|
|
3088
|
-
# SKIP: src/components/save-drawer/DeployConnector.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
3089
|
-
# SKIP: src/components/save-drawer/DeployConnector.tsx:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
3090
|
-
# SKIP: src/components/save-drawer/DeployNode.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
3091
|
-
# SKIP: src/components/save-drawer/DeployNode.tsx:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
3092
|
-
# SKIP: src/components/save-drawer/DopaDrawer.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
3093
|
-
# SKIP: src/components/save-drawer/DopaDrawer.tsx:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
3094
|
-
# SKIP: src/components/save-drawer/Visuals.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
3095
|
-
# SKIP: src/components/save-drawer/Visuals.tsx:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
3096
|
-
# SKIP: src/components/save-drawer/saverStyle.css:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
3097
|
-
# SKIP: src/components/save-drawer/saverStyle.css:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
3098
3911
|
mkdir -p "src/components/tiptap"
|
|
3912
|
+
echo "Creating src/components/tiptap/INTEGRATION.md..."
|
|
3913
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/tiptap/INTEGRATION.md"
|
|
3914
|
+
# Tiptap Editorial — Integration Guide
|
|
3915
|
+
|
|
3916
|
+
How to add the `tiptap` section to a new tenant.
|
|
3917
|
+
|
|
3918
|
+
---
|
|
3919
|
+
|
|
3920
|
+
## 1. Copy the component
|
|
3921
|
+
|
|
3922
|
+
Copy the entire folder into the new tenant:
|
|
3923
|
+
|
|
3924
|
+
```
|
|
3925
|
+
src/components/tiptap/
|
|
3926
|
+
index.ts
|
|
3927
|
+
types.ts
|
|
3928
|
+
View.tsx
|
|
3929
|
+
```
|
|
3930
|
+
|
|
3931
|
+
---
|
|
3932
|
+
|
|
3933
|
+
## 2. Install npm dependencies
|
|
3934
|
+
|
|
3935
|
+
Add to the tenant's `package.json` and run `npm install`:
|
|
3936
|
+
|
|
3937
|
+
```json
|
|
3938
|
+
"@tiptap/extension-image": "^2.11.5",
|
|
3939
|
+
"@tiptap/extension-link": "^2.11.5",
|
|
3940
|
+
"@tiptap/react": "^2.11.5",
|
|
3941
|
+
"@tiptap/starter-kit": "^2.11.5",
|
|
3942
|
+
"react-markdown": "^9.0.1",
|
|
3943
|
+
"rehype-sanitize": "^6.0.0",
|
|
3944
|
+
"remark-gfm": "^4.0.1",
|
|
3945
|
+
"tiptap-markdown": "^0.8.10"
|
|
3946
|
+
```
|
|
3947
|
+
|
|
3948
|
+
---
|
|
3949
|
+
|
|
3950
|
+
## 3. Add CSS to `src/index.css`
|
|
3951
|
+
|
|
3952
|
+
Two blocks are required — one for the public (visitor) view, one for the editor (studio) view.
|
|
3953
|
+
|
|
3954
|
+
```css
|
|
3955
|
+
/* ==========================================================================
|
|
3956
|
+
TIPTAP — Public content typography (visitor view)
|
|
3957
|
+
========================================================================== */
|
|
3958
|
+
.jp-tiptap-content > * + * { margin-top: 0.75em; }
|
|
3959
|
+
|
|
3960
|
+
.jp-tiptap-content h1 { font-size: 2em; font-weight: 700; line-height: 1.2; margin-top: 1.25em; margin-bottom: 0.25em; }
|
|
3961
|
+
.jp-tiptap-content h2 { font-size: 1.5em; font-weight: 700; line-height: 1.3; margin-top: 1.25em; margin-bottom: 0.25em; }
|
|
3962
|
+
.jp-tiptap-content h3 { font-size: 1.25em; font-weight: 600; line-height: 1.4; margin-top: 1.25em; margin-bottom: 0.25em; }
|
|
3963
|
+
.jp-tiptap-content h4 { font-size: 1em; font-weight: 600; line-height: 1.5; margin-top: 1em; margin-bottom: 0.25em; }
|
|
3964
|
+
|
|
3965
|
+
.jp-tiptap-content p { line-height: 1.7; }
|
|
3966
|
+
.jp-tiptap-content strong { font-weight: 700; }
|
|
3967
|
+
.jp-tiptap-content em { font-style: italic; }
|
|
3968
|
+
.jp-tiptap-content s { text-decoration: line-through; }
|
|
3969
|
+
|
|
3970
|
+
.jp-tiptap-content a { color: var(--primary); text-decoration: underline; text-underline-offset: 2px; }
|
|
3971
|
+
.jp-tiptap-content a:hover { opacity: 0.8; }
|
|
3972
|
+
|
|
3973
|
+
.jp-tiptap-content code {
|
|
3974
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
3975
|
+
font-size: 0.875em;
|
|
3976
|
+
background: color-mix(in oklch, var(--foreground) 8%, transparent);
|
|
3977
|
+
border-radius: 0.25em;
|
|
3978
|
+
padding: 0.1em 0.35em;
|
|
3979
|
+
}
|
|
3980
|
+
.jp-tiptap-content pre {
|
|
3981
|
+
background: color-mix(in oklch, var(--background) 60%, black);
|
|
3982
|
+
border-radius: 0.5em;
|
|
3983
|
+
padding: 1em 1.25em;
|
|
3984
|
+
overflow-x: auto;
|
|
3985
|
+
}
|
|
3986
|
+
.jp-tiptap-content pre code { background: none; padding: 0; }
|
|
3987
|
+
|
|
3988
|
+
.jp-tiptap-content ul { list-style-type: disc; padding-left: 1.625em; }
|
|
3989
|
+
.jp-tiptap-content ol { list-style-type: decimal; padding-left: 1.625em; }
|
|
3990
|
+
.jp-tiptap-content li { line-height: 1.7; margin-top: 0.25em; }
|
|
3991
|
+
.jp-tiptap-content li + li { margin-top: 0.25em; }
|
|
3992
|
+
|
|
3993
|
+
.jp-tiptap-content blockquote {
|
|
3994
|
+
border-left: 3px solid var(--border);
|
|
3995
|
+
padding-left: 1em;
|
|
3996
|
+
color: var(--muted-foreground);
|
|
3997
|
+
font-style: italic;
|
|
3998
|
+
}
|
|
3999
|
+
.jp-tiptap-content hr { border: none; border-top: 1px solid var(--border); margin: 1.5em 0; }
|
|
4000
|
+
.jp-tiptap-content img { max-width: 100%; height: auto; border-radius: 0.5rem; }
|
|
4001
|
+
|
|
4002
|
+
/* ==========================================================================
|
|
4003
|
+
TIPTAP / PROSEMIRROR — Editor typography (studio view)
|
|
4004
|
+
========================================================================== */
|
|
4005
|
+
.jp-simple-editor .ProseMirror { outline: none; word-break: break-word; }
|
|
4006
|
+
.jp-simple-editor .ProseMirror > * + * { margin-top: 0.75em; }
|
|
4007
|
+
|
|
4008
|
+
.jp-simple-editor .ProseMirror h1 { font-size: 2em; font-weight: 700; line-height: 1.2; margin-top: 1.25em; margin-bottom: 0.25em; }
|
|
4009
|
+
.jp-simple-editor .ProseMirror h2 { font-size: 1.5em; font-weight: 700; line-height: 1.3; margin-top: 1.25em; margin-bottom: 0.25em; }
|
|
4010
|
+
.jp-simple-editor .ProseMirror h3 { font-size: 1.25em; font-weight: 600; line-height: 1.4; margin-top: 1.25em; margin-bottom: 0.25em; }
|
|
4011
|
+
.jp-simple-editor .ProseMirror h4 { font-size: 1em; font-weight: 600; line-height: 1.5; margin-top: 1em; margin-bottom: 0.25em; }
|
|
4012
|
+
|
|
4013
|
+
.jp-simple-editor .ProseMirror p { line-height: 1.7; }
|
|
4014
|
+
.jp-simple-editor .ProseMirror strong { font-weight: 700; }
|
|
4015
|
+
.jp-simple-editor .ProseMirror em { font-style: italic; }
|
|
4016
|
+
.jp-simple-editor .ProseMirror s { text-decoration: line-through; }
|
|
4017
|
+
|
|
4018
|
+
.jp-simple-editor .ProseMirror a { color: var(--primary); text-decoration: underline; text-underline-offset: 2px; }
|
|
4019
|
+
.jp-simple-editor .ProseMirror a:hover { opacity: 0.8; }
|
|
4020
|
+
|
|
4021
|
+
.jp-simple-editor .ProseMirror code {
|
|
4022
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
4023
|
+
font-size: 0.875em;
|
|
4024
|
+
background: color-mix(in oklch, var(--foreground) 8%, transparent);
|
|
4025
|
+
border-radius: 0.25em;
|
|
4026
|
+
padding: 0.1em 0.35em;
|
|
4027
|
+
}
|
|
4028
|
+
.jp-simple-editor .ProseMirror pre {
|
|
4029
|
+
background: color-mix(in oklch, var(--background) 60%, black);
|
|
4030
|
+
border-radius: 0.5em;
|
|
4031
|
+
padding: 1em 1.25em;
|
|
4032
|
+
overflow-x: auto;
|
|
4033
|
+
}
|
|
4034
|
+
.jp-simple-editor .ProseMirror pre code { background: none; padding: 0; }
|
|
4035
|
+
|
|
4036
|
+
.jp-simple-editor .ProseMirror ul { list-style-type: disc; padding-left: 1.625em; }
|
|
4037
|
+
.jp-simple-editor .ProseMirror ol { list-style-type: decimal; padding-left: 1.625em; }
|
|
4038
|
+
.jp-simple-editor .ProseMirror li { line-height: 1.7; margin-top: 0.25em; }
|
|
4039
|
+
.jp-simple-editor .ProseMirror li + li { margin-top: 0.25em; }
|
|
4040
|
+
|
|
4041
|
+
.jp-simple-editor .ProseMirror blockquote {
|
|
4042
|
+
border-left: 3px solid var(--border);
|
|
4043
|
+
padding-left: 1em;
|
|
4044
|
+
color: var(--muted-foreground);
|
|
4045
|
+
font-style: italic;
|
|
4046
|
+
}
|
|
4047
|
+
.jp-simple-editor .ProseMirror hr { border: none; border-top: 1px solid var(--border); margin: 1.5em 0; }
|
|
4048
|
+
|
|
4049
|
+
.jp-simple-editor .ProseMirror img { max-width: 100%; height: auto; border-radius: 0.5rem; }
|
|
4050
|
+
.jp-simple-editor .ProseMirror img[data-uploading="true"] {
|
|
4051
|
+
opacity: 0.6;
|
|
4052
|
+
filter: grayscale(0.25);
|
|
4053
|
+
outline: 2px dashed rgb(59 130 246 / 0.7);
|
|
4054
|
+
outline-offset: 2px;
|
|
4055
|
+
}
|
|
4056
|
+
.jp-simple-editor .ProseMirror img[data-upload-error="true"] {
|
|
4057
|
+
outline: 2px solid rgb(239 68 68 / 0.8);
|
|
4058
|
+
outline-offset: 2px;
|
|
4059
|
+
}
|
|
4060
|
+
.jp-simple-editor .ProseMirror p.is-editor-empty:first-child::before {
|
|
4061
|
+
content: attr(data-placeholder);
|
|
4062
|
+
color: var(--muted-foreground);
|
|
4063
|
+
opacity: 0.5;
|
|
4064
|
+
pointer-events: none;
|
|
4065
|
+
float: left;
|
|
4066
|
+
height: 0;
|
|
4067
|
+
}
|
|
4068
|
+
```
|
|
4069
|
+
|
|
4070
|
+
---
|
|
4071
|
+
|
|
4072
|
+
## 4. Register in `src/lib/schemas.ts`
|
|
4073
|
+
|
|
4074
|
+
```ts
|
|
4075
|
+
import { TiptapSchema } from '@/components/tiptap';
|
|
4076
|
+
|
|
4077
|
+
export const SECTION_SCHEMAS = {
|
|
4078
|
+
// ... existing schemas
|
|
4079
|
+
'tiptap': TiptapSchema,
|
|
4080
|
+
} as const;
|
|
4081
|
+
```
|
|
4082
|
+
|
|
4083
|
+
---
|
|
4084
|
+
|
|
4085
|
+
## 5. Register in `src/lib/addSectionConfig.ts`
|
|
4086
|
+
|
|
4087
|
+
```ts
|
|
4088
|
+
const addableSectionTypes = [
|
|
4089
|
+
// ... existing types
|
|
4090
|
+
'tiptap',
|
|
4091
|
+
] as const;
|
|
4092
|
+
|
|
4093
|
+
const sectionTypeLabels = {
|
|
4094
|
+
// ... existing labels
|
|
4095
|
+
'tiptap': 'Tiptap Editorial',
|
|
4096
|
+
};
|
|
4097
|
+
|
|
4098
|
+
function getDefaultSectionData(type: string) {
|
|
4099
|
+
switch (type) {
|
|
4100
|
+
// ... existing cases
|
|
4101
|
+
case 'tiptap': return { content: '# Post title\n\nStart writing in Markdown...' };
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
```
|
|
4105
|
+
|
|
4106
|
+
---
|
|
4107
|
+
|
|
4108
|
+
## 6. Register in `src/lib/ComponentRegistry.tsx`
|
|
4109
|
+
|
|
4110
|
+
```tsx
|
|
4111
|
+
import { Tiptap } from '@/components/tiptap';
|
|
4112
|
+
|
|
4113
|
+
export const ComponentRegistry = {
|
|
4114
|
+
// ... existing components
|
|
4115
|
+
'tiptap': Tiptap,
|
|
4116
|
+
};
|
|
4117
|
+
```
|
|
4118
|
+
|
|
4119
|
+
---
|
|
4120
|
+
|
|
4121
|
+
## 7. Register in `src/types.ts`
|
|
4122
|
+
|
|
4123
|
+
```ts
|
|
4124
|
+
import type { TiptapData, TiptapSettings } from '@/components/tiptap';
|
|
4125
|
+
|
|
4126
|
+
export type SectionComponentPropsMap = {
|
|
4127
|
+
// ... existing entries
|
|
4128
|
+
'tiptap': { data: TiptapData; settings?: TiptapSettings };
|
|
4129
|
+
};
|
|
4130
|
+
|
|
4131
|
+
declare module '@jsonpages/core' {
|
|
4132
|
+
export interface SectionDataRegistry {
|
|
4133
|
+
// ... existing entries
|
|
4134
|
+
'tiptap': TiptapData;
|
|
4135
|
+
}
|
|
4136
|
+
export interface SectionSettingsRegistry {
|
|
4137
|
+
// ... existing entries
|
|
4138
|
+
'tiptap': TiptapSettings;
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
```
|
|
4142
|
+
|
|
4143
|
+
---
|
|
4144
|
+
|
|
4145
|
+
## Notes
|
|
4146
|
+
|
|
4147
|
+
- Typography uses tenant CSS variables (`--primary`, `--border`, `--muted-foreground`, `--font-mono`) — no hardcoded colors.
|
|
4148
|
+
- `@tailwindcss/typography` is **not** required; the CSS blocks above replace it.
|
|
4149
|
+
- The toolbar is admin-only (studio mode). In visitor mode, content is rendered via `ReactMarkdown`.
|
|
4150
|
+
- Underline is intentionally excluded: `tiptap-markdown` with `html: false` cannot round-trip `<u>` tags.
|
|
4151
|
+
|
|
4152
|
+
END_OF_FILE_CONTENT
|
|
3099
4153
|
echo "Creating src/components/tiptap/View.tsx..."
|
|
3100
4154
|
cat << 'END_OF_FILE_CONTENT' > "src/components/tiptap/View.tsx"
|
|
3101
4155
|
import React from 'react';
|
|
@@ -3770,7 +4824,7 @@ const StudioTiptapEditor: React.FC<{ data: TiptapData }> = ({ data }) => {
|
|
|
3770
4824
|
// ── Public view ───────────────────────────────────────────────────────────────
|
|
3771
4825
|
|
|
3772
4826
|
const PublicTiptapContent: React.FC<{ content: string }> = ({ content }) => (
|
|
3773
|
-
<article className="
|
|
4827
|
+
<article className="jp-tiptap-content" data-jp-field="content">
|
|
3774
4828
|
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeSanitize]}>
|
|
3775
4829
|
{content}
|
|
3776
4830
|
</ReactMarkdown>
|
|
@@ -3782,12 +4836,14 @@ const PublicTiptapContent: React.FC<{ content: string }> = ({ content }) => (
|
|
|
3782
4836
|
export const Tiptap: React.FC<{ data: TiptapData; settings?: TiptapSettings }> = ({ data }) => {
|
|
3783
4837
|
const { mode } = useStudio();
|
|
3784
4838
|
return (
|
|
3785
|
-
<section className="w-full">
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
4839
|
+
<section className="w-full py-12">
|
|
4840
|
+
<div className="container mx-auto px-6 max-w-3xl">
|
|
4841
|
+
{mode === 'studio' ? (
|
|
4842
|
+
<StudioTiptapEditor data={data} />
|
|
4843
|
+
) : (
|
|
4844
|
+
<PublicTiptapContent content={data.content ?? ''} />
|
|
4845
|
+
)}
|
|
4846
|
+
</div>
|
|
3791
4847
|
</section>
|
|
3792
4848
|
);
|
|
3793
4849
|
};
|
|
@@ -4393,7 +5449,6 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/menu.json"
|
|
|
4393
5449
|
]
|
|
4394
5450
|
}
|
|
4395
5451
|
END_OF_FILE_CONTENT
|
|
4396
|
-
# SKIP: src/data/config/menu.json:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
4397
5452
|
echo "Creating src/data/config/site.json..."
|
|
4398
5453
|
cat << 'END_OF_FILE_CONTENT' > "src/data/config/site.json"
|
|
4399
5454
|
{
|
|
@@ -4481,7 +5536,6 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/site.json"
|
|
|
4481
5536
|
}
|
|
4482
5537
|
}
|
|
4483
5538
|
END_OF_FILE_CONTENT
|
|
4484
|
-
# SKIP: src/data/config/site.json:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
4485
5539
|
echo "Creating src/data/config/theme.json..."
|
|
4486
5540
|
cat << 'END_OF_FILE_CONTENT' > "src/data/config/theme.json"
|
|
4487
5541
|
{
|
|
@@ -4513,7 +5567,6 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/theme.json"
|
|
|
4513
5567
|
}
|
|
4514
5568
|
}
|
|
4515
5569
|
END_OF_FILE_CONTENT
|
|
4516
|
-
# SKIP: src/data/config/theme.json:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
4517
5570
|
mkdir -p "src/data/pages"
|
|
4518
5571
|
echo "Creating src/data/pages/docs.json..."
|
|
4519
5572
|
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/docs.json"
|
|
@@ -5106,7 +6159,6 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
|
5106
6159
|
]
|
|
5107
6160
|
}
|
|
5108
6161
|
END_OF_FILE_CONTENT
|
|
5109
|
-
# SKIP: src/data/pages/home.json:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
5110
6162
|
echo "Creating src/data/pages/post.json..."
|
|
5111
6163
|
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/post.json"
|
|
5112
6164
|
{
|
|
@@ -5121,7 +6173,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/post.json"
|
|
|
5121
6173
|
"id": "post-editorial-main",
|
|
5122
6174
|
"type": "tiptap",
|
|
5123
6175
|
"data": {
|
|
5124
|
-
"content": "# JsonPages Cloud – Terms of Service & EULA\n\n---\n\n### **Last Updated:** March 2026\n\n### 1. THE SERVICE\n\nJsonPages provides a hybrid content management infrastructure consisting of:\n\n- **The Core:** An open-source library (@jsonpages/core) governed by the **MIT License**.\n- **The Cloud:** A proprietary SaaS platform (`cloud.jsonpages.io`) that provides the \"Git Bridge,\" Asset Pipeline, and Managed Infrastructure.\n\nBy using the Cloud Service, you agree to these terms.\n\n### 2. DATA SOVEREIGNTY & OWNERSHIP\n\n- **Your Content:** All data (JSON files), code, and assets managed through JsonPages remain your exclusive property. JsonPages acts only as an **orchestrator**.\n- **The Bridge:** You grant JsonPages the necessary permissions to perform Git operations (commits/pushes) on your behalf to your designated repositories (GitHub/GitLab).\n- **Portability:** Since your content is stored as flat JSON files in your own repository, you retain the right to migrate away from the Cloud Service at any time without data lock-in.\n- \n\n### 3. SUBSCRIPTIONS & ENTITLEMENTS\n\n- **Billing:** The Cloud Service is billed on a subscription basis (**Monthly Recurring Revenue**).\n- **Entitlements:** Each \"Project\" or \"Tenant\" consumes one entitlement. Active entitlements grant access to the Visual Studio (ICE) and the Cloud Save API.\n- **Third-Party Costs:** You are solely responsible for any costs incurred on third-party platforms (e.g., **Vercel** hosting, **GitHub** storage, **Cloudflare** workers).\n
|
|
6176
|
+
"content": "# JsonPages Cloud – Terms of Service & EULA\n\n---\n\n### **Last Updated:** March 2026\n\n### 1. THE SERVICE\n\nJsonPages provides a hybrid content management infrastructure consisting of:\n\n- **The Core:** An open-source library (@jsonpages/core) governed by the **MIT License**.\n- **The Cloud:** A proprietary SaaS platform (`cloud.jsonpages.io`) that provides the \"Git Bridge,\" Asset Pipeline, and Managed Infrastructure.\n\nBy using the Cloud Service, you agree to these terms.\n\n### 2. DATA SOVEREIGNTY & OWNERSHIP\n\n- **Your Content:** All data (JSON files), code, and assets managed through JsonPages remain your exclusive property. JsonPages acts only as an **orchestrator**.\n- **The Bridge:** You grant JsonPages the necessary permissions to perform Git operations (commits/pushes) on your behalf to your designated repositories (GitHub/GitLab).\n- **Portability:** Since your content is stored as flat JSON files in your own repository, you retain the right to migrate away from the Cloud Service at any time without data lock-in.\n- \n\n### 3. SUBSCRIPTIONS & ENTITLEMENTS\n\n- **Billing:** The Cloud Service is billed on a subscription basis (**Monthly Recurring Revenue**).\n- **Entitlements:** Each \"Project\" or \"Tenant\" consumes one entitlement. Active entitlements grant access to the Visual Studio (ICE) and the Cloud Save API.\n- **Third-Party Costs:** You are solely responsible for any costs incurred on third-party platforms (e.g., **Vercel** hosting, **GitHub** storage, **Cloudflare** workers).\n\n### 4. ACCEPTABLE USE\n\nYou may not use JsonPages Cloud to:\n\n- Host or manage illegal, harmful, or offensive content.\n- Attempt to reverse-engineer the proprietary Cloud Bridge or bypass entitlement checks.\n- Interfere with the stability of the API for other users.\n- \n\n### 5. LIMITATION OF LIABILITY\n\n- **\"As-Is\" Basis:** The service is provided \"as-is.\" While we strive for 99.9% uptime, JsonPages is not liable for data loss resulting from Git conflicts, third-party outages (Vercel/GitHub), or user error.\n- **No Warranty:** We do not warrant that the service will be error-free or uninterrupted.\n- \n\n### 6. TERMINATION\n\n- **By You:** You can cancel your subscription at any time. Your Studio access will remain active until the end of the current billing cycle.\n- \n- **By Us:** We reserve the right to suspend accounts that violate these terms or fail to settle outstanding invoices.\n\n### 7. GOVERNING LAW\n\nThese terms are governed by the laws of **Italy/European Union**, without regard to conflict of law principles."
|
|
5125
6177
|
},
|
|
5126
6178
|
"settings": {}
|
|
5127
6179
|
}
|
|
@@ -5193,6 +6245,317 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/servizi_trattamento.json"
|
|
|
5193
6245
|
}
|
|
5194
6246
|
]
|
|
5195
6247
|
}
|
|
6248
|
+
END_OF_FILE_CONTENT
|
|
6249
|
+
mkdir -p "src/emails"
|
|
6250
|
+
echo "Creating src/emails/LeadNotificationEmail.tsx..."
|
|
6251
|
+
cat << 'END_OF_FILE_CONTENT' > "src/emails/LeadNotificationEmail.tsx"
|
|
6252
|
+
import {
|
|
6253
|
+
Body,
|
|
6254
|
+
Button,
|
|
6255
|
+
Container,
|
|
6256
|
+
Head,
|
|
6257
|
+
Heading,
|
|
6258
|
+
Hr,
|
|
6259
|
+
Html,
|
|
6260
|
+
Img,
|
|
6261
|
+
Preview,
|
|
6262
|
+
Section,
|
|
6263
|
+
Text,
|
|
6264
|
+
} from "@react-email/components";
|
|
6265
|
+
|
|
6266
|
+
type LeadData = Record<string, unknown>;
|
|
6267
|
+
|
|
6268
|
+
type EmailTheme = {
|
|
6269
|
+
colors?: {
|
|
6270
|
+
primary?: string;
|
|
6271
|
+
secondary?: string;
|
|
6272
|
+
accent?: string;
|
|
6273
|
+
background?: string;
|
|
6274
|
+
surface?: string;
|
|
6275
|
+
surfaceAlt?: string;
|
|
6276
|
+
text?: string;
|
|
6277
|
+
textMuted?: string;
|
|
6278
|
+
border?: string;
|
|
6279
|
+
};
|
|
6280
|
+
typography?: {
|
|
6281
|
+
fontFamily?: {
|
|
6282
|
+
primary?: string;
|
|
6283
|
+
display?: string;
|
|
6284
|
+
mono?: string;
|
|
6285
|
+
};
|
|
6286
|
+
};
|
|
6287
|
+
borderRadius?: {
|
|
6288
|
+
sm?: string;
|
|
6289
|
+
md?: string;
|
|
6290
|
+
lg?: string;
|
|
6291
|
+
xl?: string;
|
|
6292
|
+
};
|
|
6293
|
+
};
|
|
6294
|
+
|
|
6295
|
+
export type LeadNotificationEmailProps = {
|
|
6296
|
+
tenantName: string;
|
|
6297
|
+
correlationId: string;
|
|
6298
|
+
replyTo?: string | null;
|
|
6299
|
+
leadData: LeadData;
|
|
6300
|
+
brandName?: string;
|
|
6301
|
+
logoUrl?: string;
|
|
6302
|
+
logoAlt?: string;
|
|
6303
|
+
tagline?: string;
|
|
6304
|
+
theme?: EmailTheme;
|
|
6305
|
+
};
|
|
6306
|
+
|
|
6307
|
+
function safeString(value: unknown): string {
|
|
6308
|
+
if (value == null) return "-";
|
|
6309
|
+
if (typeof value === "string") {
|
|
6310
|
+
const trimmed = value.trim();
|
|
6311
|
+
return trimmed || "-";
|
|
6312
|
+
}
|
|
6313
|
+
return JSON.stringify(value);
|
|
6314
|
+
}
|
|
6315
|
+
|
|
6316
|
+
function flattenLeadData(data: LeadData) {
|
|
6317
|
+
return Object.entries(data)
|
|
6318
|
+
.filter(([key]) => !key.startsWith("_"))
|
|
6319
|
+
.slice(0, 20)
|
|
6320
|
+
.map(([key, value]) => ({ label: key, value: safeString(value) }));
|
|
6321
|
+
}
|
|
6322
|
+
|
|
6323
|
+
export function LeadNotificationEmail({
|
|
6324
|
+
tenantName,
|
|
6325
|
+
correlationId,
|
|
6326
|
+
replyTo,
|
|
6327
|
+
leadData,
|
|
6328
|
+
brandName,
|
|
6329
|
+
logoUrl,
|
|
6330
|
+
logoAlt,
|
|
6331
|
+
tagline,
|
|
6332
|
+
theme,
|
|
6333
|
+
}: LeadNotificationEmailProps) {
|
|
6334
|
+
const fields = flattenLeadData(leadData);
|
|
6335
|
+
const brandLabel = brandName || tenantName;
|
|
6336
|
+
|
|
6337
|
+
const colors = {
|
|
6338
|
+
primary: theme?.colors?.primary || "#2D5016",
|
|
6339
|
+
background: theme?.colors?.background || "#FAFAF5",
|
|
6340
|
+
surface: theme?.colors?.surface || "#FFFFFF",
|
|
6341
|
+
text: theme?.colors?.text || "#1C1C14",
|
|
6342
|
+
textMuted: theme?.colors?.textMuted || "#5A5A4A",
|
|
6343
|
+
border: theme?.colors?.border || "#D8D5C5",
|
|
6344
|
+
};
|
|
6345
|
+
|
|
6346
|
+
const fonts = {
|
|
6347
|
+
primary: theme?.typography?.fontFamily?.primary || "Inter, Arial, sans-serif",
|
|
6348
|
+
display: theme?.typography?.fontFamily?.display || "Georgia, serif",
|
|
6349
|
+
};
|
|
6350
|
+
|
|
6351
|
+
const radius = {
|
|
6352
|
+
md: theme?.borderRadius?.md || "10px",
|
|
6353
|
+
lg: theme?.borderRadius?.lg || "16px",
|
|
6354
|
+
};
|
|
6355
|
+
|
|
6356
|
+
return (
|
|
6357
|
+
<Html>
|
|
6358
|
+
<Head />
|
|
6359
|
+
<Preview>Nuovo lead ricevuto da {brandLabel}</Preview>
|
|
6360
|
+
<Body style={{ backgroundColor: colors.background, color: colors.text, fontFamily: fonts.primary, padding: "24px" }}>
|
|
6361
|
+
<Container style={{ backgroundColor: colors.surface, border: `1px solid ${colors.border}`, borderRadius: radius.lg, padding: "24px" }}>
|
|
6362
|
+
<Section>
|
|
6363
|
+
{logoUrl ? <Img src={logoUrl} alt={logoAlt || brandLabel} height="44" style={{ marginBottom: "8px" }} /> : null}
|
|
6364
|
+
<Text style={{ color: colors.text, fontSize: "18px", fontWeight: 700, margin: "0 0 6px 0" }}>{brandLabel}</Text>
|
|
6365
|
+
<Text style={{ color: colors.textMuted, marginTop: "0", marginBottom: "0" }}>{tagline || "Notifica automatica lead"}</Text>
|
|
6366
|
+
</Section>
|
|
6367
|
+
|
|
6368
|
+
<Hr style={{ borderColor: colors.border, margin: "20px 0" }} />
|
|
6369
|
+
|
|
6370
|
+
<Heading as="h2" style={{ color: colors.text, margin: "0 0 12px 0", fontSize: "22px", fontFamily: fonts.display }}>
|
|
6371
|
+
Nuovo lead da {tenantName}
|
|
6372
|
+
</Heading>
|
|
6373
|
+
<Text style={{ color: colors.textMuted, marginTop: "0", marginBottom: "16px" }}>Correlation ID: {correlationId}</Text>
|
|
6374
|
+
|
|
6375
|
+
<Section style={{ border: `1px solid ${colors.border}`, borderRadius: radius.md, padding: "12px" }}>
|
|
6376
|
+
{fields.length === 0 ? (
|
|
6377
|
+
<Text style={{ color: colors.textMuted, margin: 0 }}>Nessun campo lead disponibile.</Text>
|
|
6378
|
+
) : (
|
|
6379
|
+
fields.map((field) => (
|
|
6380
|
+
<Text key={field.label} style={{ margin: "0 0 8px 0", color: colors.text, fontSize: "14px", wordBreak: "break-word" }}>
|
|
6381
|
+
<strong>{field.label}:</strong> {field.value}
|
|
6382
|
+
</Text>
|
|
6383
|
+
))
|
|
6384
|
+
)}
|
|
6385
|
+
</Section>
|
|
6386
|
+
|
|
6387
|
+
<Section style={{ marginTop: "18px" }}>
|
|
6388
|
+
<Button
|
|
6389
|
+
href={replyTo ? `mailto:${replyTo}` : "mailto:"}
|
|
6390
|
+
style={{
|
|
6391
|
+
backgroundColor: colors.primary,
|
|
6392
|
+
color: "#ffffff",
|
|
6393
|
+
borderRadius: radius.md,
|
|
6394
|
+
textDecoration: "none",
|
|
6395
|
+
padding: "12px 18px",
|
|
6396
|
+
fontWeight: 600,
|
|
6397
|
+
}}
|
|
6398
|
+
>
|
|
6399
|
+
Rispondi ora
|
|
6400
|
+
</Button>
|
|
6401
|
+
</Section>
|
|
6402
|
+
</Container>
|
|
6403
|
+
</Body>
|
|
6404
|
+
</Html>
|
|
6405
|
+
);
|
|
6406
|
+
}
|
|
6407
|
+
|
|
6408
|
+
export default LeadNotificationEmail;
|
|
6409
|
+
|
|
6410
|
+
END_OF_FILE_CONTENT
|
|
6411
|
+
echo "Creating src/emails/LeadSenderConfirmationEmail.tsx..."
|
|
6412
|
+
cat << 'END_OF_FILE_CONTENT' > "src/emails/LeadSenderConfirmationEmail.tsx"
|
|
6413
|
+
import {
|
|
6414
|
+
Body,
|
|
6415
|
+
Container,
|
|
6416
|
+
Head,
|
|
6417
|
+
Heading,
|
|
6418
|
+
Hr,
|
|
6419
|
+
Html,
|
|
6420
|
+
Img,
|
|
6421
|
+
Preview,
|
|
6422
|
+
Section,
|
|
6423
|
+
Text,
|
|
6424
|
+
} from "@react-email/components";
|
|
6425
|
+
|
|
6426
|
+
type LeadData = Record<string, unknown>;
|
|
6427
|
+
|
|
6428
|
+
type EmailTheme = {
|
|
6429
|
+
colors?: {
|
|
6430
|
+
primary?: string;
|
|
6431
|
+
secondary?: string;
|
|
6432
|
+
accent?: string;
|
|
6433
|
+
background?: string;
|
|
6434
|
+
surface?: string;
|
|
6435
|
+
surfaceAlt?: string;
|
|
6436
|
+
text?: string;
|
|
6437
|
+
textMuted?: string;
|
|
6438
|
+
border?: string;
|
|
6439
|
+
};
|
|
6440
|
+
typography?: {
|
|
6441
|
+
fontFamily?: {
|
|
6442
|
+
primary?: string;
|
|
6443
|
+
display?: string;
|
|
6444
|
+
mono?: string;
|
|
6445
|
+
};
|
|
6446
|
+
};
|
|
6447
|
+
borderRadius?: {
|
|
6448
|
+
sm?: string;
|
|
6449
|
+
md?: string;
|
|
6450
|
+
lg?: string;
|
|
6451
|
+
xl?: string;
|
|
6452
|
+
};
|
|
6453
|
+
};
|
|
6454
|
+
|
|
6455
|
+
export type LeadSenderConfirmationEmailProps = {
|
|
6456
|
+
tenantName: string;
|
|
6457
|
+
correlationId: string;
|
|
6458
|
+
leadData: LeadData;
|
|
6459
|
+
brandName?: string;
|
|
6460
|
+
logoUrl?: string;
|
|
6461
|
+
logoAlt?: string;
|
|
6462
|
+
tagline?: string;
|
|
6463
|
+
theme?: EmailTheme;
|
|
6464
|
+
};
|
|
6465
|
+
|
|
6466
|
+
function safeString(value: unknown): string {
|
|
6467
|
+
if (value == null) return "-";
|
|
6468
|
+
if (typeof value === "string") {
|
|
6469
|
+
const trimmed = value.trim();
|
|
6470
|
+
return trimmed || "-";
|
|
6471
|
+
}
|
|
6472
|
+
return JSON.stringify(value);
|
|
6473
|
+
}
|
|
6474
|
+
|
|
6475
|
+
function flattenLeadData(data: LeadData) {
|
|
6476
|
+
const skipKeys = new Set(["recipientEmail", "tenant", "source", "submittedAt", "email_confirm"]);
|
|
6477
|
+
return Object.entries(data)
|
|
6478
|
+
.filter(([key]) => !key.startsWith("_") && !skipKeys.has(key))
|
|
6479
|
+
.slice(0, 12)
|
|
6480
|
+
.map(([key, value]) => ({ label: key, value: safeString(value) }));
|
|
6481
|
+
}
|
|
6482
|
+
|
|
6483
|
+
export function LeadSenderConfirmationEmail({
|
|
6484
|
+
tenantName,
|
|
6485
|
+
correlationId,
|
|
6486
|
+
leadData,
|
|
6487
|
+
brandName,
|
|
6488
|
+
logoUrl,
|
|
6489
|
+
logoAlt,
|
|
6490
|
+
tagline,
|
|
6491
|
+
theme,
|
|
6492
|
+
}: LeadSenderConfirmationEmailProps) {
|
|
6493
|
+
const fields = flattenLeadData(leadData);
|
|
6494
|
+
const brandLabel = brandName || tenantName;
|
|
6495
|
+
|
|
6496
|
+
const colors = {
|
|
6497
|
+
primary: theme?.colors?.primary || "#2D5016",
|
|
6498
|
+
background: theme?.colors?.background || "#FAFAF5",
|
|
6499
|
+
surface: theme?.colors?.surface || "#FFFFFF",
|
|
6500
|
+
text: theme?.colors?.text || "#1C1C14",
|
|
6501
|
+
textMuted: theme?.colors?.textMuted || "#5A5A4A",
|
|
6502
|
+
border: theme?.colors?.border || "#D8D5C5",
|
|
6503
|
+
};
|
|
6504
|
+
|
|
6505
|
+
const fonts = {
|
|
6506
|
+
primary: theme?.typography?.fontFamily?.primary || "Inter, Arial, sans-serif",
|
|
6507
|
+
display: theme?.typography?.fontFamily?.display || "Georgia, serif",
|
|
6508
|
+
};
|
|
6509
|
+
|
|
6510
|
+
const radius = {
|
|
6511
|
+
md: theme?.borderRadius?.md || "10px",
|
|
6512
|
+
lg: theme?.borderRadius?.lg || "16px",
|
|
6513
|
+
};
|
|
6514
|
+
|
|
6515
|
+
return (
|
|
6516
|
+
<Html>
|
|
6517
|
+
<Head />
|
|
6518
|
+
<Preview>Conferma invio richiesta - {brandLabel}</Preview>
|
|
6519
|
+
<Body style={{ backgroundColor: colors.background, color: colors.background, fontFamily: fonts.primary, padding: "24px" }}>
|
|
6520
|
+
<Container style={{ backgroundColor: colors.primary, color: colors.background, border: `1px solid ${colors.border}`, borderRadius: radius.lg, padding: "24px" }}>
|
|
6521
|
+
<Section>
|
|
6522
|
+
{logoUrl ? <Img src={logoUrl} alt={logoAlt || brandLabel} height="44" style={{ marginBottom: "8px" }} /> : null}
|
|
6523
|
+
<Text style={{ color: colors.background, fontSize: "18px", fontWeight: 700, margin: "0 0 6px 0" }}>{brandLabel}</Text>
|
|
6524
|
+
<Text style={{ color: colors.background, marginTop: "0", marginBottom: "0" }}>{tagline || "Conferma automatica di ricezione"}</Text>
|
|
6525
|
+
</Section>
|
|
6526
|
+
|
|
6527
|
+
<Hr style={{ borderColor: colors.border, margin: "20px 0" }} />
|
|
6528
|
+
|
|
6529
|
+
<Heading as="h2" style={{ color: colors.background, margin: "0 0 12px 0", fontSize: "22px", fontFamily: fonts.display }}>
|
|
6530
|
+
Richiesta ricevuta
|
|
6531
|
+
</Heading>
|
|
6532
|
+
<Text style={{ color: colors.background, marginTop: "0", marginBottom: "16px" }}>
|
|
6533
|
+
Grazie, abbiamo ricevuto la tua richiesta per {tenantName}. Ti risponderemo il prima possibile.
|
|
6534
|
+
</Text>
|
|
6535
|
+
|
|
6536
|
+
<Section style={{ border: `1px solid ${colors.border}`, borderRadius: radius.md, padding: "12px" }}>
|
|
6537
|
+
<Text style={{ margin: "0 0 8px 0", color: colors.background, fontWeight: 600 }}>Riepilogo inviato</Text>
|
|
6538
|
+
{fields.length === 0 ? (
|
|
6539
|
+
<Text style={{ color: colors.background, margin: 0 }}>Nessun dettaglio disponibile.</Text>
|
|
6540
|
+
) : (
|
|
6541
|
+
fields.map((field) => (
|
|
6542
|
+
<Text key={field.label} style={{ margin: "0 0 8px 0", color: colors.background, fontSize: "14px", wordBreak: "break-word" }}>
|
|
6543
|
+
<strong>{field.label}:</strong> {field.value}
|
|
6544
|
+
</Text>
|
|
6545
|
+
))
|
|
6546
|
+
)}
|
|
6547
|
+
</Section>
|
|
6548
|
+
|
|
6549
|
+
<Hr style={{ borderColor: colors.border, margin: "20px 0 12px 0" }} />
|
|
6550
|
+
<Text style={{ color: colors.background, fontSize: "12px", margin: 0 }}>Riferimento richiesta: {correlationId}</Text>
|
|
6551
|
+
</Container>
|
|
6552
|
+
</Body>
|
|
6553
|
+
</Html>
|
|
6554
|
+
);
|
|
6555
|
+
}
|
|
6556
|
+
|
|
6557
|
+
export default LeadSenderConfirmationEmail;
|
|
6558
|
+
|
|
5196
6559
|
END_OF_FILE_CONTENT
|
|
5197
6560
|
echo "Creating src/fonts.css..."
|
|
5198
6561
|
cat << 'END_OF_FILE_CONTENT' > "src/fonts.css"
|
|
@@ -5412,6 +6775,61 @@ html {
|
|
|
5412
6775
|
pointer-events: none;
|
|
5413
6776
|
}
|
|
5414
6777
|
|
|
6778
|
+
/* ==========================================================================
|
|
6779
|
+
TIPTAP — Public content typography (visitor view)
|
|
6780
|
+
ReactMarkdown renders plain HTML; preflight resets it. Re-apply here.
|
|
6781
|
+
========================================================================== */
|
|
6782
|
+
.jp-tiptap-content > * + * { margin-top: 0.75em; }
|
|
6783
|
+
|
|
6784
|
+
.jp-tiptap-content h1 { font-size: 2em; font-weight: 700; line-height: 1.2; margin-top: 1.25em; margin-bottom: 0.25em; }
|
|
6785
|
+
.jp-tiptap-content h2 { font-size: 1.5em; font-weight: 700; line-height: 1.3; margin-top: 1.25em; margin-bottom: 0.25em; }
|
|
6786
|
+
.jp-tiptap-content h3 { font-size: 1.25em; font-weight: 600; line-height: 1.4; margin-top: 1.25em; margin-bottom: 0.25em; }
|
|
6787
|
+
.jp-tiptap-content h4 { font-size: 1em; font-weight: 600; line-height: 1.5; margin-top: 1em; margin-bottom: 0.25em; }
|
|
6788
|
+
|
|
6789
|
+
.jp-tiptap-content p { line-height: 1.7; }
|
|
6790
|
+
|
|
6791
|
+
.jp-tiptap-content strong { font-weight: 700; }
|
|
6792
|
+
.jp-tiptap-content em { font-style: italic; }
|
|
6793
|
+
.jp-tiptap-content s { text-decoration: line-through; }
|
|
6794
|
+
|
|
6795
|
+
.jp-tiptap-content a { color: var(--primary); text-decoration: underline; text-underline-offset: 2px; }
|
|
6796
|
+
.jp-tiptap-content a:hover { opacity: 0.8; }
|
|
6797
|
+
|
|
6798
|
+
.jp-tiptap-content code {
|
|
6799
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
6800
|
+
font-size: 0.875em;
|
|
6801
|
+
background: color-mix(in oklch, var(--foreground) 8%, transparent);
|
|
6802
|
+
border-radius: 0.25em;
|
|
6803
|
+
padding: 0.1em 0.35em;
|
|
6804
|
+
}
|
|
6805
|
+
.jp-tiptap-content pre {
|
|
6806
|
+
background: color-mix(in oklch, var(--background) 60%, black);
|
|
6807
|
+
border-radius: 0.5em;
|
|
6808
|
+
padding: 1em 1.25em;
|
|
6809
|
+
overflow-x: auto;
|
|
6810
|
+
}
|
|
6811
|
+
.jp-tiptap-content pre code { background: none; padding: 0; }
|
|
6812
|
+
|
|
6813
|
+
.jp-tiptap-content ul { list-style-type: disc; padding-left: 1.625em; }
|
|
6814
|
+
.jp-tiptap-content ol { list-style-type: decimal; padding-left: 1.625em; }
|
|
6815
|
+
.jp-tiptap-content li { line-height: 1.7; margin-top: 0.25em; }
|
|
6816
|
+
.jp-tiptap-content li + li { margin-top: 0.25em; }
|
|
6817
|
+
|
|
6818
|
+
.jp-tiptap-content blockquote {
|
|
6819
|
+
border-left: 3px solid var(--border);
|
|
6820
|
+
padding-left: 1em;
|
|
6821
|
+
color: var(--muted-foreground);
|
|
6822
|
+
font-style: italic;
|
|
6823
|
+
}
|
|
6824
|
+
|
|
6825
|
+
.jp-tiptap-content hr {
|
|
6826
|
+
border: none;
|
|
6827
|
+
border-top: 1px solid var(--border);
|
|
6828
|
+
margin: 1.5em 0;
|
|
6829
|
+
}
|
|
6830
|
+
|
|
6831
|
+
.jp-tiptap-content img { max-width: 100%; height: auto; border-radius: 0.5rem; }
|
|
6832
|
+
|
|
5415
6833
|
/* ==========================================================================
|
|
5416
6834
|
TIPTAP / PROSEMIRROR — Editor typography
|
|
5417
6835
|
Tailwind preflight resets all heading/list styles. Re-apply here using
|
|
@@ -5556,7 +6974,6 @@ export const ComponentRegistry: {
|
|
|
5556
6974
|
};
|
|
5557
6975
|
|
|
5558
6976
|
END_OF_FILE_CONTENT
|
|
5559
|
-
# SKIP: src/lib/ComponentRegistry.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
5560
6977
|
echo "Creating src/lib/IconResolver.tsx..."
|
|
5561
6978
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/IconResolver.tsx"
|
|
5562
6979
|
import React from 'react';
|
|
@@ -5615,7 +7032,6 @@ export const Icon: React.FC<IconProps> = ({ name, size = 20, className }) => {
|
|
|
5615
7032
|
|
|
5616
7033
|
|
|
5617
7034
|
END_OF_FILE_CONTENT
|
|
5618
|
-
# SKIP: src/lib/IconResolver.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
5619
7035
|
echo "Creating src/lib/addSectionConfig.ts..."
|
|
5620
7036
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/addSectionConfig.ts"
|
|
5621
7037
|
import type { AddSectionConfig } from '@jsonpages/core';
|
|
@@ -5677,7 +7093,6 @@ export const addSectionConfig: AddSectionConfig = {
|
|
|
5677
7093
|
};
|
|
5678
7094
|
|
|
5679
7095
|
END_OF_FILE_CONTENT
|
|
5680
|
-
# SKIP: src/lib/addSectionConfig.ts:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
5681
7096
|
echo "Creating src/lib/base-schemas.ts..."
|
|
5682
7097
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/base-schemas.ts"
|
|
5683
7098
|
import { z } from 'zod';
|
|
@@ -5720,11 +7135,6 @@ export const CtaSchema = z.object({
|
|
|
5720
7135
|
});
|
|
5721
7136
|
|
|
5722
7137
|
END_OF_FILE_CONTENT
|
|
5723
|
-
# SKIP: src/lib/base-schemas.ts:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
5724
|
-
# SKIP: src/lib/cloudSaveStream.ts:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
5725
|
-
# SKIP: src/lib/cloudSaveStream.ts:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
5726
|
-
# SKIP: src/lib/deploySteps.ts:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
5727
|
-
# SKIP: src/lib/deploySteps.ts:Zone.Identifier:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
5728
7138
|
echo "Creating src/lib/draftStorage.ts..."
|
|
5729
7139
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/draftStorage.ts"
|
|
5730
7140
|
/**
|
|
@@ -5753,7 +7163,6 @@ export function getHydratedData(
|
|
|
5753
7163
|
}
|
|
5754
7164
|
|
|
5755
7165
|
END_OF_FILE_CONTENT
|
|
5756
|
-
# SKIP: src/lib/draftStorage.ts:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
5757
7166
|
echo "Creating src/lib/getFilePages.ts..."
|
|
5758
7167
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/getFilePages.ts"
|
|
5759
7168
|
/**
|
|
@@ -5802,7 +7211,6 @@ export function getFilePages(): Record<string, PageConfig> {
|
|
|
5802
7211
|
}
|
|
5803
7212
|
|
|
5804
7213
|
END_OF_FILE_CONTENT
|
|
5805
|
-
# SKIP: src/lib/getFilePages.ts:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
5806
7214
|
echo "Creating src/lib/schemas.ts..."
|
|
5807
7215
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/schemas.ts"
|
|
5808
7216
|
export { BaseSectionData, BaseArrayItem, BaseSectionSettingsSchema, CtaSchema } from './base-schemas';
|
|
@@ -5852,7 +7260,95 @@ export const SECTION_SCHEMAS = {
|
|
|
5852
7260
|
export type SectionType = keyof typeof SECTION_SCHEMAS;
|
|
5853
7261
|
|
|
5854
7262
|
END_OF_FILE_CONTENT
|
|
5855
|
-
|
|
7263
|
+
echo "Creating src/lib/useFormSubmit.ts..."
|
|
7264
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/useFormSubmit.ts"
|
|
7265
|
+
import { useState, useCallback } from 'react';
|
|
7266
|
+
|
|
7267
|
+
export type SubmitStatus = 'idle' | 'submitting' | 'success' | 'error';
|
|
7268
|
+
|
|
7269
|
+
interface UseFormSubmitOptions {
|
|
7270
|
+
source: string;
|
|
7271
|
+
tenantId: string;
|
|
7272
|
+
}
|
|
7273
|
+
|
|
7274
|
+
export function useFormSubmit({ source, tenantId }: UseFormSubmitOptions) {
|
|
7275
|
+
const [status, setStatus] = useState<SubmitStatus>('idle');
|
|
7276
|
+
const [message, setMessage] = useState<string>('');
|
|
7277
|
+
|
|
7278
|
+
const submit = useCallback(async (
|
|
7279
|
+
formData: FormData,
|
|
7280
|
+
recipientEmail: string,
|
|
7281
|
+
pageSlug: string,
|
|
7282
|
+
sectionId: string
|
|
7283
|
+
) => {
|
|
7284
|
+
const cloudApiUrl = import.meta.env.VITE_JSONPAGES_CLOUD_URL as string | undefined;
|
|
7285
|
+
const cloudApiKey = import.meta.env.VITE_JSONPAGES_API_KEY as string | undefined;
|
|
7286
|
+
|
|
7287
|
+
if (!cloudApiUrl || !cloudApiKey) {
|
|
7288
|
+
setStatus('error');
|
|
7289
|
+
setMessage('Configurazione API non disponibile. Riprova tra poco.');
|
|
7290
|
+
return false;
|
|
7291
|
+
}
|
|
7292
|
+
|
|
7293
|
+
// Trasformiamo FormData in un oggetto piatto per il payload JSON
|
|
7294
|
+
const data: Record<string, any> = {};
|
|
7295
|
+
formData.forEach((value, key) => {
|
|
7296
|
+
data[key] = String(value).trim();
|
|
7297
|
+
});
|
|
7298
|
+
|
|
7299
|
+
const payload = {
|
|
7300
|
+
...data,
|
|
7301
|
+
recipientEmail,
|
|
7302
|
+
page: pageSlug,
|
|
7303
|
+
section: sectionId,
|
|
7304
|
+
tenant: tenantId,
|
|
7305
|
+
source: source,
|
|
7306
|
+
submittedAt: new Date().toISOString(),
|
|
7307
|
+
};
|
|
7308
|
+
|
|
7309
|
+
// Idempotency Key per evitare doppi invii accidentali
|
|
7310
|
+
const idempotencyKey = `form-${sectionId}-${Date.now()}`;
|
|
7311
|
+
|
|
7312
|
+
setStatus('submitting');
|
|
7313
|
+
setMessage('Invio in corso...');
|
|
7314
|
+
|
|
7315
|
+
try {
|
|
7316
|
+
const apiBase = cloudApiUrl.replace(/\/$/, '');
|
|
7317
|
+
const response = await fetch(`${apiBase}/forms/submit`, {
|
|
7318
|
+
method: 'POST',
|
|
7319
|
+
headers: {
|
|
7320
|
+
Authorization: `Bearer ${cloudApiKey}`,
|
|
7321
|
+
'Content-Type': 'application/json',
|
|
7322
|
+
'Idempotency-Key': idempotencyKey,
|
|
7323
|
+
},
|
|
7324
|
+
body: JSON.stringify(payload),
|
|
7325
|
+
});
|
|
7326
|
+
|
|
7327
|
+
const body = (await response.json().catch(() => ({}))) as { error?: string; code?: string };
|
|
7328
|
+
|
|
7329
|
+
if (!response.ok) {
|
|
7330
|
+
throw new Error(body.error || body.code || `Submit failed (${response.status})`);
|
|
7331
|
+
}
|
|
7332
|
+
|
|
7333
|
+
setStatus('success');
|
|
7334
|
+
setMessage('Richiesta inviata con successo. Ti risponderemo al più presto.');
|
|
7335
|
+
return true;
|
|
7336
|
+
} catch (error: unknown) {
|
|
7337
|
+
const errorMsg = error instanceof Error ? error.message : 'Invio non riuscito. Riprova tra poco.';
|
|
7338
|
+
setStatus('error');
|
|
7339
|
+
setMessage(errorMsg);
|
|
7340
|
+
return false;
|
|
7341
|
+
}
|
|
7342
|
+
}, [source, tenantId]);
|
|
7343
|
+
|
|
7344
|
+
const reset = useCallback(() => {
|
|
7345
|
+
setStatus('idle');
|
|
7346
|
+
setMessage('');
|
|
7347
|
+
}, []);
|
|
7348
|
+
|
|
7349
|
+
return { submit, status, message, reset };
|
|
7350
|
+
}
|
|
7351
|
+
END_OF_FILE_CONTENT
|
|
5856
7352
|
echo "Creating src/lib/utils.ts..."
|
|
5857
7353
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/utils.ts"
|
|
5858
7354
|
import { clsx, type ClassValue } from 'clsx';
|
|
@@ -5863,7 +7359,6 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
5863
7359
|
}
|
|
5864
7360
|
|
|
5865
7361
|
END_OF_FILE_CONTENT
|
|
5866
|
-
# SKIP: src/lib/utils.ts:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
5867
7362
|
echo "Creating src/main.tsx..."
|
|
5868
7363
|
cat << 'END_OF_FILE_CONTENT' > "src/main.tsx"
|
|
5869
7364
|
import '@/types'; // TBP: load type augmentation from capsule-driven types
|
|
@@ -5882,7 +7377,6 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
5882
7377
|
|
|
5883
7378
|
|
|
5884
7379
|
END_OF_FILE_CONTENT
|
|
5885
|
-
# SKIP: src/main.tsx:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
5886
7380
|
echo "Creating src/main_.tsx..."
|
|
5887
7381
|
cat << 'END_OF_FILE_CONTENT' > "src/main_.tsx"
|
|
5888
7382
|
import '@/types'; // TBP: load type augmentation from capsule-driven types
|
|
@@ -5997,7 +7491,6 @@ declare module '@jsonpages/core' {
|
|
|
5997
7491
|
export * from '@jsonpages/core';
|
|
5998
7492
|
|
|
5999
7493
|
END_OF_FILE_CONTENT
|
|
6000
|
-
# SKIP: src/types/deploy.ts:Zone.Identifier è un file binario e non può essere convertito in testo.
|
|
6001
7494
|
echo "Creating src/vite-env.d.ts..."
|
|
6002
7495
|
cat << 'END_OF_FILE_CONTENT' > "src/vite-env.d.ts"
|
|
6003
7496
|
/// <reference types="vite/client" />
|