@olonjs/cli 3.0.121 → 3.0.123

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.
@@ -1631,9 +1631,9 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
1631
1631
  "dev": "vite",
1632
1632
  "dev:clean": "vite --force",
1633
1633
  "verify:webmcp": "node scripts/webmcp-feature-check.mjs",
1634
- "prebuild": "node scripts/sync-pages-to-public.mjs",
1634
+ "prebuild": "node scripts/sync-pages-to-public.mjs && node scripts/generate-llms-txt.mjs",
1635
1635
  "build": "tsc && vite build",
1636
- "dist": "bash ./src2Code.sh --template alpha src .cursor vercel.json index.html tsconfig.json tsconfig.node.json vite.config.ts scripts specs package.json public/assets/images/plug-graded-square.jpg",
1636
+ "dist": "bash ./src2Code.sh --template alpha src .cursor vercel.json index.html tsconfig.json tsconfig.node.json vite.config.ts scripts ../../specs package.json public/assets/images/plug-graded-square.jpg",
1637
1637
  "preview": "vite preview",
1638
1638
  "bake:email": "tsx scripts/bake-email.tsx",
1639
1639
  "bakemail": "npm run bake:email --",
@@ -1644,7 +1644,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
1644
1644
  "@tiptap/extension-link": "^2.11.5",
1645
1645
  "@tiptap/react": "^2.11.5",
1646
1646
  "@tiptap/starter-kit": "^2.11.5",
1647
- "@olonjs/core": "^1.0.110",
1647
+ "@olonjs/core": "^1.0.112",
1648
1648
  "class-variance-authority": "^0.7.1",
1649
1649
  "clsx": "^2.1.1",
1650
1650
  "lucide-react": "^0.474.0",
@@ -1959,11 +1959,7 @@ import { build } from 'vite';
1959
1959
  import path from 'path';
1960
1960
  import { fileURLToPath, pathToFileURL } from 'url';
1961
1961
  import fs from 'fs/promises';
1962
- import { createRequire } from 'module';
1963
-
1964
- const require = createRequire(import.meta.url);
1965
- const corePkgPath = require.resolve('@olonjs/core/package.json');
1966
- const contractsUrl = pathToFileURL(corePkgPath.replace('package.json', 'src/lib/webmcp-contracts.mjs')).href;
1962
+ import { webmcp } from '@olonjs/core';
1967
1963
 
1968
1964
  const {
1969
1965
  buildPageContract,
@@ -1971,7 +1967,7 @@ const {
1971
1967
  buildPageManifestHref,
1972
1968
  buildSiteManifest,
1973
1969
  buildLlmsTxt,
1974
- } = await import(contractsUrl);
1970
+ } = webmcp;
1975
1971
 
1976
1972
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
1977
1973
  const root = path.resolve(__dirname, '..');
@@ -2055,7 +2051,9 @@ await build({
2055
2051
  },
2056
2052
  },
2057
2053
  ssr: {
2058
- noExternal: ['@olonjs/core'],
2054
+ // SSG must be self-contained: the SSR artifact should not depend on
2055
+ // runtime resolution of app/framework packages at bake time.
2056
+ noExternal: true,
2059
2057
  },
2060
2058
  });
2061
2059
  console.log('[bake] SSR build done.');
@@ -2168,6 +2166,47 @@ for (const { slug, out, depth } of targets) {
2168
2166
 
2169
2167
  console.log('\n[bake] All pages baked. OK\n');
2170
2168
 
2169
+ END_OF_FILE_CONTENT
2170
+ echo "Creating scripts/generate-llms-txt.mjs..."
2171
+ cat << 'END_OF_FILE_CONTENT' > "scripts/generate-llms-txt.mjs"
2172
+ import fs from 'fs';
2173
+ import path from 'path';
2174
+ import { fileURLToPath } from 'url';
2175
+ import { webmcp } from '@olonjs/core';
2176
+
2177
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
2178
+ const rootDir = path.resolve(__dirname, '..');
2179
+ const { buildLlmsTxt } = webmcp;
2180
+
2181
+ const pagesDir = path.join(rootDir, 'src', 'data', 'pages');
2182
+ const siteConfig = JSON.parse(fs.readFileSync(path.join(rootDir, 'src', 'data', 'config', 'site.json'), 'utf-8'));
2183
+
2184
+ function listJsonFilesRecursive(dir) {
2185
+ const items = fs.readdirSync(dir, { withFileTypes: true });
2186
+ const files = [];
2187
+ for (const item of items) {
2188
+ const fullPath = path.join(dir, item.name);
2189
+ if (item.isDirectory()) {
2190
+ files.push(...listJsonFilesRecursive(fullPath));
2191
+ continue;
2192
+ }
2193
+ if (item.isFile() && item.name.toLowerCase().endsWith('.json')) files.push(fullPath);
2194
+ }
2195
+ return files;
2196
+ }
2197
+
2198
+ const pages = {};
2199
+ for (const fullPath of listJsonFilesRecursive(pagesDir)) {
2200
+ const slug = path.relative(pagesDir, fullPath).replace(/\\/g, '/').replace(/\.json$/i, '');
2201
+ pages[slug] = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
2202
+ }
2203
+
2204
+ const llmsTxt = buildLlmsTxt({ pages, schemas: {}, siteConfig });
2205
+
2206
+ const outPath = path.join(rootDir, 'public', 'llms.txt');
2207
+ fs.writeFileSync(outPath, llmsTxt, 'utf-8');
2208
+ console.log('[generate-llms-txt] Written -> public/llms.txt');
2209
+
2171
2210
  END_OF_FILE_CONTENT
2172
2211
  echo "Creating scripts/sync-pages-to-public.mjs..."
2173
2212
  cat << 'END_OF_FILE_CONTENT' > "scripts/sync-pages-to-public.mjs"
@@ -2180,6 +2219,9 @@ const __dirname = path.dirname(__filename);
2180
2219
  const rootDir = path.resolve(__dirname, '..');
2181
2220
  const sourceDir = path.join(rootDir, 'src', 'data', 'pages');
2182
2221
  const targetDir = path.join(rootDir, 'public', 'pages');
2222
+ const sourceSiteConfigPath = path.join(rootDir, 'src', 'data', 'config', 'site.json');
2223
+ const targetConfigDir = path.join(rootDir, 'public', 'config');
2224
+ const targetSiteConfigPath = path.join(targetConfigDir, 'site.json');
2183
2225
 
2184
2226
  if (!fs.existsSync(sourceDir)) {
2185
2227
  console.warn('[sync-pages-to-public] Source directory not found:', sourceDir);
@@ -2190,7 +2232,12 @@ fs.rmSync(targetDir, { recursive: true, force: true });
2190
2232
  fs.mkdirSync(targetDir, { recursive: true });
2191
2233
  fs.cpSync(sourceDir, targetDir, { recursive: true });
2192
2234
 
2193
- console.log('[sync-pages-to-public] Synced pages to public/pages');
2235
+ if (fs.existsSync(sourceSiteConfigPath)) {
2236
+ fs.mkdirSync(targetConfigDir, { recursive: true });
2237
+ fs.cpSync(sourceSiteConfigPath, targetSiteConfigPath);
2238
+ }
2239
+
2240
+ console.log('[sync-pages-to-public] Synced pages to public/pages and site config to public/config/site.json');
2194
2241
 
2195
2242
  END_OF_FILE_CONTENT
2196
2243
  echo "Creating scripts/webmcp-feature-check.mjs..."
@@ -2494,615 +2541,6 @@ main().catch((error) => {
2494
2541
  process.exit(1);
2495
2542
  });
2496
2543
 
2497
- END_OF_FILE_CONTENT
2498
- mkdir -p "specs"
2499
- echo "Creating specs/olonjsSpecs_V.1.3.md..."
2500
- cat << 'END_OF_FILE_CONTENT' > "specs/olonjsSpecs_V.1.3.md"
2501
- # 📐 OlonJS Architecture Specifications v1.3
2502
-
2503
- **Status:** Mandatory Standard
2504
- **Version:** 1.3.0 (Sovereign Core Edition — Architecture + Studio/ICE UX, Path-Deterministic Nested Editing)
2505
- **Target:** Senior Architects / AI Agents / Enterprise Governance
2506
-
2507
- **Scope v1.3:** This edition preserves the complete v1.2 architecture (MTRP, JSP, TBP, CIP, ECIP, JAP + Studio/ICE UX contract: IDAC, TOCC, BSDS, ASC, JEB + Tenant Type & Code-Generation Annex) as a **faithful superset**, and adds strict path-based/nested-array behavior for Studio selection and Inspector expansion.
2508
- **Scope note (breaking):** In strict v1.3 Studio semantics, the legacy flat protocol (`itemField` / `itemId`) is removed in favor of `itemPath` (root-to-leaf path segments).
2509
-
2510
- ---
2511
-
2512
- ## 1. 📐 Modular Type Registry Pattern (MTRP) v1.2
2513
-
2514
- **Objective:** Establish a strictly typed, open-ended protocol for extending content data structures where the **Core Engine** is the orchestrator and the **Tenant** is the provider.
2515
-
2516
- ### 1.1 The Sovereign Dependency Inversion
2517
- The **Core** defines the empty `SectionDataRegistry`. The **Tenant** "injects" its specific definitions using **Module Augmentation**. This allows the Core to be distributed as a compiled NPM package while remaining aware of Tenant-specific types at compile-time.
2518
-
2519
- ### 1.2 Technical Implementation (`@olonjs/core/kernel`)
2520
- ```typescript
2521
- export interface SectionDataRegistry {} // Augmented by Tenant
2522
- export interface SectionSettingsRegistry {} // Augmented by Tenant
2523
-
2524
- export interface BaseSection<K extends keyof SectionDataRegistry> {
2525
- id: string;
2526
- type: K;
2527
- data: SectionDataRegistry[K];
2528
- settings?: K extends keyof SectionSettingsRegistry
2529
- ? SectionSettingsRegistry[K]
2530
- : BaseSectionSettings;
2531
- }
2532
-
2533
- export type Section = {
2534
- [K in keyof SectionDataRegistry]: BaseSection<K>
2535
- }[keyof SectionDataRegistry];
2536
- ```
2537
-
2538
- **SectionType:** Core exports (or Tenant infers) **`SectionType`** as **`keyof SectionDataRegistry`**. After Tenant module augmentation, this is the union of all section type keys (e.g. `'header' | 'footer' | 'hero' | ...`). The Tenant uses this type for the ComponentRegistry and SECTION_SCHEMAS keys.
2539
-
2540
- **Perché servono:** Il Core deve poter renderizzare section senza conoscere i tipi concreti a compile-time; il Tenant deve poter aggiungere nuovi tipi senza modificare il Core. I registry vuoti + module augmentation permettono di distribuire Core come pacchetto NPM e mantenere type-safety end-to-end (Section, registry, config). Senza MTRP, ogni nuovo tipo richiederebbe cambi nel Core o tipi deboli (`any`).
2541
-
2542
- ---
2543
-
2544
- ## 2. 📐 JsonPages Site Protocol (JSP) v1.8
2545
-
2546
- **Objective:** Define the deterministic file system and the **Sovereign Projection Engine** (CLI).
2547
-
2548
- ### 2.1 The File System Ontology (The Silo Contract)
2549
- Every site must reside in an isolated directory. Global Governance is physically separated from Local Content.
2550
- * **`/config/site.json`** — Global Identity & Reserved System Blocks (Header/Footer). See Appendix A for typed shape.
2551
- * **`/config/menu.json`** — Navigation Tree (SSOT for System Header). See Appendix A.
2552
- * **`/config/theme.json`** — Theme tokens (optional but recommended). See Appendix A.
2553
- * **`/pages/[slug].json`** — Local Body Content per page. See Appendix A (PageConfig).
2554
-
2555
- **Application path convention:** The runtime app typically imports these via an alias (e.g. **`@/data/config/`** and **`@/data/pages/`**). The physical silo may be `src/data/config/` and `src/data/pages/` so that `site.json`, `menu.json`, `theme.json` live under `src/data/config/`, and page JSONs under `src/data/pages/`. The CLI or projection script may use `/config/` and `/pages/` at repo root; the **contract** is that the app receives **siteConfig**, **menuConfig**, **themeConfig**, and **pages** as defined in JEB (§10) and Appendix A.
2556
-
2557
- ### 2.2 Deterministic Projection (CLI Workflow)
2558
- The CLI (`@olonjs/cli`) creates new tenants by:
2559
- 1. **Infra Projection:** Generating `package.json`, `tsconfig.json`, and `vite.config.ts` (The Shell).
2560
- 2. **Source Projection:** Executing a deterministic script (`src_tenant_alpha.sh`) to reconstruct the `src` folder (The DNA).
2561
- 3. **Dependency Resolution:** Enforcing specific versions of React, Radix, and Tailwind v4.
2562
-
2563
- **Perché servono:** Una struttura file deterministica (config vs pages) separa governance globale (site, menu, theme) dal contenuto per pagina; il CLI può rigenerare tenant e tooling può trovare dati e schemi sempre negli stessi path. Senza JSP, ogni tenant sarebbe una struttura ad hoc e ingestione/export/Bake sarebbero fragili.
2564
-
2565
- ---
2566
-
2567
- ## 3. 🧱 Tenant Block Protocol (TBP) v1.0
2568
-
2569
- **Objective:** Standardize the "Capsule" structure for components to enable automated ingestion (Pull) by the SaaS.
2570
-
2571
- ### 3.1 The Atomic Capsule Structure
2572
- Components are self-contained directories under **`src/components/<sectionType>/`**:
2573
- * **`View.tsx`** — The pure React component (Dumb View). Props: see Appendix A (SectionComponentPropsMap).
2574
- * **`schema.ts`** — Zod schema(s) for the **data** contract (and optionally **settings**). Exports at least one schema (e.g. `HeroSchema`) used as the **data** schema for that type. Must extend BaseSectionData (§8) for data; array items must extend BaseArrayItem (§8).
2575
- * **`types.ts`** — TypeScript interfaces inferred from the schema (e.g. `HeroData`, `HeroSettings`). Export types with names **`<SectionType>Data`** and **`<SectionType>Settings`** (or equivalent) so the Tenant can aggregate them in a single types module.
2576
- * **`index.ts`** — Public API: re-exports View, schema(s), and types.
2577
-
2578
- ### 3.2 Reserved System Types
2579
- * **`type: 'header'`** — Reserved for `site.json`. Receives **`menu: MenuItem[]`** in addition to `data` and `settings`. Menu is sourced from `menu.json` (see Appendix A). The Tenant **must** type `SectionComponentPropsMap['header']` as `{ data: HeaderData; settings?: HeaderSettings; menu: MenuItem[] }`.
2580
- * **`type: 'footer'`** — Reserved for `site.json`. Props: `{ data: FooterData; settings?: FooterSettings }` only (no `menu`).
2581
- * **`type: 'sectionHeader'`** — A standard local block. Must define its own `links` array in its local schema if used.
2582
-
2583
- **Perché servono:** La capsula (View + schema + types + index) è l’unità di estensione: il Core e il Form Factory possono scoprire tipi e contratti per tipo senza convenzioni ad hoc. Header/footer riservati evitano conflitti tra globale e locale. Senza TBP, aggregazione di SECTION_SCHEMAS e registry sarebbe incoerente e l’ingestion da SaaS non sarebbe automatizzabile.
2584
-
2585
- ---
2586
-
2587
- ## 4. 🧱 Component Implementation Protocol (CIP) v1.5
2588
-
2589
- **Objective:** Ensure system-wide stability and Admin UI integrity.
2590
-
2591
- 1. **The "Sovereign View" Law:** Components receive `data` and `settings` (and `menu` for header only) and return JSX. They are metadata-blind (never import Zod schemas).
2592
- 2. **Z-Index Neutrality:** Components must not use `z-index > 1`. Layout delegation (sticky/fixed) is managed by the `SectionRenderer`.
2593
- 3. **Agnostic Asset Protocol:** Use `resolveAssetUrl(path, tenantId)` for all media. Resolved URLs are under **`/assets/...`** with no tenantId segment in the path (e.g. relative `img/hero.jpg` → `/assets/img/hero.jpg`).
2594
-
2595
- ### 4.4 Local Design Tokens (v1.2)
2596
- Section Views that control their own background, text, borders, or radii **shall** define a **local scope** via an inline `style` object on the section root: e.g. `--local-bg`, `--local-text`, `--local-text-muted`, `--local-surface`, `--local-border`, `--local-radius-lg`, `--local-accent`, mapped to theme variables. All Tailwind classes that affect color or radius in that section **must** use these variables (e.g. `bg-[var(--local-bg)]`, `text-[var(--local-text)]`). No naked utilities (e.g. `bg-blue-500`). An optional **`label`** in section data may be rendered with class **`jp-section-label`** for overlay type labels.
2597
-
2598
- ### 4.5 Z-Index & Overlay Governance (v1.2)
2599
- Section content root **must** stay at **`z-index` ≤ 1** (prefer `z-0`) so the Sovereign Overlay can sit above with high z-index in Tenant CSS (§7). Header/footer may use a higher z-index (e.g. 50) only as a documented exception for global chrome.
2600
-
2601
- **Perché servono (CIP):** View “dumb” (solo data/settings) e senza import di Zod evita accoppiamento e permette al Form Factory di essere l’unica fonte di verità sugli schemi. Z-index basso evita che il contenuto copra l’overlay di selezione in Studio. Asset via `resolveAssetUrl`: i path relativi vengono risolti in `/assets/...` (senza segmento tenantId nel path). Token locali (`--local-*`) rendono le section temabili e coerenti con overlay e tema; senza, stili “nudi” creano drift visivo e conflitti con l’UI di editing.
2602
-
2603
- ---
2604
-
2605
- ## 5. 🛠️ Editor Component Implementation Protocol (ECIP) v1.5
2606
-
2607
- **Objective:** Standardize the Polymorphic ICE engine.
2608
-
2609
- 1. **Recursive Form Factory:** The Admin UI builds forms by traversing the Zod ontology.
2610
- 2. **UI Metadata:** Use `.describe('ui:[widget]')` in schemas to pass instructions to the Form Factory.
2611
- 3. **Deterministic IDs:** Every object in a `ZodArray` must extend `BaseArrayItem` (containing an `id`) to ensure React reconciliation stability during reordering.
2612
-
2613
- ### 5.4 UI Metadata Vocabulary (v1.2)
2614
- Standard keys for the Form Factory:
2615
-
2616
- | Key | Use case |
2617
- |-----|----------|
2618
- | `ui:text` | Single-line text input. |
2619
- | `ui:textarea` | Multi-line text. |
2620
- | `ui:select` | Enum / single choice. |
2621
- | `ui:number` | Numeric input. |
2622
- | `ui:list` | Array of items; list editor (add/remove/reorder). |
2623
- | `ui:icon-picker` | Icon selection. |
2624
-
2625
- Unknown keys may be treated as `ui:text`. Array fields must use `BaseArrayItem` for items.
2626
-
2627
- ### 5.5 Path-Only Nested Selection & Expansion (v1.3, breaking)
2628
- In strict v1.3 Studio/Inspector behavior, nested editing targets are represented by **path segments from root to leaf**.
2629
-
2630
- ```typescript
2631
- export type SelectionPathSegment = { fieldKey: string; itemId?: string };
2632
- export type SelectionPath = SelectionPathSegment[];
2633
- ```
2634
-
2635
- Rules:
2636
- * Expansion and focus for nested arrays **must** be computed from `SelectionPath` (root → leaf), not from a single flat pair.
2637
- * Matching by `fieldKey` alone is non-compliant for nested structures.
2638
- * Legacy flat payload fields **`itemField`** and **`itemId`** are removed from strict v1.3 selection protocol.
2639
-
2640
- **Perché servono (ECIP):** Il Form Factory deve sapere quale widget usare (text, textarea, select, list, …) senza hardcodare per tipo; `.describe('ui:...')` è il contratto. BaseArrayItem con `id` su ogni item di array garantisce chiavi stabili in React e reorder/delete corretti nell’Inspector. In v1.3 la selezione/espansione path-only elimina ambiguità su array annidati: senza path completo root→leaf, la sidebar può aprire il ramo sbagliato o non aprire il target.
2641
-
2642
- ---
2643
-
2644
- ## 6. 🎯 ICE Data Attribute Contract (IDAC) v1.1
2645
-
2646
- **Objective:** Mandatory data attributes so the Stage (iframe) and Inspector can bind selection and field/item editing without coupling to Tenant DOM.
2647
-
2648
- ### 6.1 Section-Level Markup (Core-Provided)
2649
- **SectionRenderer** (Core) wraps each section root with:
2650
- * **`data-section-id`** — Section instance ID (e.g. UUID). On the wrapper that contains content + overlay.
2651
- * Sibling overlay element **`data-jp-section-overlay`** — Selection ring and type label. **Tenant does not add this;** Core injects it.
2652
-
2653
- Tenant Views render the **content** root only (e.g. `<section>` or `<div>`), placed **inside** the Core wrapper.
2654
-
2655
- ### 6.2 Field-Level Binding (Tenant-Provided)
2656
- For every **editable scalar field** the View **must** attach **`data-jp-field="<fieldKey>"`** (key matches schema path: e.g. `title`, `description`, `sectionTitle`, `label`).
2657
-
2658
- ### 6.3 Array-Item Binding (Tenant-Provided)
2659
- For every **editable array item** the View **must** attach:
2660
- * **`data-jp-item-id="<stableId>"`** — Prefer `item.id`; fallback e.g. `legacy-${index}` only outside strict mode.
2661
- * **`data-jp-item-field="<arrayKey>"`** — e.g. `cards`, `layers`, `products`, `paragraphs`.
2662
-
2663
- ### 6.4 Compliance
2664
- **Reserved types** (`header`, `footer`): ICE attributes optional unless Studio edits them. **All other section types** in the Stage and in `SECTION_SCHEMAS` **must** implement §6.2 and §6.3 for every editable field and array item.
2665
-
2666
- ### 6.5 Strict Path Extraction for Nested Arrays (v1.3, breaking)
2667
- For nested array targets, the Core/Inspector contract is path-based:
2668
- * The runtime selection target is expressed as `itemPath: SelectionPath` (root → leaf).
2669
- * Flat identity (`itemField` + `itemId`) is not sufficient for nested structures and is removed in strict v1.3 payloads.
2670
- * In strict mode, index-based identity fallback is non-compliant for editable object arrays.
2671
-
2672
- **Perché servono (IDAC):** Lo Stage è in un iframe e l’Inspector deve sapere **quale campo o item** corrisponde al click (o alla selezione) senza conoscere la struttura DOM del Tenant. **`data-jp-field`** associa un nodo DOM al path dello schema (es. `title`, `description`): così il Core può evidenziare la riga giusta nella sidebar, applicare opacità attivo/inattivo e aprire il form sul campo corretto. **`data-jp-item-id`** e **`data-jp-item-field`** fanno lo stesso per gli item di array (liste, reorder, delete). In v1.3, `itemPath` rende deterministico anche il caso nested (array dentro array), eliminando mismatch tra selezione canvas e ramo aperto in sidebar.
2673
-
2674
- ---
2675
-
2676
- ## 7. 🎨 Tenant Overlay CSS Contract (TOCC) v1.0
2677
-
2678
- **Objective:** The Stage iframe loads only Tenant HTML/CSS. Core injects overlay **markup** but does **not** ship overlay styles. The Tenant **must** supply CSS so overlay is visible.
2679
-
2680
- ### 7.1 Required Selectors (Tenant global CSS)
2681
- 1. **`[data-jp-section-overlay]`** — `position: absolute; inset: 0`; `pointer-events: none`; base state transparent.
2682
- 2. **`[data-section-id]:hover [data-jp-section-overlay]`** — Hover: e.g. dashed border, subtle tint.
2683
- 3. **`[data-section-id][data-jp-selected] [data-jp-section-overlay]`** — Selected: solid border, optional tint.
2684
- 4. **`[data-jp-section-overlay] > div`** (type label) — Position and visibility (e.g. visible on hover/selected).
2685
-
2686
- ### 7.2 Z-Index
2687
- Overlay **z-index** high (e.g. 9999). Section content at or below CIP limit (§4.5).
2688
-
2689
- ### 7.3 Responsibility
2690
- **Core:** Injects wrapper and overlay DOM; sets `data-jp-selected`. **Tenant:** All overlay **visual** rules.
2691
-
2692
- **Perché servono (TOCC):** L’iframe dello Stage carica solo HTML/CSS del Tenant; il Core inietta il markup dell’overlay ma non gli stili. Senza CSS Tenant per i selettori TOCC, bordo hover/selected e type label non sarebbero visibili: l’autore non vedrebbe quale section è selezionata né il label del tipo. TOCC chiarisce la responsabilità (Core = markup, Tenant = aspetto) e garantisce UX uniforme tra tenant.
2693
-
2694
- ---
2695
-
2696
- ## 8. 📦 Base Section Data & Settings (BSDS) v1.0
2697
-
2698
- **Objective:** Standardize base schema fragments for anchors, array items, and section settings.
2699
-
2700
- ### 8.1 BaseSectionData
2701
- Every section data schema **must** extend a base with at least **`anchorId`** (optional string). Canonical Zod (Tenant `lib/base-schemas.ts` or equivalent):
2702
-
2703
- ```typescript
2704
- export const BaseSectionData = z.object({
2705
- anchorId: z.string().optional().describe('ui:text'),
2706
- });
2707
- ```
2708
-
2709
- ### 8.2 BaseArrayItem
2710
- Every array item schema editable in the Inspector **must** include **`id`** (optional string minimum). Canonical Zod:
2711
-
2712
- ```typescript
2713
- export const BaseArrayItem = z.object({
2714
- id: z.string().optional(),
2715
- });
2716
- ```
2717
-
2718
- Recommended: required UUID for new items. Used by `data-jp-item-id` and React reconciliation.
2719
-
2720
- ### 8.3 BaseSectionSettings (Optional)
2721
- Common section-level settings. Canonical Zod (name **BaseSectionSettingsSchema** or as exported by Core):
2722
-
2723
- ```typescript
2724
- export const BaseSectionSettingsSchema = z.object({
2725
- paddingTop: z.enum(['none', 'sm', 'md', 'lg', 'xl', '2xl']).default('md').describe('ui:select'),
2726
- paddingBottom: z.enum(['none', 'sm', 'md', 'lg', 'xl', '2xl']).default('md').describe('ui:select'),
2727
- theme: z.enum(['dark', 'light', 'accent']).default('dark').describe('ui:select'),
2728
- container: z.enum(['boxed', 'fluid']).default('boxed').describe('ui:select'),
2729
- });
2730
- ```
2731
-
2732
- Capsules may extend this for type-specific settings. Core may export **BaseSectionSettings** as the TypeScript type inferred from this or a superset.
2733
-
2734
- **Perché servono (BSDS):** anchorId permette deep-link e navigazione in-page; id sugli array item è necessario per `data-jp-item-id`, reorder e React reconciliation. BaseSectionSettings comuni (padding, theme, container) evitano ripetizione e allineano il Form Factory tra capsule. Senza base condivisi, ogni capsule inventa convenzioni e validazione/add-section diventano fragili.
2735
-
2736
- ---
2737
-
2738
- ## 9. 📌 AddSectionConfig (ASC) v1.0
2739
-
2740
- **Objective:** Formalize the "Add Section" contract used by the Studio.
2741
-
2742
- **Type (Core exports `AddSectionConfig`):**
2743
- ```typescript
2744
- interface AddSectionConfig {
2745
- addableSectionTypes: readonly string[];
2746
- sectionTypeLabels: Record<string, string>;
2747
- getDefaultSectionData(sectionType: string): Record<string, unknown>;
2748
- }
2749
- ```
2750
-
2751
- **Shape:** Tenant provides one object (e.g. `addSectionConfig`) with:
2752
- * **`addableSectionTypes`** — Readonly array of section type keys. Only these types appear in the Add Section Library. Must be a subset of (or equal to) the keys in SectionDataRegistry.
2753
- * **`sectionTypeLabels`** — Map type key → display string (e.g. `{ hero: 'Hero', 'cta-banner': 'CTA Banner' }`).
2754
- * **`getDefaultSectionData(sectionType: string): Record<string, unknown>`** — Returns default `data` for a new section. Must conform to the capsule’s data schema so the new section validates.
2755
-
2756
- Core creates a new section with deterministic UUID, `type`, and `data` from `getDefaultSectionData(type)`.
2757
-
2758
- **Perché servono (ASC):** Lo Studio deve mostrare una libreria “Aggiungi sezione” con nomi leggibili e, alla scelta, creare una section con dati iniziali validi. addableSectionTypes, sectionTypeLabels e getDefaultSectionData sono il contratto: il Tenant è l’unica fonte di verità su quali tipi sono addabili e con quali default. Senza ASC, il Core non saprebbe cosa mostrare in modal né come popolare i dati della nuova section.
2759
-
2760
- ---
2761
-
2762
- ## 10. ⚙️ JsonPagesConfig & Engine Bootstrap (JEB) v1.1
2763
-
2764
- **Objective:** Bootstrap contract between Tenant app and `@olonjs/core`.
2765
-
2766
- ### 10.1 JsonPagesConfig (required fields)
2767
- The Tenant passes a single **config** object to **JsonPagesEngine**. Required fields:
2768
-
2769
- | Field | Type | Description |
2770
- |-------|------|-------------|
2771
- | **tenantId** | string | Passed to `resolveAssetUrl(path, tenantId)`; resolved asset URLs are **`/assets/...`** with no tenantId segment in the path. |
2772
- | **registry** | `{ [K in SectionType]: React.FC<SectionComponentPropsMap[K]> }` | Component registry. Must match MTRP keys. See Appendix A. |
2773
- | **schemas** | `Record<SectionType, ZodType>` or equivalent | SECTION_SCHEMAS: type → **data** Zod schema. Form Factory uses this. See Appendix A. |
2774
- | **pages** | `Record<string, PageConfig>` | Slug → page config. See Appendix A. |
2775
- | **siteConfig** | SiteConfig | Global site (identity, header/footer blocks). See Appendix A. |
2776
- | **themeConfig** | ThemeConfig | Theme tokens. See Appendix A. |
2777
- | **menuConfig** | MenuConfig | Navigation tree (SSOT for header menu). See Appendix A. |
2778
- | **themeCss** | `{ tenant: string }` | At least **tenant**: string (inline CSS or URL) for Stage iframe injection. |
2779
- | **addSection** | AddSectionConfig | Add-section config (§9). |
2780
-
2781
- Core may define optional fields. The Tenant must not omit required fields.
2782
-
2783
- ### 10.2 JsonPagesEngine
2784
- Root component: **`<JsonPagesEngine config={config} />`**. Responsibilities: route → page, SectionRenderer per section; in Studio mode Sovereign Shell (Inspector, Control Bar, postMessage); section wrappers and overlay per IDAC and JAP. Tenant does not implement the Shell.
2785
-
2786
- ### 10.3 Studio Selection Event Contract (v1.3, breaking)
2787
- In strict v1.3 Studio, section selection payload for nested targets is path-based:
2788
-
2789
- ```typescript
2790
- type SectionSelectMessage = {
2791
- type: 'SECTION_SELECT';
2792
- section: { id: string; type: string; scope: 'global' | 'local' };
2793
- itemPath?: SelectionPath; // root -> leaf
2794
- };
2795
- ```
2796
-
2797
- Removed from strict protocol:
2798
- * `itemField`
2799
- * `itemId`
2800
-
2801
- **Perché servono (JEB):** Un unico punto di bootstrap (config + Engine) evita che il Tenant replichi logica di routing, Shell e overlay. I campi obbligatori in JsonPagesConfig (tenantId, registry, schemas, pages, siteConfig, themeConfig, menuConfig, themeCss, addSection) sono il minimo per far funzionare rendering, Studio e Form Factory; omissioni causano errori a runtime. In v1.3, il payload `itemPath` sincronizza in modo non ambiguo Stage e Inspector su nested arrays.
2802
-
2803
- ---
2804
-
2805
- # 🏛️ OlonJS_ADMIN_PROTOCOL (JAP) v1.2
2806
-
2807
- **Status:** Mandatory Standard
2808
- **Version:** 1.2.0 (Sovereign Shell Edition — Path/Nested Strictness)
2809
- **Objective:** Deterministic orchestration of the "Studio" environment (ICE Level 1).
2810
-
2811
- ---
2812
-
2813
- ## 1. The Sovereign Shell Topology
2814
- The Admin interface is a **Sovereign Shell** from `@olonjs/core`.
2815
- 1. **The Stage (Canvas):** Isolated Iframe; postMessage for data updates and selection mirroring. Section markup follows **IDAC** (§6); overlay styling follows **TOCC** (§7).
2816
- 2. **The Inspector (Sidebar):** Consumes Tenant Zod schemas to generate editors; binding via `data-jp-field` and `data-jp-item-*`.
2817
- 3. **The Studio Actions:** Save to file, Hot Save, Add Section.
2818
-
2819
- ## 2. State Orchestration & Persistence
2820
- * **Working Draft:** Reactive local state for unsaved changes.
2821
- * **Sync Law:** Inspector changes → Working Draft → Stage via `STUDIO_EVENTS.UPDATE_DRAFTS`.
2822
- * **Persistence Protocol:** Studio invokes tenant-provided `saveToFile` and `hotSave` callbacks for editorial persistence.
2823
-
2824
- ## 3. Context Switching (Global vs. Local)
2825
- * **Header/Footer** selection → Global Mode, `site.json`.
2826
- * Any other section → Page Mode, current `[slug].json`.
2827
-
2828
- ## 4. Section Lifecycle Management
2829
- 1. **Add Section:** Modal from Tenant `SECTION_SCHEMAS`; UUID + default data via **AddSectionConfig** (§9).
2830
- 2. **Reorder:** Inspector or Stage Overlay; array mutation in Working Draft.
2831
- 3. **Delete:** Confirmation; remove from array, clear selection.
2832
-
2833
- ## 5. Stage Isolation & Overlay
2834
- * **CSS Shielding:** Stage in Iframe; Tenant CSS does not leak into Admin.
2835
- * **Sovereign Overlay:** Selection ring and type labels injected per **IDAC** (§6); Tenant styles them per **TOCC** (§7).
2836
-
2837
- ## 6. "Green Build" Validation
2838
- Studio enforces `tsc && vite build`. No Studio or SSG build should proceed with TypeScript errors.
2839
-
2840
- ## 7. Path-Deterministic Selection & Sidebar Expansion (v1.3, breaking)
2841
- * Section/item focus synchronization uses `itemPath` (root → leaf), not flat `itemField/itemId`.
2842
- * Sidebar expansion state for nested arrays must be derived from all path segments.
2843
- * Flat-only matching may open/close wrong branches and is non-compliant in strict mode.
2844
-
2845
- **Perché servono (JAP):** Stage in iframe + Inspector + Studio actions separano il contesto di editing dal sito; postMessage e Working Draft permettono modifiche senza toccare subito i file. Save to file e Hot Save richiedono uno stato coerente. Global vs Page mode evita confusione su dove si sta editando (site.json vs [slug].json). Add/Reorder/Delete sono gestiti in un solo modo (Working Draft + ASC). Green Build garantisce che Studio e SSG compilino correttamente. In v1.3, il path completo elimina ambiguità nella sincronizzazione Stage↔Sidebar su strutture annidate.
2846
-
2847
- ---
2848
-
2849
- ## Compliance: Legacy vs Full UX (v1.3)
2850
-
2851
- | Dimension | Legacy / Less UX | Full UX (Core-aligned) |
2852
- |-----------|-------------------|-------------------------|
2853
- | **ICE binding** | No `data-jp-*`; Inspector cannot bind. | IDAC (§6) on every editable section/field/item. |
2854
- | **Section wrapper** | Plain `<section>`; no overlay contract. | Core wrapper + overlay; Tenant CSS per TOCC (§7). |
2855
- | **Design tokens** | Raw BEM / fixed classes. | Local tokens (§4.4); `var(--local-*)` only. |
2856
- | **Base schemas** | Ad hoc. | BSDS (§8): BaseSectionData, BaseArrayItem, BaseSectionSettings. |
2857
- | **Add Section** | Ad hoc defaults. | ASC (§9): addableSectionTypes, labels, getDefaultSectionData. |
2858
- | **Bootstrap** | Implicit. | JEB (§10): JsonPagesConfig + JsonPagesEngine. |
2859
- | **Selection payload** | Flat `itemField/itemId`. | Path-only `itemPath: SelectionPath` (JEB §10.3). |
2860
- | **Nested array expansion** | Single-segment or field-only heuristics. | Root-to-leaf path expansion (ECIP §5.5, JAP §7). |
2861
- | **Array item identity (strict)** | Index fallback tolerated. | Stable `id` required for editable object arrays. |
2862
-
2863
- **Rule:** Every page section (non-header/footer) that appears in the Stage and in `SECTION_SCHEMAS` must comply with §6, §7, §4.4, §8, §9, §10 for full Studio UX.
2864
-
2865
- ---
2866
-
2867
- ## Summary of v1.3 Additions
2868
-
2869
- | § | Title | Purpose |
2870
- |---|--------|--------|
2871
- | 5.5 | Path-Only Nested Selection & Expansion | ECIP: root→leaf `SelectionPath`; remove flat matching in strict mode. |
2872
- | 6.5 | Strict Path Extraction for Nested Arrays | IDAC: path-based nested targeting; no strict flat fallback. |
2873
- | 10.3 | Studio Selection Event Contract | JEB: `SECTION_SELECT` uses `itemPath`; remove `itemField/itemId`. |
2874
- | JAP §7 | Path-Deterministic Selection & Sidebar Expansion | Studio state synchronization for nested arrays. |
2875
- | Compliance | Legacy vs Full UX (v1.3) | Explicit breaking delta for flat protocol removal and strict IDs. |
2876
- | **Appendix A.6** | **v1.3 Path/Nested Strictness Addendum** | Type/export and migration checklist for path-only protocol. |
2877
-
2878
- ---
2879
-
2880
- # Appendix A — Tenant Type & Code-Generation Annex
2881
-
2882
- **Objective:** Make the specification **sufficient** to generate or audit a full tenant (new site, new components, new data) without a reference codebase. Defines TypeScript types, JSON shapes, schema contract, file paths, and integration pattern.
2883
-
2884
- **Status:** Mandatory for code-generation and governance. Compliance ensures generated tenants are typed and wired like the reference implementation.
2885
-
2886
- ---
2887
-
2888
- ## A.1 Core-Provided Types (from `@olonjs/core`)
2889
-
2890
- The following are assumed to be exported by Core. The Tenant augments **SectionDataRegistry** and **SectionSettingsRegistry**; all other types are consumed as-is.
2891
-
2892
- | Type | Description |
2893
- |------|-------------|
2894
- | **SectionType** | `keyof SectionDataRegistry` (after Tenant augmentation). Union of all section type keys. |
2895
- | **Section** | Union of `BaseSection<K>` for all K in SectionDataRegistry. See MTRP §1.2. |
2896
- | **BaseSectionSettings** | Optional base type for section settings (may align with BSDS §8.3). |
2897
- | **MenuItem** | Navigation item. **Minimum shape:** `{ label: string; href: string }`. Core may extend (e.g. `children?: MenuItem[]`). |
2898
- | **AddSectionConfig** | See §9. |
2899
- | **JsonPagesConfig** | See §10.1. |
2900
-
2901
- **Perché servono (A.1):** Il Tenant deve conoscere i tipi esportati dal Core (SectionType, MenuItem, AddSectionConfig, JsonPagesConfig) per tipizzare registry, config e augmentation senza dipendere da implementazioni interne.
2902
-
2903
- ---
2904
-
2905
- ## A.2 Tenant-Provided Types (single source: `src/types.ts` or equivalent)
2906
-
2907
- The Tenant **must** define the following in one module (e.g. **`src/types.ts`**). This module **must** perform the **module augmentation** of `@olonjs/core` for **SectionDataRegistry** and **SectionSettingsRegistry**, and **must** export **SectionComponentPropsMap** and re-export from `@olonjs/core` so that **SectionType** is available after augmentation.
2908
-
2909
- ### A.2.1 SectionComponentPropsMap
2910
-
2911
- Maps each section type to the props of its React component. **Header** is the only type that receives **menu**.
2912
-
2913
- **Option A — Explicit (recommended for clarity and tooling):** For each section type K, add one entry. Header receives **menu**.
2914
-
2915
- ```typescript
2916
- import type { MenuItem } from '@olonjs/core';
2917
- // Import Data/Settings from each capsule.
2918
-
2919
- export type SectionComponentPropsMap = {
2920
- 'header': { data: HeaderData; settings?: HeaderSettings; menu: MenuItem[] };
2921
- 'footer': { data: FooterData; settings?: FooterSettings };
2922
- 'hero': { data: HeroData; settings?: HeroSettings };
2923
- // ... one entry per SectionType, e.g. 'feature-grid', 'cta-banner', etc.
2924
- };
2925
- ```
2926
-
2927
- **Option B — Mapped type (DRY, requires SectionDataRegistry/SectionSettingsRegistry in scope):**
2928
-
2929
- ```typescript
2930
- import type { MenuItem } from '@olonjs/core';
2931
-
2932
- export type SectionComponentPropsMap = {
2933
- [K in SectionType]: K extends 'header'
2934
- ? { data: SectionDataRegistry[K]; settings?: SectionSettingsRegistry[K]; menu: MenuItem[] }
2935
- : { data: SectionDataRegistry[K]; settings?: K extends keyof SectionSettingsRegistry ? SectionSettingsRegistry[K] : BaseSectionSettings };
2936
- };
2937
- ```
2938
-
2939
- SectionType is imported from Core (after Tenant augmentation). In practice Option A is the reference pattern; Option B is valid if the Tenant prefers a single derived definition.
2940
-
2941
- **Perché servono (A.2):** SectionComponentPropsMap e i tipi di config (PageConfig, SiteConfig, MenuConfig, ThemeConfig) definiscono il contratto tra dati (JSON, API) e componente; l’augmentation è l’unico modo per estendere i registry del Core senza fork. Senza questi tipi, generazione tenant e refactor sarebbero senza guida e il type-check fallirebbe.
2942
-
2943
- ### A.2.2 ComponentRegistry type
2944
-
2945
- The registry object **must** be typed as:
2946
-
2947
- ```typescript
2948
- import type { SectionType } from '@olonjs/core';
2949
- import type { SectionComponentPropsMap } from '@/types';
2950
-
2951
- export const ComponentRegistry: {
2952
- [K in SectionType]: React.FC<SectionComponentPropsMap[K]>;
2953
- } = { /* ... */ };
2954
- ```
2955
-
2956
- File: **`src/lib/ComponentRegistry.tsx`** (or equivalent). Imports one View per section type and assigns it to the corresponding key.
2957
-
2958
- ### A.2.3 PageConfig
2959
-
2960
- Minimum shape for a single page (used in **pages** and in each **`[slug].json`**):
2961
-
2962
- ```typescript
2963
- export interface PageConfig {
2964
- id?: string;
2965
- slug: string;
2966
- meta?: {
2967
- title?: string;
2968
- description?: string;
2969
- };
2970
- sections: Section[];
2971
- }
2972
- ```
2973
-
2974
- **Section** is the union type from MTRP (§1.2). Each element of **sections** has **id**, **type**, **data**, **settings** and conforms to the capsule schemas.
2975
-
2976
- ### A.2.4 SiteConfig
2977
-
2978
- Minimum shape for **site.json** (and for **siteConfig** in JsonPagesConfig):
2979
-
2980
- ```typescript
2981
- export interface SiteConfigIdentity {
2982
- title?: string;
2983
- logoUrl?: string;
2984
- }
2985
-
2986
- export interface SiteConfig {
2987
- identity?: SiteConfigIdentity;
2988
- pages?: Array<{ slug: string; label: string }>;
2989
- header: {
2990
- id: string;
2991
- type: 'header';
2992
- data: HeaderData;
2993
- settings?: HeaderSettings;
2994
- };
2995
- footer: {
2996
- id: string;
2997
- type: 'footer';
2998
- data: FooterData;
2999
- settings?: FooterSettings;
3000
- };
3001
- }
3002
- ```
3003
-
3004
- **HeaderData**, **FooterData**, **HeaderSettings**, **FooterSettings** are the types exported from the header and footer capsules.
3005
-
3006
- ### A.2.5 MenuConfig
3007
-
3008
- Minimum shape for **menu.json** (and for **menuConfig** in JsonPagesConfig). Structure is tenant-defined; Core expects the header to receive **MenuItem[]**. Common pattern: an object with a key (e.g. **main**) whose value is **MenuItem[]**.
3009
-
3010
- ```typescript
3011
- export interface MenuConfig {
3012
- main?: MenuItem[];
3013
- [key: string]: MenuItem[] | undefined;
3014
- }
3015
- ```
3016
-
3017
- Or simply **`MenuItem[]`** if the app uses a single flat list. The Tenant must ensure that the value passed to the header component as **menu** conforms to **MenuItem[]** (e.g. `menuConfig.main` or `menuConfig` if it is the array).
3018
-
3019
- ### A.2.6 ThemeConfig
3020
-
3021
- Minimum shape for **theme.json** (and for **themeConfig** in JsonPagesConfig). Tenant-defined; typically tokens for colors, typography, radius.
3022
-
3023
- ```typescript
3024
- export interface ThemeConfig {
3025
- name?: string;
3026
- tokens?: {
3027
- colors?: Record<string, string>;
3028
- typography?: Record<string, string | Record<string, string>>;
3029
- borderRadius?: Record<string, string>;
3030
- };
3031
- [key: string]: unknown;
3032
- }
3033
- ```
3034
-
3035
- ---
3036
-
3037
- ## A.3 Schema Contract (SECTION_SCHEMAS)
3038
-
3039
- **Location:** **`src/lib/schemas.ts`** (or equivalent).
3040
-
3041
- **Contract:**
3042
- * **SECTION_SCHEMAS** is a **single object** whose keys are **SectionType** and whose values are **Zod schemas for the section data** (not settings, unless the Form Factory contract expects a combined or per-type settings schema; then each value may be the data schema only, and settings may be defined per capsule and aggregated elsewhere if needed).
3043
- * The Tenant **must** re-export **BaseSectionData**, **BaseArrayItem**, and optionally **BaseSectionSettingsSchema** from **`src/lib/base-schemas.ts`** (or equivalent). Each capsule’s data schema **must** extend BaseSectionData; each array item schema **must** extend or include BaseArrayItem.
3044
- * **SECTION_SCHEMAS** is typed as **`Record<SectionType, ZodType>`** or **`{ [K in SectionType]: ZodType }`** so that keys match the registry and SectionDataRegistry.
3045
-
3046
- **Export:** The app imports **SECTION_SCHEMAS** and passes it as **config.schemas** to JsonPagesEngine. The Form Factory traverses these schemas to build editors.
3047
-
3048
- **Perché servono (A.3):** Un unico oggetto SECTION_SCHEMAS con chiavi = SectionType e valori = schema data permette al Form Factory di costruire form per tipo senza convenzioni ad hoc; i base schema garantiscono anchorId e id su item. Senza questo contratto, l’Inspector non saprebbe quali campi mostrare né come validare.
3049
-
3050
- ---
3051
-
3052
- ## A.4 File Paths & Data Layout
3053
-
3054
- | Purpose | Path (conventional) | Description |
3055
- |---------|---------------------|-------------|
3056
- | Site config | **`src/data/config/site.json`** | SiteConfig (identity, header, footer, pages list). |
3057
- | Menu config | **`src/data/config/menu.json`** | MenuConfig (e.g. main nav). |
3058
- | Theme config | **`src/data/config/theme.json`** | ThemeConfig (tokens). |
3059
- | Page data | **`src/data/pages/<slug>.json`** | One file per page; content is PageConfig (slug, meta, sections). |
3060
- | Base schemas | **`src/lib/base-schemas.ts`** | BaseSectionData, BaseArrayItem, BaseSectionSettingsSchema. |
3061
- | Schema aggregate | **`src/lib/schemas.ts`** | SECTION_SCHEMAS; re-exports base schemas. |
3062
- | Registry | **`src/lib/ComponentRegistry.tsx`** | ComponentRegistry object. |
3063
- | Add-section config | **`src/lib/addSectionConfig.ts`** | addSectionConfig (AddSectionConfig). |
3064
- | Tenant types & augmentation | **`src/types.ts`** | SectionComponentPropsMap, PageConfig, SiteConfig, MenuConfig, ThemeConfig; **declare module '@olonjs/core'** for SectionDataRegistry and SectionSettingsRegistry; re-export from Core. |
3065
- | Bootstrap | **`src/App.tsx`** | Imports config (site, theme, menu, pages), registry, schemas, addSection, themeCss; builds JsonPagesConfig; renders **<JsonPagesEngine config={config} />**. |
3066
-
3067
- The app entry (e.g. **main.tsx**) renders **App**. No other bootstrap contract is specified; the Tenant may use Vite aliases (e.g. **@/**) for the paths above.
3068
-
3069
- **Perché servono (A.4):** Path fissi (data/config, data/pages, lib/schemas, types.ts, App.tsx) permettono a CLI, tooling e agenti di trovare sempre gli stessi file; l’onboarding e la generazione da spec sono deterministici. Senza convenzione, ogni tenant sarebbe una struttura diversa.
3070
-
3071
- ---
3072
-
3073
- ## A.5 Integration Checklist (Code-Generation)
3074
-
3075
- When generating or auditing a tenant, ensure the following in order:
3076
-
3077
- 1. **Capsules** — For each section type, create **`src/components/<type>/`** with View.tsx, schema.ts, types.ts, index.ts. Data schema extends BaseSectionData; array items extend BaseArrayItem; View complies with CIP and IDAC (§6.2–6.3 for non-reserved types).
3078
- 2. **Base schemas** — **src/lib/base-schemas.ts** exports BaseSectionData, BaseArrayItem, BaseSectionSettingsSchema (and optional CtaSchema or similar shared fragments).
3079
- 3. **types.ts** — Define SectionComponentPropsMap (header with **menu**), PageConfig, SiteConfig, MenuConfig, ThemeConfig; **declare module '@olonjs/core'** and augment SectionDataRegistry and SectionSettingsRegistry; re-export from `@olonjs/core`.
3080
- 4. **ComponentRegistry** — Import every View; build object **{ [K in SectionType]: ViewComponent }**; type as **{ [K in SectionType]: React.FC<SectionComponentPropsMap[K]> }**.
3081
- 5. **schemas.ts** — Import base schemas and each capsule’s data schema; export SECTION_SCHEMAS as **{ [K in SectionType]: SchemaK }**; export SectionType as **keyof typeof SECTION_SCHEMAS** if not using Core’s SectionType.
3082
- 6. **addSectionConfig** — addableSectionTypes, sectionTypeLabels, getDefaultSectionData; export as AddSectionConfig.
3083
- 7. **App.tsx** — Import site, theme, menu, pages from data paths; build config (tenantId, registry, schemas, pages, siteConfig, themeConfig, menuConfig, themeCss: { tenant }, addSection); render JsonPagesEngine.
3084
- 8. **Data files** — Create or update site.json, menu.json, theme.json, and one or more **<slug>.json** under the paths in A.4. Ensure JSON shapes match SiteConfig, MenuConfig, ThemeConfig, PageConfig.
3085
- 9. **Tenant CSS** — Include TOCC (§7) selectors in global CSS so the Stage overlay is visible.
3086
- 10. **Reserved types** — Header and footer capsules receive props per SectionComponentPropsMap; menu is populated from menuConfig (e.g. menuConfig.main) when building the config or inside Core when rendering the header.
3087
-
3088
- **Perché servono (A.5):** La checklist in ordine evita di dimenticare passi (es. augmentation prima del registry, TOCC dopo le View) e rende la spec sufficiente per generare o verificare un tenant senza codebase di riferimento.
3089
-
3090
- ---
3091
-
3092
- ## A.6 v1.3 Path/Nested Strictness Addendum (breaking)
3093
-
3094
- This addendum extends Appendix A without removing prior v1.2 obligations:
3095
-
3096
- 1. **Type exports** — Core and/or shared types module should expose `SelectionPathSegment` and `SelectionPath` for Studio messaging and Inspector expansion logic.
3097
- 2. **Protocol migration** — Replace flat payload fields `itemField` / `itemId` with `itemPath?: SelectionPath` in strict v1.3 channels.
3098
- 3. **Nested array compliance** — For editable object arrays, item identity must be stable (`id`) and propagated to DOM attributes (`data-jp-item-id`), schema items (BaseArrayItem), and selection path segments (`itemId` when segment targets array item).
3099
- 4. **Backward compatibility policy** — Legacy flat fields may exist only in transitional adapters outside strict mode; normative v1.3 contract is path-only.
3100
-
3101
- ---
3102
-
3103
- **Validation:** Align with current `@olonjs/core` exports (SectionType, MenuItem, AddSectionConfig, JsonPagesConfig, and in v1.3 path types for Studio selection).
3104
- **Distribution:** Core via `.yalc`; tenant projections via `@olonjs/cli`. This annex makes the spec **necessary and sufficient** for tenant code-generation and governance at enterprise grade.
3105
-
3106
2544
  END_OF_FILE_CONTENT
3107
2545
  mkdir -p "src"
3108
2546
  echo "Creating src/App.tsx..."
@@ -3138,6 +2576,7 @@ const CLOUD_API_URL =
3138
2576
  import.meta.env.VITE_OLONJS_CLOUD_URL ?? import.meta.env.VITE_JSONPAGES_CLOUD_URL;
3139
2577
  const CLOUD_API_KEY =
3140
2578
  import.meta.env.VITE_OLONJS_API_KEY ?? import.meta.env.VITE_JSONPAGES_API_KEY;
2579
+ const SAVE2REPO_ENABLED = import.meta.env.VITE_SAVE2REPO === 'true';
3141
2580
 
3142
2581
  const themeConfig = themeData as unknown as ThemeConfig;
3143
2582
  const menuConfig: MenuConfig = { main: [] };
@@ -3404,6 +2843,45 @@ function normalizeSlugForCache(slug: string): string {
3404
2843
  );
3405
2844
  }
3406
2845
 
2846
+ function buildPublishedPageHref(slug: string): string {
2847
+ return `/pages/${normalizeSlugForCache(slug)}.json`;
2848
+ }
2849
+
2850
+ async function loadPublishedStaticContent(
2851
+ knownSlugs: string[]
2852
+ ): Promise<{ pages: Record<string, PageConfig>; siteConfig: SiteConfig }> {
2853
+ const siteResponse = await fetch('/config/site.json', { cache: 'no-store' });
2854
+ if (!siteResponse.ok) {
2855
+ throw new Error(`Static site config unavailable: ${siteResponse.status}`);
2856
+ }
2857
+
2858
+ const sitePayload = (await siteResponse.json().catch(() => null)) as unknown;
2859
+ const nextSite = coerceSiteConfig(sitePayload);
2860
+ if (!nextSite) {
2861
+ throw new Error('Static site config is invalid.');
2862
+ }
2863
+
2864
+ const pageEntries = await Promise.all(
2865
+ knownSlugs.map(async (slug) => {
2866
+ const response = await fetch(buildPublishedPageHref(slug), { cache: 'no-store' });
2867
+ if (!response.ok) {
2868
+ throw new Error(`Static page unavailable for slug "${slug}": ${response.status}`);
2869
+ }
2870
+ return [slug, (await response.json().catch(() => null)) as unknown] as const;
2871
+ })
2872
+ );
2873
+
2874
+ const nextPages = normalizePageRegistry(Object.fromEntries(pageEntries));
2875
+ if (Object.keys(nextPages).length === 0) {
2876
+ throw new Error('Static published pages are empty.');
2877
+ }
2878
+
2879
+ return {
2880
+ pages: nextPages,
2881
+ siteConfig: nextSite,
2882
+ };
2883
+ }
2884
+
3407
2885
  function readCachedCloudContent(fingerprint: string): CachedCloudContent | null {
3408
2886
  try {
3409
2887
  const raw = localStorage.getItem(CLOUD_CACHE_KEY);
@@ -3447,6 +2925,8 @@ function setTenantPreviewReady(ready: boolean): void {
3447
2925
 
3448
2926
  function App() {
3449
2927
  const isCloudMode = Boolean(CLOUD_API_URL && CLOUD_API_KEY);
2928
+ const isSave2RepoMode = isCloudMode && SAVE2REPO_ENABLED;
2929
+ const isHotSaveMode = isCloudMode && !isSave2RepoMode;
3450
2930
  const localInitialData = useMemo(() => (isCloudMode ? null : getInitialData()), [isCloudMode]);
3451
2931
  const localInitialPages = useMemo(() => {
3452
2932
  if (!localInitialData) return {};
@@ -3528,6 +3008,54 @@ function App() {
3528
3008
  logBootstrapEvent('boot.local.ready', { mode: 'local' });
3529
3009
  return;
3530
3010
  }
3011
+
3012
+ if (isSave2RepoMode) {
3013
+ if (contentLoadInFlight.current) {
3014
+ return;
3015
+ }
3016
+
3017
+ setContentMode('cloud');
3018
+ setContentFallback(null);
3019
+ setShowTopProgress(true);
3020
+ setHasInitialCloudResolved(false);
3021
+ logBootstrapEvent('boot.start', { mode: 'save2repo-static', pageCount: Object.keys(filePages).length });
3022
+
3023
+ let inFlight: Promise<void> | null = null;
3024
+ inFlight = loadPublishedStaticContent(Object.keys(filePages))
3025
+ .then(({ pages: nextPages, siteConfig: nextSite }) => {
3026
+ setPages(nextPages);
3027
+ setSiteConfig(nextSite);
3028
+ setContentMode('cloud');
3029
+ setContentFallback(null);
3030
+ setHasInitialCloudResolved(true);
3031
+ logBootstrapEvent('boot.save2repo.success', {
3032
+ mode: 'save2repo-static',
3033
+ pageCount: Object.keys(nextPages).length,
3034
+ });
3035
+ })
3036
+ .catch((error: unknown) => {
3037
+ const failure = toCloudLoadFailure(error);
3038
+ setContentMode('error');
3039
+ setContentFallback(failure);
3040
+ setHasInitialCloudResolved(true);
3041
+ logBootstrapEvent('boot.save2repo.error', {
3042
+ mode: 'save2repo-static',
3043
+ reasonCode: failure.reasonCode,
3044
+ correlationId: failure.correlationId ?? null,
3045
+ });
3046
+ })
3047
+ .finally(() => {
3048
+ setShowTopProgress(false);
3049
+ if (contentLoadInFlight.current === inFlight) {
3050
+ contentLoadInFlight.current = null;
3051
+ }
3052
+ });
3053
+ contentLoadInFlight.current = inFlight;
3054
+ return () => {
3055
+ contentLoadInFlight.current = null;
3056
+ };
3057
+ }
3058
+
3531
3059
  if (contentLoadInFlight.current) {
3532
3060
  return;
3533
3061
  }
@@ -3700,7 +3228,7 @@ function App() {
3700
3228
  });
3701
3229
  contentLoadInFlight.current = inFlight;
3702
3230
  return () => controller.abort();
3703
- }, [isCloudMode, CLOUD_API_KEY, CLOUD_API_URL, cloudApiCandidates, bootstrapRunId]);
3231
+ }, [isCloudMode, isSave2RepoMode, CLOUD_API_KEY, CLOUD_API_URL, cloudApiCandidates, bootstrapRunId]);
3704
3232
 
3705
3233
  const runCloudSave = useCallback(
3706
3234
  async (
@@ -3862,8 +3390,12 @@ function App() {
3862
3390
  },
3863
3391
  });
3864
3392
  },
3865
- showLegacySave: !isCloudMode,
3866
- showHotSave: isCloudMode,
3393
+ async coldSave(state: ProjectState, slug: string): Promise<void> {
3394
+ await runCloudSave({ state, slug }, true);
3395
+ },
3396
+ showLocalSave: !isCloudMode,
3397
+ showHotSave: isHotSaveMode,
3398
+ showColdSave: isSave2RepoMode,
3867
3399
  },
3868
3400
  assets: {
3869
3401
  assetsBaseUrl: '/assets',
@@ -4093,7 +3625,6 @@ function App() {
4093
3625
 
4094
3626
  export default App;
4095
3627
 
4096
-
4097
3628
  END_OF_FILE_CONTENT
4098
3629
  mkdir -p "src/components"
4099
3630
  echo "Creating src/components/NotFound.tsx..."
@@ -11548,8 +11079,8 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
11548
11079
  "href": "#architecture"
11549
11080
  },
11550
11081
  "image": {
11551
- "url": "/assets/images/plug-graded-square.jpg",
11552
- "alt": "Olon interface port engraved into a dark stone surface"
11082
+ "url": "/assets/images/1775421583171-Screenshot_2026-04-05_at_20-08-49_OlonJS_MCP___LinkedIn_Cover.png",
11083
+ "alt": "1775421583171-Screenshot_2026-04-05_at_20-08-49_OlonJS_MCP___LinkedIn_Cover.png"
11553
11084
  }
11554
11085
  }
11555
11086
  },
@@ -11726,7 +11257,6 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
11726
11257
  }
11727
11258
  ]
11728
11259
  }
11729
-
11730
11260
  END_OF_FILE_CONTENT
11731
11261
  echo "Creating src/data/pages/home_.json..."
11732
11262
  cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home_.json"
@@ -14010,7 +13540,6 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
14010
13540
 
14011
13541
 
14012
13542
  END_OF_FILE_CONTENT
14013
- # SKIP: src/registry-types.ts is binary and cannot be embedded as text.
14014
13543
  mkdir -p "src/types"
14015
13544
  echo "Creating src/types.ts..."
14016
13545
  cat << 'END_OF_FILE_CONTENT' > "src/types.ts"