@jsonpages/cli 3.0.64 → 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 +821 -2
- 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,7 +584,7 @@ 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",
|
|
587
|
+
"dist": "bash ./src2Code.sh src index.html scripts docs package.json",
|
|
39
588
|
"preview": "vite preview",
|
|
40
589
|
"bake:email": "tsx scripts/bake-email.tsx",
|
|
41
590
|
"bakemail": "npm run bake:email --"
|
|
@@ -45,7 +594,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
45
594
|
"@tiptap/extension-link": "^2.11.5",
|
|
46
595
|
"@tiptap/react": "^2.11.5",
|
|
47
596
|
"@tiptap/starter-kit": "^2.11.5",
|
|
48
|
-
"@jsonpages/core": "^1.0.
|
|
597
|
+
"@jsonpages/core": "^1.0.52",
|
|
49
598
|
"clsx": "^2.1.1",
|
|
50
599
|
"lucide-react": "^0.474.0",
|
|
51
600
|
"react": "^19.0.0",
|
|
@@ -71,6 +620,276 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
71
620
|
}
|
|
72
621
|
}
|
|
73
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);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
main().catch((error) => {
|
|
889
|
+
console.error(error instanceof Error ? error.message : error);
|
|
890
|
+
process.exit(1);
|
|
891
|
+
});
|
|
892
|
+
|
|
74
893
|
END_OF_FILE_CONTENT
|
|
75
894
|
mkdir -p "src"
|
|
76
895
|
echo "Creating src/App.tsx..."
|