@olonjs/cli 3.0.97 → 3.0.98
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 +1107 -1608
- package/assets/templates/agritourism/src_tenant.sh +775 -5
- package/assets/templates/alpha/src_tenant.sh +1107 -1608
- package/package.json +1 -1
|
@@ -1632,6 +1632,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1632
1632
|
"scripts": {
|
|
1633
1633
|
"dev": "vite",
|
|
1634
1634
|
"dev:clean": "vite --force",
|
|
1635
|
+
"verify:webmcp": "node scripts/webmcp-feature-check.mjs",
|
|
1635
1636
|
"prebuild": "node scripts/sync-pages-to-public.mjs",
|
|
1636
1637
|
"build": "tsc && vite build",
|
|
1637
1638
|
"dist": "bash ./src2Code.sh --template alpha src .cursor vercel.json index.html tsconfig.json tsconfig.node.json vite.config.ts scripts specs package.json",
|
|
@@ -1645,7 +1646,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1645
1646
|
"@tiptap/extension-link": "^2.11.5",
|
|
1646
1647
|
"@tiptap/react": "^2.11.5",
|
|
1647
1648
|
"@tiptap/starter-kit": "^2.11.5",
|
|
1648
|
-
"@olonjs/core": "^1.0.
|
|
1649
|
+
"@olonjs/core": "^1.0.86",
|
|
1649
1650
|
"class-variance-authority": "^0.7.1",
|
|
1650
1651
|
"clsx": "^2.1.1",
|
|
1651
1652
|
"lucide-react": "^0.474.0",
|
|
@@ -1959,10 +1960,34 @@ import { build } from 'vite';
|
|
|
1959
1960
|
import path from 'path';
|
|
1960
1961
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
1961
1962
|
import fs from 'fs/promises';
|
|
1963
|
+
import {
|
|
1964
|
+
buildPageContract,
|
|
1965
|
+
buildPageManifest,
|
|
1966
|
+
buildPageManifestHref,
|
|
1967
|
+
buildSiteManifest,
|
|
1968
|
+
} from '../../../packages/core/src/lib/webmcp-contracts.mjs';
|
|
1962
1969
|
|
|
1963
1970
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1964
1971
|
const root = path.resolve(__dirname, '..');
|
|
1965
1972
|
const pagesDir = path.resolve(root, 'src/data/pages');
|
|
1973
|
+
const publicDir = path.resolve(root, 'public');
|
|
1974
|
+
const distDir = path.resolve(root, 'dist');
|
|
1975
|
+
|
|
1976
|
+
async function writeJsonTargets(relativePath, value) {
|
|
1977
|
+
const targets = [
|
|
1978
|
+
path.resolve(publicDir, relativePath),
|
|
1979
|
+
path.resolve(distDir, relativePath),
|
|
1980
|
+
];
|
|
1981
|
+
|
|
1982
|
+
for (const targetPath of targets) {
|
|
1983
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
1984
|
+
await fs.writeFile(targetPath, `${JSON.stringify(value, null, 2)}\n`, 'utf-8');
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
function escapeHtmlAttribute(value) {
|
|
1989
|
+
return String(value).replace(/&/g, '&').replace(/"/g, '"');
|
|
1990
|
+
}
|
|
1966
1991
|
|
|
1967
1992
|
function toCanonicalSlug(relativeJsonPath) {
|
|
1968
1993
|
const normalized = relativeJsonPath.replace(/\\/g, '/');
|
|
@@ -2032,7 +2057,7 @@ if (targets.length === 0) {
|
|
|
2032
2057
|
console.log(`[bake] Targets: ${targets.map((t) => t.slug).join(', ')}`);
|
|
2033
2058
|
|
|
2034
2059
|
const ssrEntryUrl = pathToFileURL(path.resolve(root, 'dist-ssr/entry-ssg.js')).href;
|
|
2035
|
-
const { render, getCss, getPageMeta } = await import(ssrEntryUrl);
|
|
2060
|
+
const { render, getCss, getPageMeta, getWebMcpBuildState } = await import(ssrEntryUrl);
|
|
2036
2061
|
|
|
2037
2062
|
const template = await fs.readFile(path.resolve(root, 'dist/index.html'), 'utf-8');
|
|
2038
2063
|
const hasCommentMarker = template.includes('<!--app-html-->');
|
|
@@ -2043,6 +2068,33 @@ if (!hasCommentMarker && !hasRootDivMarker) {
|
|
|
2043
2068
|
|
|
2044
2069
|
const inlinedCss = getCss();
|
|
2045
2070
|
const styleTag = `<style data-bake="inline">${inlinedCss}</style>`;
|
|
2071
|
+
const webMcpBuildState = getWebMcpBuildState();
|
|
2072
|
+
|
|
2073
|
+
for (const { slug } of targets) {
|
|
2074
|
+
const pageConfig = webMcpBuildState.pages[slug];
|
|
2075
|
+
if (!pageConfig) continue;
|
|
2076
|
+
const contract = buildPageContract({
|
|
2077
|
+
slug,
|
|
2078
|
+
pageConfig,
|
|
2079
|
+
schemas: webMcpBuildState.schemas,
|
|
2080
|
+
siteConfig: webMcpBuildState.siteConfig,
|
|
2081
|
+
});
|
|
2082
|
+
await writeJsonTargets(`schemas/${slug}.schema.json`, contract);
|
|
2083
|
+
const pageManifest = buildPageManifest({
|
|
2084
|
+
slug,
|
|
2085
|
+
pageConfig,
|
|
2086
|
+
schemas: webMcpBuildState.schemas,
|
|
2087
|
+
siteConfig: webMcpBuildState.siteConfig,
|
|
2088
|
+
});
|
|
2089
|
+
await writeJsonTargets(buildPageManifestHref(slug).replace(/^\//, ''), pageManifest);
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
const mcpManifest = buildSiteManifest({
|
|
2093
|
+
pages: webMcpBuildState.pages,
|
|
2094
|
+
schemas: webMcpBuildState.schemas,
|
|
2095
|
+
siteConfig: webMcpBuildState.siteConfig,
|
|
2096
|
+
});
|
|
2097
|
+
await writeJsonTargets('mcp-manifest.json', mcpManifest);
|
|
2046
2098
|
|
|
2047
2099
|
for (const { slug, out, depth } of targets) {
|
|
2048
2100
|
console.log(`\n[bake] Rendering /${slug === 'home' ? '' : slug}...`);
|
|
@@ -2056,12 +2108,26 @@ for (const { slug, out, depth } of targets) {
|
|
|
2056
2108
|
`<meta property="og:title" content="${safeTitle}">`,
|
|
2057
2109
|
`<meta property="og:description" content="${safeDescription}">`,
|
|
2058
2110
|
].join('\n ');
|
|
2111
|
+
const jsonLd = JSON.stringify({
|
|
2112
|
+
'@context': 'https://schema.org',
|
|
2113
|
+
'@type': 'WebPage',
|
|
2114
|
+
name: title,
|
|
2115
|
+
description,
|
|
2116
|
+
url: slug === 'home' ? '/' : `/${slug}`,
|
|
2117
|
+
});
|
|
2059
2118
|
|
|
2060
2119
|
const prefix = depth > 0 ? '../'.repeat(depth) : './';
|
|
2061
2120
|
const fixedTemplate = depth > 0 ? template.replace(/(['"])\.\//g, `$1${prefix}`) : template;
|
|
2121
|
+
const mcpManifestHref = `${prefix}${buildPageManifestHref(slug).replace(/^\//, '')}`;
|
|
2122
|
+
const contractHref = `${prefix}schemas/${slug}.schema.json`;
|
|
2123
|
+
const contractLinks = [
|
|
2124
|
+
`<link rel="mcp-manifest" href="${escapeHtmlAttribute(mcpManifestHref)}">`,
|
|
2125
|
+
`<link rel="olon-contract" href="${escapeHtmlAttribute(contractHref)}">`,
|
|
2126
|
+
`<script type="application/ld+json">${jsonLd}</script>`,
|
|
2127
|
+
].join('\n ');
|
|
2062
2128
|
|
|
2063
2129
|
let bakedHtml = fixedTemplate
|
|
2064
|
-
.replace('</head>', ` ${styleTag}\n</head>`)
|
|
2130
|
+
.replace('</head>', ` ${styleTag}\n ${contractLinks}\n</head>`)
|
|
2065
2131
|
.replace(/<title>.*?<\/title>/, `<title>${safeTitle}</title>\n ${metaTags}`);
|
|
2066
2132
|
|
|
2067
2133
|
if (hasCommentMarker) {
|
|
@@ -2103,1604 +2169,920 @@ fs.cpSync(sourceDir, targetDir, { recursive: true });
|
|
|
2103
2169
|
console.log('[sync-pages-to-public] Synced pages to public/pages');
|
|
2104
2170
|
|
|
2105
2171
|
END_OF_FILE_CONTENT
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
**Version:** 1.3.0 (Sovereign Core Edition — Architecture + Studio/ICE UX, Path-Deterministic Nested Editing)
|
|
2113
|
-
**Target:** Senior Architects / AI Agents / Enterprise Governance
|
|
2114
|
-
|
|
2115
|
-
**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.
|
|
2116
|
-
**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).
|
|
2117
|
-
|
|
2118
|
-
---
|
|
2119
|
-
|
|
2120
|
-
## 1. 📐 Modular Type Registry Pattern (MTRP) v1.2
|
|
2121
|
-
|
|
2122
|
-
**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.
|
|
2123
|
-
|
|
2124
|
-
### 1.1 The Sovereign Dependency Inversion
|
|
2125
|
-
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.
|
|
2126
|
-
|
|
2127
|
-
### 1.2 Technical Implementation (`@olonjs/core/kernel`)
|
|
2128
|
-
```typescript
|
|
2129
|
-
export interface SectionDataRegistry {} // Augmented by Tenant
|
|
2130
|
-
export interface SectionSettingsRegistry {} // Augmented by Tenant
|
|
2131
|
-
|
|
2132
|
-
export interface BaseSection<K extends keyof SectionDataRegistry> {
|
|
2133
|
-
id: string;
|
|
2134
|
-
type: K;
|
|
2135
|
-
data: SectionDataRegistry[K];
|
|
2136
|
-
settings?: K extends keyof SectionSettingsRegistry
|
|
2137
|
-
? SectionSettingsRegistry[K]
|
|
2138
|
-
: BaseSectionSettings;
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
export type Section = {
|
|
2142
|
-
[K in keyof SectionDataRegistry]: BaseSection<K>
|
|
2143
|
-
}[keyof SectionDataRegistry];
|
|
2144
|
-
```
|
|
2145
|
-
|
|
2146
|
-
**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.
|
|
2147
|
-
|
|
2148
|
-
**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`).
|
|
2149
|
-
|
|
2150
|
-
---
|
|
2151
|
-
|
|
2152
|
-
## 2. 📐 JsonPages Site Protocol (JSP) v1.8
|
|
2153
|
-
|
|
2154
|
-
**Objective:** Define the deterministic file system and the **Sovereign Projection Engine** (CLI).
|
|
2155
|
-
|
|
2156
|
-
### 2.1 The File System Ontology (The Silo Contract)
|
|
2157
|
-
Every site must reside in an isolated directory. Global Governance is physically separated from Local Content.
|
|
2158
|
-
* **`/config/site.json`** — Global Identity & Reserved System Blocks (Header/Footer). See Appendix A for typed shape.
|
|
2159
|
-
* **`/config/menu.json`** — Navigation Tree (SSOT for System Header). See Appendix A.
|
|
2160
|
-
* **`/config/theme.json`** — Theme tokens (optional but recommended). See Appendix A.
|
|
2161
|
-
* **`/pages/[slug].json`** — Local Body Content per page. See Appendix A (PageConfig).
|
|
2162
|
-
|
|
2163
|
-
**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.
|
|
2164
|
-
|
|
2165
|
-
### 2.2 Deterministic Projection (CLI Workflow)
|
|
2166
|
-
The CLI (`@olonjs/cli`) creates new tenants by:
|
|
2167
|
-
1. **Infra Projection:** Generating `package.json`, `tsconfig.json`, and `vite.config.ts` (The Shell).
|
|
2168
|
-
2. **Source Projection:** Executing a deterministic script (`src_tenant_alpha.sh`) to reconstruct the `src` folder (The DNA).
|
|
2169
|
-
3. **Dependency Resolution:** Enforcing specific versions of React, Radix, and Tailwind v4.
|
|
2170
|
-
|
|
2171
|
-
**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.
|
|
2172
|
-
|
|
2173
|
-
---
|
|
2174
|
-
|
|
2175
|
-
## 3. 🧱 Tenant Block Protocol (TBP) v1.0
|
|
2176
|
-
|
|
2177
|
-
**Objective:** Standardize the "Capsule" structure for components to enable automated ingestion (Pull) by the SaaS.
|
|
2178
|
-
|
|
2179
|
-
### 3.1 The Atomic Capsule Structure
|
|
2180
|
-
Components are self-contained directories under **`src/components/<sectionType>/`**:
|
|
2181
|
-
* **`View.tsx`** — The pure React component (Dumb View). Props: see Appendix A (SectionComponentPropsMap).
|
|
2182
|
-
* **`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).
|
|
2183
|
-
* **`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.
|
|
2184
|
-
* **`index.ts`** — Public API: re-exports View, schema(s), and types.
|
|
2185
|
-
|
|
2186
|
-
### 3.2 Reserved System Types
|
|
2187
|
-
* **`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[] }`.
|
|
2188
|
-
* **`type: 'footer'`** — Reserved for `site.json`. Props: `{ data: FooterData; settings?: FooterSettings }` only (no `menu`).
|
|
2189
|
-
* **`type: 'sectionHeader'`** — A standard local block. Must define its own `links` array in its local schema if used.
|
|
2190
|
-
|
|
2191
|
-
**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.
|
|
2192
|
-
|
|
2193
|
-
---
|
|
2194
|
-
|
|
2195
|
-
## 4. 🧱 Component Implementation Protocol (CIP) v1.5
|
|
2196
|
-
|
|
2197
|
-
**Objective:** Ensure system-wide stability and Admin UI integrity.
|
|
2198
|
-
|
|
2199
|
-
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).
|
|
2200
|
-
2. **Z-Index Neutrality:** Components must not use `z-index > 1`. Layout delegation (sticky/fixed) is managed by the `SectionRenderer`.
|
|
2201
|
-
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`).
|
|
2202
|
-
|
|
2203
|
-
### 4.4 Local Design Tokens (v1.2)
|
|
2204
|
-
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.
|
|
2205
|
-
|
|
2206
|
-
### 4.5 Z-Index & Overlay Governance (v1.2)
|
|
2207
|
-
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.
|
|
2208
|
-
|
|
2209
|
-
**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.
|
|
2210
|
-
|
|
2211
|
-
---
|
|
2212
|
-
|
|
2213
|
-
## 5. 🛠️ Editor Component Implementation Protocol (ECIP) v1.5
|
|
2214
|
-
|
|
2215
|
-
**Objective:** Standardize the Polymorphic ICE engine.
|
|
2216
|
-
|
|
2217
|
-
1. **Recursive Form Factory:** The Admin UI builds forms by traversing the Zod ontology.
|
|
2218
|
-
2. **UI Metadata:** Use `.describe('ui:[widget]')` in schemas to pass instructions to the Form Factory.
|
|
2219
|
-
3. **Deterministic IDs:** Every object in a `ZodArray` must extend `BaseArrayItem` (containing an `id`) to ensure React reconciliation stability during reordering.
|
|
2220
|
-
|
|
2221
|
-
### 5.4 UI Metadata Vocabulary (v1.2)
|
|
2222
|
-
Standard keys for the Form Factory:
|
|
2223
|
-
|
|
2224
|
-
| Key | Use case |
|
|
2225
|
-
|-----|----------|
|
|
2226
|
-
| `ui:text` | Single-line text input. |
|
|
2227
|
-
| `ui:textarea` | Multi-line text. |
|
|
2228
|
-
| `ui:select` | Enum / single choice. |
|
|
2229
|
-
| `ui:number` | Numeric input. |
|
|
2230
|
-
| `ui:list` | Array of items; list editor (add/remove/reorder). |
|
|
2231
|
-
| `ui:icon-picker` | Icon selection. |
|
|
2232
|
-
|
|
2233
|
-
Unknown keys may be treated as `ui:text`. Array fields must use `BaseArrayItem` for items.
|
|
2234
|
-
|
|
2235
|
-
### 5.5 Path-Only Nested Selection & Expansion (v1.3, breaking)
|
|
2236
|
-
In strict v1.3 Studio/Inspector behavior, nested editing targets are represented by **path segments from root to leaf**.
|
|
2237
|
-
|
|
2238
|
-
```typescript
|
|
2239
|
-
export type SelectionPathSegment = { fieldKey: string; itemId?: string };
|
|
2240
|
-
export type SelectionPath = SelectionPathSegment[];
|
|
2241
|
-
```
|
|
2242
|
-
|
|
2243
|
-
Rules:
|
|
2244
|
-
* Expansion and focus for nested arrays **must** be computed from `SelectionPath` (root → leaf), not from a single flat pair.
|
|
2245
|
-
* Matching by `fieldKey` alone is non-compliant for nested structures.
|
|
2246
|
-
* Legacy flat payload fields **`itemField`** and **`itemId`** are removed from strict v1.3 selection protocol.
|
|
2247
|
-
|
|
2248
|
-
**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.
|
|
2249
|
-
|
|
2250
|
-
---
|
|
2251
|
-
|
|
2252
|
-
## 6. 🎯 ICE Data Attribute Contract (IDAC) v1.1
|
|
2253
|
-
|
|
2254
|
-
**Objective:** Mandatory data attributes so the Stage (iframe) and Inspector can bind selection and field/item editing without coupling to Tenant DOM.
|
|
2255
|
-
|
|
2256
|
-
### 6.1 Section-Level Markup (Core-Provided)
|
|
2257
|
-
**SectionRenderer** (Core) wraps each section root with:
|
|
2258
|
-
* **`data-section-id`** — Section instance ID (e.g. UUID). On the wrapper that contains content + overlay.
|
|
2259
|
-
* Sibling overlay element **`data-jp-section-overlay`** — Selection ring and type label. **Tenant does not add this;** Core injects it.
|
|
2260
|
-
|
|
2261
|
-
Tenant Views render the **content** root only (e.g. `<section>` or `<div>`), placed **inside** the Core wrapper.
|
|
2262
|
-
|
|
2263
|
-
### 6.2 Field-Level Binding (Tenant-Provided)
|
|
2264
|
-
For every **editable scalar field** the View **must** attach **`data-jp-field="<fieldKey>"`** (key matches schema path: e.g. `title`, `description`, `sectionTitle`, `label`).
|
|
2265
|
-
|
|
2266
|
-
### 6.3 Array-Item Binding (Tenant-Provided)
|
|
2267
|
-
For every **editable array item** the View **must** attach:
|
|
2268
|
-
* **`data-jp-item-id="<stableId>"`** — Prefer `item.id`; fallback e.g. `legacy-${index}` only outside strict mode.
|
|
2269
|
-
* **`data-jp-item-field="<arrayKey>"`** — e.g. `cards`, `layers`, `products`, `paragraphs`.
|
|
2270
|
-
|
|
2271
|
-
### 6.4 Compliance
|
|
2272
|
-
**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.
|
|
2273
|
-
|
|
2274
|
-
### 6.5 Strict Path Extraction for Nested Arrays (v1.3, breaking)
|
|
2275
|
-
For nested array targets, the Core/Inspector contract is path-based:
|
|
2276
|
-
* The runtime selection target is expressed as `itemPath: SelectionPath` (root → leaf).
|
|
2277
|
-
* Flat identity (`itemField` + `itemId`) is not sufficient for nested structures and is removed in strict v1.3 payloads.
|
|
2278
|
-
* In strict mode, index-based identity fallback is non-compliant for editable object arrays.
|
|
2279
|
-
|
|
2280
|
-
**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.
|
|
2281
|
-
|
|
2282
|
-
---
|
|
2283
|
-
|
|
2284
|
-
## 7. 🎨 Tenant Overlay CSS Contract (TOCC) v1.0
|
|
2285
|
-
|
|
2286
|
-
**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.
|
|
2287
|
-
|
|
2288
|
-
### 7.1 Required Selectors (Tenant global CSS)
|
|
2289
|
-
1. **`[data-jp-section-overlay]`** — `position: absolute; inset: 0`; `pointer-events: none`; base state transparent.
|
|
2290
|
-
2. **`[data-section-id]:hover [data-jp-section-overlay]`** — Hover: e.g. dashed border, subtle tint.
|
|
2291
|
-
3. **`[data-section-id][data-jp-selected] [data-jp-section-overlay]`** — Selected: solid border, optional tint.
|
|
2292
|
-
4. **`[data-jp-section-overlay] > div`** (type label) — Position and visibility (e.g. visible on hover/selected).
|
|
2293
|
-
|
|
2294
|
-
### 7.2 Z-Index
|
|
2295
|
-
Overlay **z-index** high (e.g. 9999). Section content at or below CIP limit (§4.5).
|
|
2296
|
-
|
|
2297
|
-
### 7.3 Responsibility
|
|
2298
|
-
**Core:** Injects wrapper and overlay DOM; sets `data-jp-selected`. **Tenant:** All overlay **visual** rules.
|
|
2299
|
-
|
|
2300
|
-
**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.
|
|
2301
|
-
|
|
2302
|
-
---
|
|
2303
|
-
|
|
2304
|
-
## 8. 📦 Base Section Data & Settings (BSDS) v1.0
|
|
2305
|
-
|
|
2306
|
-
**Objective:** Standardize base schema fragments for anchors, array items, and section settings.
|
|
2307
|
-
|
|
2308
|
-
### 8.1 BaseSectionData
|
|
2309
|
-
Every section data schema **must** extend a base with at least **`anchorId`** (optional string). Canonical Zod (Tenant `lib/base-schemas.ts` or equivalent):
|
|
2310
|
-
|
|
2311
|
-
```typescript
|
|
2312
|
-
export const BaseSectionData = z.object({
|
|
2313
|
-
anchorId: z.string().optional().describe('ui:text'),
|
|
2314
|
-
});
|
|
2315
|
-
```
|
|
2316
|
-
|
|
2317
|
-
### 8.2 BaseArrayItem
|
|
2318
|
-
Every array item schema editable in the Inspector **must** include **`id`** (optional string minimum). Canonical Zod:
|
|
2319
|
-
|
|
2320
|
-
```typescript
|
|
2321
|
-
export const BaseArrayItem = z.object({
|
|
2322
|
-
id: z.string().optional(),
|
|
2323
|
-
});
|
|
2324
|
-
```
|
|
2325
|
-
|
|
2326
|
-
Recommended: required UUID for new items. Used by `data-jp-item-id` and React reconciliation.
|
|
2327
|
-
|
|
2328
|
-
### 8.3 BaseSectionSettings (Optional)
|
|
2329
|
-
Common section-level settings. Canonical Zod (name **BaseSectionSettingsSchema** or as exported by Core):
|
|
2330
|
-
|
|
2331
|
-
```typescript
|
|
2332
|
-
export const BaseSectionSettingsSchema = z.object({
|
|
2333
|
-
paddingTop: z.enum(['none', 'sm', 'md', 'lg', 'xl', '2xl']).default('md').describe('ui:select'),
|
|
2334
|
-
paddingBottom: z.enum(['none', 'sm', 'md', 'lg', 'xl', '2xl']).default('md').describe('ui:select'),
|
|
2335
|
-
theme: z.enum(['dark', 'light', 'accent']).default('dark').describe('ui:select'),
|
|
2336
|
-
container: z.enum(['boxed', 'fluid']).default('boxed').describe('ui:select'),
|
|
2337
|
-
});
|
|
2338
|
-
```
|
|
2339
|
-
|
|
2340
|
-
Capsules may extend this for type-specific settings. Core may export **BaseSectionSettings** as the TypeScript type inferred from this or a superset.
|
|
2341
|
-
|
|
2342
|
-
**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.
|
|
2343
|
-
|
|
2344
|
-
---
|
|
2345
|
-
|
|
2346
|
-
## 9. 📌 AddSectionConfig (ASC) v1.0
|
|
2347
|
-
|
|
2348
|
-
**Objective:** Formalize the "Add Section" contract used by the Studio.
|
|
2349
|
-
|
|
2350
|
-
**Type (Core exports `AddSectionConfig`):**
|
|
2351
|
-
```typescript
|
|
2352
|
-
interface AddSectionConfig {
|
|
2353
|
-
addableSectionTypes: readonly string[];
|
|
2354
|
-
sectionTypeLabels: Record<string, string>;
|
|
2355
|
-
getDefaultSectionData(sectionType: string): Record<string, unknown>;
|
|
2356
|
-
}
|
|
2357
|
-
```
|
|
2358
|
-
|
|
2359
|
-
**Shape:** Tenant provides one object (e.g. `addSectionConfig`) with:
|
|
2360
|
-
* **`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.
|
|
2361
|
-
* **`sectionTypeLabels`** — Map type key → display string (e.g. `{ hero: 'Hero', 'cta-banner': 'CTA Banner' }`).
|
|
2362
|
-
* **`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.
|
|
2363
|
-
|
|
2364
|
-
Core creates a new section with deterministic UUID, `type`, and `data` from `getDefaultSectionData(type)`.
|
|
2365
|
-
|
|
2366
|
-
**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.
|
|
2367
|
-
|
|
2368
|
-
---
|
|
2369
|
-
|
|
2370
|
-
## 10. ⚙️ JsonPagesConfig & Engine Bootstrap (JEB) v1.1
|
|
2371
|
-
|
|
2372
|
-
**Objective:** Bootstrap contract between Tenant app and `@olonjs/core`.
|
|
2373
|
-
|
|
2374
|
-
### 10.1 JsonPagesConfig (required fields)
|
|
2375
|
-
The Tenant passes a single **config** object to **JsonPagesEngine**. Required fields:
|
|
2376
|
-
|
|
2377
|
-
| Field | Type | Description |
|
|
2378
|
-
|-------|------|-------------|
|
|
2379
|
-
| **tenantId** | string | Passed to `resolveAssetUrl(path, tenantId)`; resolved asset URLs are **`/assets/...`** with no tenantId segment in the path. |
|
|
2380
|
-
| **registry** | `{ [K in SectionType]: React.FC<SectionComponentPropsMap[K]> }` | Component registry. Must match MTRP keys. See Appendix A. |
|
|
2381
|
-
| **schemas** | `Record<SectionType, ZodType>` or equivalent | SECTION_SCHEMAS: type → **data** Zod schema. Form Factory uses this. See Appendix A. |
|
|
2382
|
-
| **pages** | `Record<string, PageConfig>` | Slug → page config. See Appendix A. |
|
|
2383
|
-
| **siteConfig** | SiteConfig | Global site (identity, header/footer blocks). See Appendix A. |
|
|
2384
|
-
| **themeConfig** | ThemeConfig | Theme tokens. See Appendix A. |
|
|
2385
|
-
| **menuConfig** | MenuConfig | Navigation tree (SSOT for header menu). See Appendix A. |
|
|
2386
|
-
| **themeCss** | `{ tenant: string }` | At least **tenant**: string (inline CSS or URL) for Stage iframe injection. |
|
|
2387
|
-
| **addSection** | AddSectionConfig | Add-section config (§9). |
|
|
2388
|
-
|
|
2389
|
-
Core may define optional fields. The Tenant must not omit required fields.
|
|
2390
|
-
|
|
2391
|
-
### 10.2 JsonPagesEngine
|
|
2392
|
-
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.
|
|
2393
|
-
|
|
2394
|
-
### 10.3 Studio Selection Event Contract (v1.3, breaking)
|
|
2395
|
-
In strict v1.3 Studio, section selection payload for nested targets is path-based:
|
|
2396
|
-
|
|
2397
|
-
```typescript
|
|
2398
|
-
type SectionSelectMessage = {
|
|
2399
|
-
type: 'SECTION_SELECT';
|
|
2400
|
-
section: { id: string; type: string; scope: 'global' | 'local' };
|
|
2401
|
-
itemPath?: SelectionPath; // root -> leaf
|
|
2402
|
-
};
|
|
2403
|
-
```
|
|
2404
|
-
|
|
2405
|
-
Removed from strict protocol:
|
|
2406
|
-
* `itemField`
|
|
2407
|
-
* `itemId`
|
|
2408
|
-
|
|
2409
|
-
**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.
|
|
2410
|
-
|
|
2411
|
-
---
|
|
2412
|
-
|
|
2413
|
-
# 🏛️ OlonJS_ADMIN_PROTOCOL (JAP) v1.2
|
|
2414
|
-
|
|
2415
|
-
**Status:** Mandatory Standard
|
|
2416
|
-
**Version:** 1.2.0 (Sovereign Shell Edition — Path/Nested Strictness)
|
|
2417
|
-
**Objective:** Deterministic orchestration of the "Studio" environment (ICE Level 1).
|
|
2418
|
-
|
|
2419
|
-
---
|
|
2420
|
-
|
|
2421
|
-
## 1. The Sovereign Shell Topology
|
|
2422
|
-
The Admin interface is a **Sovereign Shell** from `@olonjs/core`.
|
|
2423
|
-
1. **The Stage (Canvas):** Isolated Iframe; postMessage for data updates and selection mirroring. Section markup follows **IDAC** (§6); overlay styling follows **TOCC** (§7).
|
|
2424
|
-
2. **The Inspector (Sidebar):** Consumes Tenant Zod schemas to generate editors; binding via `data-jp-field` and `data-jp-item-*`.
|
|
2425
|
-
3. **The Control Bar:** Save, Export, Add Section.
|
|
2426
|
-
|
|
2427
|
-
## 2. State Orchestration & Persistence
|
|
2428
|
-
* **Working Draft:** Reactive local state for unsaved changes.
|
|
2429
|
-
* **Sync Law:** Inspector changes → Working Draft → Stage via `STUDIO_EVENTS.UPDATE_DRAFTS`.
|
|
2430
|
-
* **Bake Protocol:** "Bake HTML" requests snapshot from Iframe, injects `ProjectState` as JSON, triggers download.
|
|
2431
|
-
|
|
2432
|
-
## 3. Context Switching (Global vs. Local)
|
|
2433
|
-
* **Header/Footer** selection → Global Mode, `site.json`.
|
|
2434
|
-
* Any other section → Page Mode, current `[slug].json`.
|
|
2435
|
-
|
|
2436
|
-
## 4. Section Lifecycle Management
|
|
2437
|
-
1. **Add Section:** Modal from Tenant `SECTION_SCHEMAS`; UUID + default data via **AddSectionConfig** (§9).
|
|
2438
|
-
2. **Reorder:** Inspector or Stage Overlay; array mutation in Working Draft.
|
|
2439
|
-
3. **Delete:** Confirmation; remove from array, clear selection.
|
|
2440
|
-
|
|
2441
|
-
## 5. Stage Isolation & Overlay
|
|
2442
|
-
* **CSS Shielding:** Stage in Iframe; Tenant CSS does not leak into Admin.
|
|
2443
|
-
* **Sovereign Overlay:** Selection ring and type labels injected per **IDAC** (§6); Tenant styles them per **TOCC** (§7).
|
|
2444
|
-
|
|
2445
|
-
## 6. "Green Build" Validation
|
|
2446
|
-
Studio enforces `tsc && vite build`. No export with TypeScript errors.
|
|
2447
|
-
|
|
2448
|
-
## 7. Path-Deterministic Selection & Sidebar Expansion (v1.3, breaking)
|
|
2449
|
-
* Section/item focus synchronization uses `itemPath` (root → leaf), not flat `itemField/itemId`.
|
|
2450
|
-
* Sidebar expansion state for nested arrays must be derived from all path segments.
|
|
2451
|
-
* Flat-only matching may open/close wrong branches and is non-compliant in strict mode.
|
|
2452
|
-
|
|
2453
|
-
**Perché servono (JAP):** Stage in iframe + Inspector + Control Bar separano il contesto di editing dal sito; postMessage e Working Draft permettono modifiche senza toccare subito i file. Bake ed Export 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 ciò che si esporta compili. In v1.3, il path completo elimina ambiguità nella sincronizzazione Stage↔Sidebar su strutture annidate.
|
|
2454
|
-
|
|
2455
|
-
---
|
|
2456
|
-
|
|
2457
|
-
## Compliance: Legacy vs Full UX (v1.3)
|
|
2458
|
-
|
|
2459
|
-
| Dimension | Legacy / Less UX | Full UX (Core-aligned) |
|
|
2460
|
-
|-----------|-------------------|-------------------------|
|
|
2461
|
-
| **ICE binding** | No `data-jp-*`; Inspector cannot bind. | IDAC (§6) on every editable section/field/item. |
|
|
2462
|
-
| **Section wrapper** | Plain `<section>`; no overlay contract. | Core wrapper + overlay; Tenant CSS per TOCC (§7). |
|
|
2463
|
-
| **Design tokens** | Raw BEM / fixed classes. | Local tokens (§4.4); `var(--local-*)` only. |
|
|
2464
|
-
| **Base schemas** | Ad hoc. | BSDS (§8): BaseSectionData, BaseArrayItem, BaseSectionSettings. |
|
|
2465
|
-
| **Add Section** | Ad hoc defaults. | ASC (§9): addableSectionTypes, labels, getDefaultSectionData. |
|
|
2466
|
-
| **Bootstrap** | Implicit. | JEB (§10): JsonPagesConfig + JsonPagesEngine. |
|
|
2467
|
-
| **Selection payload** | Flat `itemField/itemId`. | Path-only `itemPath: SelectionPath` (JEB §10.3). |
|
|
2468
|
-
| **Nested array expansion** | Single-segment or field-only heuristics. | Root-to-leaf path expansion (ECIP §5.5, JAP §7). |
|
|
2469
|
-
| **Array item identity (strict)** | Index fallback tolerated. | Stable `id` required for editable object arrays. |
|
|
2470
|
-
|
|
2471
|
-
**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.
|
|
2472
|
-
|
|
2473
|
-
---
|
|
2474
|
-
|
|
2475
|
-
## Summary of v1.3 Additions
|
|
2476
|
-
|
|
2477
|
-
| § | Title | Purpose |
|
|
2478
|
-
|---|--------|--------|
|
|
2479
|
-
| 5.5 | Path-Only Nested Selection & Expansion | ECIP: root→leaf `SelectionPath`; remove flat matching in strict mode. |
|
|
2480
|
-
| 6.5 | Strict Path Extraction for Nested Arrays | IDAC: path-based nested targeting; no strict flat fallback. |
|
|
2481
|
-
| 10.3 | Studio Selection Event Contract | JEB: `SECTION_SELECT` uses `itemPath`; remove `itemField/itemId`. |
|
|
2482
|
-
| JAP §7 | Path-Deterministic Selection & Sidebar Expansion | Studio state synchronization for nested arrays. |
|
|
2483
|
-
| Compliance | Legacy vs Full UX (v1.3) | Explicit breaking delta for flat protocol removal and strict IDs. |
|
|
2484
|
-
| **Appendix A.6** | **v1.3 Path/Nested Strictness Addendum** | Type/export and migration checklist for path-only protocol. |
|
|
2485
|
-
|
|
2486
|
-
---
|
|
2487
|
-
|
|
2488
|
-
# Appendix A — Tenant Type & Code-Generation Annex
|
|
2489
|
-
|
|
2490
|
-
**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.
|
|
2491
|
-
|
|
2492
|
-
**Status:** Mandatory for code-generation and governance. Compliance ensures generated tenants are typed and wired like the reference implementation.
|
|
2493
|
-
|
|
2494
|
-
---
|
|
2495
|
-
|
|
2496
|
-
## A.1 Core-Provided Types (from `@olonjs/core`)
|
|
2497
|
-
|
|
2498
|
-
The following are assumed to be exported by Core. The Tenant augments **SectionDataRegistry** and **SectionSettingsRegistry**; all other types are consumed as-is.
|
|
2499
|
-
|
|
2500
|
-
| Type | Description |
|
|
2501
|
-
|------|-------------|
|
|
2502
|
-
| **SectionType** | `keyof SectionDataRegistry` (after Tenant augmentation). Union of all section type keys. |
|
|
2503
|
-
| **Section** | Union of `BaseSection<K>` for all K in SectionDataRegistry. See MTRP §1.2. |
|
|
2504
|
-
| **BaseSectionSettings** | Optional base type for section settings (may align with BSDS §8.3). |
|
|
2505
|
-
| **MenuItem** | Navigation item. **Minimum shape:** `{ label: string; href: string }`. Core may extend (e.g. `children?: MenuItem[]`). |
|
|
2506
|
-
| **AddSectionConfig** | See §9. |
|
|
2507
|
-
| **JsonPagesConfig** | See §10.1. |
|
|
2508
|
-
|
|
2509
|
-
**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.
|
|
2510
|
-
|
|
2511
|
-
---
|
|
2512
|
-
|
|
2513
|
-
## A.2 Tenant-Provided Types (single source: `src/types.ts` or equivalent)
|
|
2514
|
-
|
|
2515
|
-
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.
|
|
2516
|
-
|
|
2517
|
-
### A.2.1 SectionComponentPropsMap
|
|
2518
|
-
|
|
2519
|
-
Maps each section type to the props of its React component. **Header** is the only type that receives **menu**.
|
|
2520
|
-
|
|
2521
|
-
**Option A — Explicit (recommended for clarity and tooling):** For each section type K, add one entry. Header receives **menu**.
|
|
2522
|
-
|
|
2523
|
-
```typescript
|
|
2524
|
-
import type { MenuItem } from '@olonjs/core';
|
|
2525
|
-
// Import Data/Settings from each capsule.
|
|
2526
|
-
|
|
2527
|
-
export type SectionComponentPropsMap = {
|
|
2528
|
-
'header': { data: HeaderData; settings?: HeaderSettings; menu: MenuItem[] };
|
|
2529
|
-
'footer': { data: FooterData; settings?: FooterSettings };
|
|
2530
|
-
'hero': { data: HeroData; settings?: HeroSettings };
|
|
2531
|
-
// ... one entry per SectionType, e.g. 'feature-grid', 'cta-banner', etc.
|
|
2532
|
-
};
|
|
2533
|
-
```
|
|
2534
|
-
|
|
2535
|
-
**Option B — Mapped type (DRY, requires SectionDataRegistry/SectionSettingsRegistry in scope):**
|
|
2536
|
-
|
|
2537
|
-
```typescript
|
|
2538
|
-
import type { MenuItem } from '@olonjs/core';
|
|
2539
|
-
|
|
2540
|
-
export type SectionComponentPropsMap = {
|
|
2541
|
-
[K in SectionType]: K extends 'header'
|
|
2542
|
-
? { data: SectionDataRegistry[K]; settings?: SectionSettingsRegistry[K]; menu: MenuItem[] }
|
|
2543
|
-
: { data: SectionDataRegistry[K]; settings?: K extends keyof SectionSettingsRegistry ? SectionSettingsRegistry[K] : BaseSectionSettings };
|
|
2544
|
-
};
|
|
2545
|
-
```
|
|
2546
|
-
|
|
2547
|
-
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.
|
|
2548
|
-
|
|
2549
|
-
**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.
|
|
2550
|
-
|
|
2551
|
-
### A.2.2 ComponentRegistry type
|
|
2552
|
-
|
|
2553
|
-
The registry object **must** be typed as:
|
|
2554
|
-
|
|
2555
|
-
```typescript
|
|
2556
|
-
import type { SectionType } from '@olonjs/core';
|
|
2557
|
-
import type { SectionComponentPropsMap } from '@/types';
|
|
2558
|
-
|
|
2559
|
-
export const ComponentRegistry: {
|
|
2560
|
-
[K in SectionType]: React.FC<SectionComponentPropsMap[K]>;
|
|
2561
|
-
} = { /* ... */ };
|
|
2562
|
-
```
|
|
2563
|
-
|
|
2564
|
-
File: **`src/lib/ComponentRegistry.tsx`** (or equivalent). Imports one View per section type and assigns it to the corresponding key.
|
|
2565
|
-
|
|
2566
|
-
### A.2.3 PageConfig
|
|
2567
|
-
|
|
2568
|
-
Minimum shape for a single page (used in **pages** and in each **`[slug].json`**):
|
|
2569
|
-
|
|
2570
|
-
```typescript
|
|
2571
|
-
export interface PageConfig {
|
|
2572
|
-
id?: string;
|
|
2573
|
-
slug: string;
|
|
2574
|
-
meta?: {
|
|
2575
|
-
title?: string;
|
|
2576
|
-
description?: string;
|
|
2577
|
-
};
|
|
2578
|
-
sections: Section[];
|
|
2579
|
-
}
|
|
2580
|
-
```
|
|
2581
|
-
|
|
2582
|
-
**Section** is the union type from MTRP (§1.2). Each element of **sections** has **id**, **type**, **data**, **settings** and conforms to the capsule schemas.
|
|
2583
|
-
|
|
2584
|
-
### A.2.4 SiteConfig
|
|
2585
|
-
|
|
2586
|
-
Minimum shape for **site.json** (and for **siteConfig** in JsonPagesConfig):
|
|
2587
|
-
|
|
2588
|
-
```typescript
|
|
2589
|
-
export interface SiteConfigIdentity {
|
|
2590
|
-
title?: string;
|
|
2591
|
-
logoUrl?: string;
|
|
2592
|
-
}
|
|
2593
|
-
|
|
2594
|
-
export interface SiteConfig {
|
|
2595
|
-
identity?: SiteConfigIdentity;
|
|
2596
|
-
pages?: Array<{ slug: string; label: string }>;
|
|
2597
|
-
header: {
|
|
2598
|
-
id: string;
|
|
2599
|
-
type: 'header';
|
|
2600
|
-
data: HeaderData;
|
|
2601
|
-
settings?: HeaderSettings;
|
|
2602
|
-
};
|
|
2603
|
-
footer: {
|
|
2604
|
-
id: string;
|
|
2605
|
-
type: 'footer';
|
|
2606
|
-
data: FooterData;
|
|
2607
|
-
settings?: FooterSettings;
|
|
2608
|
-
};
|
|
2609
|
-
}
|
|
2610
|
-
```
|
|
2611
|
-
|
|
2612
|
-
**HeaderData**, **FooterData**, **HeaderSettings**, **FooterSettings** are the types exported from the header and footer capsules.
|
|
2613
|
-
|
|
2614
|
-
### A.2.5 MenuConfig
|
|
2615
|
-
|
|
2616
|
-
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[]**.
|
|
2617
|
-
|
|
2618
|
-
```typescript
|
|
2619
|
-
export interface MenuConfig {
|
|
2620
|
-
main?: MenuItem[];
|
|
2621
|
-
[key: string]: MenuItem[] | undefined;
|
|
2622
|
-
}
|
|
2623
|
-
```
|
|
2624
|
-
|
|
2625
|
-
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).
|
|
2626
|
-
|
|
2627
|
-
### A.2.6 ThemeConfig
|
|
2628
|
-
|
|
2629
|
-
Minimum shape for **theme.json** (and for **themeConfig** in JsonPagesConfig). Tenant-defined; typically tokens for colors, typography, radius.
|
|
2630
|
-
|
|
2631
|
-
```typescript
|
|
2632
|
-
export interface ThemeConfig {
|
|
2633
|
-
name?: string;
|
|
2634
|
-
tokens?: {
|
|
2635
|
-
colors?: Record<string, string>;
|
|
2636
|
-
typography?: Record<string, string | Record<string, string>>;
|
|
2637
|
-
borderRadius?: Record<string, string>;
|
|
2638
|
-
};
|
|
2639
|
-
[key: string]: unknown;
|
|
2640
|
-
}
|
|
2641
|
-
```
|
|
2642
|
-
|
|
2643
|
-
---
|
|
2644
|
-
|
|
2645
|
-
## A.3 Schema Contract (SECTION_SCHEMAS)
|
|
2646
|
-
|
|
2647
|
-
**Location:** **`src/lib/schemas.ts`** (or equivalent).
|
|
2648
|
-
|
|
2649
|
-
**Contract:**
|
|
2650
|
-
* **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).
|
|
2651
|
-
* 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.
|
|
2652
|
-
* **SECTION_SCHEMAS** is typed as **`Record<SectionType, ZodType>`** or **`{ [K in SectionType]: ZodType }`** so that keys match the registry and SectionDataRegistry.
|
|
2653
|
-
|
|
2654
|
-
**Export:** The app imports **SECTION_SCHEMAS** and passes it as **config.schemas** to JsonPagesEngine. The Form Factory traverses these schemas to build editors.
|
|
2655
|
-
|
|
2656
|
-
**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.
|
|
2657
|
-
|
|
2658
|
-
---
|
|
2659
|
-
|
|
2660
|
-
## A.4 File Paths & Data Layout
|
|
2661
|
-
|
|
2662
|
-
| Purpose | Path (conventional) | Description |
|
|
2663
|
-
|---------|---------------------|-------------|
|
|
2664
|
-
| Site config | **`src/data/config/site.json`** | SiteConfig (identity, header, footer, pages list). |
|
|
2665
|
-
| Menu config | **`src/data/config/menu.json`** | MenuConfig (e.g. main nav). |
|
|
2666
|
-
| Theme config | **`src/data/config/theme.json`** | ThemeConfig (tokens). |
|
|
2667
|
-
| Page data | **`src/data/pages/<slug>.json`** | One file per page; content is PageConfig (slug, meta, sections). |
|
|
2668
|
-
| Base schemas | **`src/lib/base-schemas.ts`** | BaseSectionData, BaseArrayItem, BaseSectionSettingsSchema. |
|
|
2669
|
-
| Schema aggregate | **`src/lib/schemas.ts`** | SECTION_SCHEMAS; re-exports base schemas. |
|
|
2670
|
-
| Registry | **`src/lib/ComponentRegistry.tsx`** | ComponentRegistry object. |
|
|
2671
|
-
| Add-section config | **`src/lib/addSectionConfig.ts`** | addSectionConfig (AddSectionConfig). |
|
|
2672
|
-
| Tenant types & augmentation | **`src/types.ts`** | SectionComponentPropsMap, PageConfig, SiteConfig, MenuConfig, ThemeConfig; **declare module '@olonjs/core'** for SectionDataRegistry and SectionSettingsRegistry; re-export from Core. |
|
|
2673
|
-
| Bootstrap | **`src/App.tsx`** | Imports config (site, theme, menu, pages), registry, schemas, addSection, themeCss; builds JsonPagesConfig; renders **<JsonPagesEngine config={config} />**. |
|
|
2674
|
-
|
|
2675
|
-
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.
|
|
2676
|
-
|
|
2677
|
-
**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.
|
|
2678
|
-
|
|
2679
|
-
---
|
|
2680
|
-
|
|
2681
|
-
## A.5 Integration Checklist (Code-Generation)
|
|
2682
|
-
|
|
2683
|
-
When generating or auditing a tenant, ensure the following in order:
|
|
2684
|
-
|
|
2685
|
-
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).
|
|
2686
|
-
2. **Base schemas** — **src/lib/base-schemas.ts** exports BaseSectionData, BaseArrayItem, BaseSectionSettingsSchema (and optional CtaSchema or similar shared fragments).
|
|
2687
|
-
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`.
|
|
2688
|
-
4. **ComponentRegistry** — Import every View; build object **{ [K in SectionType]: ViewComponent }**; type as **{ [K in SectionType]: React.FC<SectionComponentPropsMap[K]> }**.
|
|
2689
|
-
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.
|
|
2690
|
-
6. **addSectionConfig** — addableSectionTypes, sectionTypeLabels, getDefaultSectionData; export as AddSectionConfig.
|
|
2691
|
-
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.
|
|
2692
|
-
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.
|
|
2693
|
-
9. **Tenant CSS** — Include TOCC (§7) selectors in global CSS so the Stage overlay is visible.
|
|
2694
|
-
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.
|
|
2695
|
-
|
|
2696
|
-
**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.
|
|
2697
|
-
|
|
2698
|
-
---
|
|
2699
|
-
|
|
2700
|
-
## A.6 v1.3 Path/Nested Strictness Addendum (breaking)
|
|
2701
|
-
|
|
2702
|
-
This addendum extends Appendix A without removing prior v1.2 obligations:
|
|
2703
|
-
|
|
2704
|
-
1. **Type exports** — Core and/or shared types module should expose `SelectionPathSegment` and `SelectionPath` for Studio messaging and Inspector expansion logic.
|
|
2705
|
-
2. **Protocol migration** — Replace flat payload fields `itemField` / `itemId` with `itemPath?: SelectionPath` in strict v1.3 channels.
|
|
2706
|
-
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).
|
|
2707
|
-
4. **Backward compatibility policy** — Legacy flat fields may exist only in transitional adapters outside strict mode; normative v1.3 contract is path-only.
|
|
2708
|
-
|
|
2709
|
-
---
|
|
2710
|
-
|
|
2711
|
-
**Validation:** Align with current `@olonjs/core` exports (SectionType, MenuItem, AddSectionConfig, JsonPagesConfig, and in v1.3 path types for Studio selection).
|
|
2712
|
-
**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.
|
|
2172
|
+
echo "Creating scripts/webmcp-feature-check.mjs..."
|
|
2173
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/webmcp-feature-check.mjs"
|
|
2174
|
+
import fs from 'fs/promises';
|
|
2175
|
+
import path from 'path';
|
|
2176
|
+
import { fileURLToPath } from 'url';
|
|
2177
|
+
import { createRequire } from 'module';
|
|
2713
2178
|
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
cat << 'END_OF_FILE_CONTENT' > "src/App.tsx"
|
|
2718
|
-
/**
|
|
2719
|
-
* Thin Entry Point (Tenant).
|
|
2720
|
-
* Data from getHydratedData (file-backed or draft); assets from public/assets/images.
|
|
2721
|
-
* Supports Hybrid Persistence: Local Filesystem (Dev) or Cloud Bridge (Prod).
|
|
2722
|
-
*/
|
|
2723
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2724
|
-
import { JsonPagesEngine } from '@olonjs/core';
|
|
2725
|
-
import type { JsonPagesConfig, LibraryImageEntry, ProjectState } from '@olonjs/core';
|
|
2726
|
-
import { ComponentRegistry } from '@/lib/ComponentRegistry';
|
|
2727
|
-
import { SECTION_SCHEMAS } from '@/lib/schemas';
|
|
2728
|
-
import { addSectionConfig } from '@/lib/addSectionConfig';
|
|
2729
|
-
import { getHydratedData } from '@/lib/draftStorage';
|
|
2730
|
-
import type { SiteConfig, ThemeConfig, MenuConfig, PageConfig } from '@/types';
|
|
2731
|
-
import type { DeployPhase, StepId } from '@/types/deploy';
|
|
2732
|
-
import { DEPLOY_STEPS } from '@/lib/deploySteps';
|
|
2733
|
-
import { startCloudSaveStream } from '@/lib/cloudSaveStream';
|
|
2734
|
-
import siteData from '@/data/config/site.json';
|
|
2735
|
-
import themeData from '@/data/config/theme.json';
|
|
2736
|
-
import menuData from '@/data/config/menu.json';
|
|
2737
|
-
import { getFilePages } from '@/lib/getFilePages';
|
|
2738
|
-
import { DopaDrawer } from '@/components/save-drawer/DopaDrawer';
|
|
2739
|
-
import { Skeleton } from '@/components/ui/skeleton';
|
|
2740
|
-
import { ThemeProvider } from '@/components/ThemeProvider';
|
|
2179
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
2180
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
2181
|
+
const baseUrl = process.env.WEBMCP_BASE_URL ?? 'http://127.0.0.1:4173';
|
|
2741
2182
|
|
|
2742
|
-
|
|
2183
|
+
function pageFilePathFromSlug(slug) {
|
|
2184
|
+
return path.resolve(rootDir, 'src', 'data', 'pages', `${slug}.json`);
|
|
2185
|
+
}
|
|
2743
2186
|
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
const CLOUD_API_KEY =
|
|
2748
|
-
import.meta.env.VITE_OLONJS_API_KEY ?? import.meta.env.VITE_JSONPAGES_API_KEY;
|
|
2187
|
+
function adminUrlFromSlug(slug) {
|
|
2188
|
+
return `${baseUrl}/admin${slug === 'home' ? '' : `/${slug}`}`;
|
|
2189
|
+
}
|
|
2749
2190
|
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2191
|
+
function isStringSchema(schema) {
|
|
2192
|
+
if (!schema || typeof schema !== 'object') return false;
|
|
2193
|
+
if (schema.type === 'string') return true;
|
|
2194
|
+
if (Array.isArray(schema.anyOf)) {
|
|
2195
|
+
return schema.anyOf.some((entry) => entry && typeof entry === 'object' && entry.type === 'string');
|
|
2196
|
+
}
|
|
2197
|
+
return false;
|
|
2198
|
+
}
|
|
2758
2199
|
|
|
2759
|
-
|
|
2760
|
-
const
|
|
2761
|
-
|
|
2762
|
-
const
|
|
2763
|
-
const
|
|
2764
|
-
|
|
2200
|
+
function findTopLevelStringField(sectionSchema) {
|
|
2201
|
+
const properties = sectionSchema?.properties;
|
|
2202
|
+
if (!properties || typeof properties !== 'object') return null;
|
|
2203
|
+
const preferred = ['title', 'sectionTitle', 'label', 'headline', 'name'];
|
|
2204
|
+
for (const key of preferred) {
|
|
2205
|
+
if (isStringSchema(properties[key])) return key;
|
|
2206
|
+
}
|
|
2207
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
2208
|
+
if (isStringSchema(value)) return key;
|
|
2209
|
+
}
|
|
2210
|
+
return null;
|
|
2211
|
+
}
|
|
2765
2212
|
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
type ContentMode = 'cloud' | 'error';
|
|
2777
|
-
type ContentStatus = 'ok' | 'empty_namespace' | 'legacy_fallback';
|
|
2778
|
-
|
|
2779
|
-
type ContentResponse = {
|
|
2780
|
-
ok?: boolean;
|
|
2781
|
-
siteConfig?: unknown;
|
|
2782
|
-
pages?: unknown;
|
|
2783
|
-
items?: unknown;
|
|
2784
|
-
error?: string;
|
|
2785
|
-
code?: string;
|
|
2786
|
-
correlationId?: string;
|
|
2787
|
-
contentStatus?: ContentStatus;
|
|
2788
|
-
usedUnscopedFallback?: boolean;
|
|
2789
|
-
namespace?: string;
|
|
2790
|
-
namespaceMatchedKeys?: number;
|
|
2791
|
-
};
|
|
2792
|
-
|
|
2793
|
-
type CachedCloudContent = {
|
|
2794
|
-
keyFingerprint: string;
|
|
2795
|
-
savedAt: number;
|
|
2796
|
-
siteConfig: unknown | null;
|
|
2797
|
-
pages: Record<string, unknown>;
|
|
2798
|
-
};
|
|
2799
|
-
|
|
2800
|
-
const CLOUD_CACHE_KEY = 'jp_cloud_content_cache_v1';
|
|
2801
|
-
const CLOUD_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
2802
|
-
|
|
2803
|
-
function normalizeApiBase(raw: string): string {
|
|
2804
|
-
return raw.trim().replace(/\/+$/, '');
|
|
2805
|
-
}
|
|
2806
|
-
|
|
2807
|
-
function buildApiCandidates(raw: string): string[] {
|
|
2808
|
-
const base = normalizeApiBase(raw);
|
|
2809
|
-
const withApi = /\/api\/v1$/i.test(base) ? base : `${base}/api/v1`;
|
|
2810
|
-
const candidates = [withApi, base];
|
|
2811
|
-
return Array.from(new Set(candidates.filter(Boolean)));
|
|
2213
|
+
async function loadPlaywright() {
|
|
2214
|
+
const require = createRequire(import.meta.url);
|
|
2215
|
+
try {
|
|
2216
|
+
return require('playwright');
|
|
2217
|
+
} catch (error) {
|
|
2218
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2219
|
+
throw new Error(
|
|
2220
|
+
`Playwright is required for WebMCP verification. Install it before running this script. Original error: ${message}`
|
|
2221
|
+
);
|
|
2222
|
+
}
|
|
2812
2223
|
}
|
|
2813
2224
|
|
|
2814
|
-
function
|
|
2815
|
-
|
|
2225
|
+
async function readPageJson(slug) {
|
|
2226
|
+
const pageFilePath = pageFilePathFromSlug(slug);
|
|
2227
|
+
const raw = await fs.readFile(pageFilePath, 'utf8');
|
|
2228
|
+
return { raw, json: JSON.parse(raw), pageFilePath };
|
|
2816
2229
|
}
|
|
2817
2230
|
|
|
2818
|
-
function
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2231
|
+
async function waitFor(predicate, timeoutMs, label) {
|
|
2232
|
+
const startedAt = Date.now();
|
|
2233
|
+
for (;;) {
|
|
2234
|
+
const result = await predicate();
|
|
2235
|
+
if (result) return result;
|
|
2236
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
2237
|
+
throw new Error(`Timed out while waiting for ${label}.`);
|
|
2238
|
+
}
|
|
2239
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
2240
|
+
}
|
|
2826
2241
|
}
|
|
2827
2242
|
|
|
2828
|
-
function
|
|
2829
|
-
|
|
2243
|
+
async function waitForFileFieldValue(slug, sectionId, fieldKey, expectedValue) {
|
|
2244
|
+
await waitFor(async () => {
|
|
2245
|
+
const { json } = await readPageJson(slug);
|
|
2246
|
+
const section = Array.isArray(json.sections)
|
|
2247
|
+
? json.sections.find((item) => item?.id === sectionId)
|
|
2248
|
+
: null;
|
|
2249
|
+
return section?.data?.[fieldKey] === expectedValue;
|
|
2250
|
+
}, 8_000, `file field "${fieldKey}" = "${expectedValue}"`);
|
|
2830
2251
|
}
|
|
2831
2252
|
|
|
2832
|
-
function
|
|
2833
|
-
|
|
2834
|
-
|
|
2253
|
+
async function ensureResponseOk(response, label) {
|
|
2254
|
+
if (!response.ok) {
|
|
2255
|
+
const text = await response.text();
|
|
2256
|
+
throw new Error(`${label} failed with ${response.status}: ${text}`);
|
|
2257
|
+
}
|
|
2258
|
+
return response;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
async function fetchJson(relativePath, label) {
|
|
2262
|
+
const response = await ensureResponseOk(await fetch(`${baseUrl}${relativePath}`), label);
|
|
2263
|
+
return response.json();
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
async function selectTarget() {
|
|
2267
|
+
const siteIndex = await fetchJson('/mcp-manifest.json', 'Manifest index request');
|
|
2268
|
+
const requestedSlug = typeof process.env.WEBMCP_TARGET_SLUG === 'string' && process.env.WEBMCP_TARGET_SLUG.trim()
|
|
2269
|
+
? process.env.WEBMCP_TARGET_SLUG.trim()
|
|
2270
|
+
: null;
|
|
2271
|
+
|
|
2272
|
+
const candidatePages = requestedSlug
|
|
2273
|
+
? (siteIndex.pages ?? []).filter((page) => page?.slug === requestedSlug)
|
|
2274
|
+
: (siteIndex.pages ?? []);
|
|
2275
|
+
|
|
2276
|
+
for (const pageEntry of candidatePages) {
|
|
2277
|
+
if (!pageEntry?.slug || !pageEntry?.manifestHref || !pageEntry?.contractHref) continue;
|
|
2278
|
+
const pageManifest = await fetchJson(pageEntry.manifestHref, `Page manifest request for ${pageEntry.slug}`);
|
|
2279
|
+
const pageContract = await fetchJson(pageEntry.contractHref, `Page contract request for ${pageEntry.slug}`);
|
|
2280
|
+
const localInstances = Array.isArray(pageContract.sectionInstances)
|
|
2281
|
+
? pageContract.sectionInstances.filter((section) => section?.scope === 'local')
|
|
2282
|
+
: [];
|
|
2283
|
+
const tools = Array.isArray(pageManifest.tools) ? pageManifest.tools : [];
|
|
2284
|
+
|
|
2285
|
+
for (const tool of tools) {
|
|
2286
|
+
const sectionType = tool?.sectionType;
|
|
2287
|
+
if (typeof tool?.name !== 'string' || typeof sectionType !== 'string') continue;
|
|
2288
|
+
const targetInstance = localInstances.find((section) => section?.type === sectionType);
|
|
2289
|
+
if (!targetInstance?.id) continue;
|
|
2290
|
+
const targetFieldKey = findTopLevelStringField(pageContract.sectionSchemas?.[sectionType]);
|
|
2291
|
+
if (!targetFieldKey) continue;
|
|
2292
|
+
const pageState = await readPageJson(pageEntry.slug);
|
|
2293
|
+
const section = Array.isArray(pageState.json.sections)
|
|
2294
|
+
? pageState.json.sections.find((item) => item?.id === targetInstance.id)
|
|
2295
|
+
: null;
|
|
2296
|
+
const originalValue = section?.data?.[targetFieldKey];
|
|
2297
|
+
if (typeof originalValue !== 'string') continue;
|
|
2298
|
+
|
|
2299
|
+
return {
|
|
2300
|
+
slug: pageEntry.slug,
|
|
2301
|
+
manifestHref: pageEntry.manifestHref,
|
|
2302
|
+
contractHref: pageEntry.contractHref,
|
|
2303
|
+
toolName: tool.name,
|
|
2304
|
+
sectionId: targetInstance.id,
|
|
2305
|
+
fieldKey: targetFieldKey,
|
|
2306
|
+
originalValue,
|
|
2307
|
+
originalState: pageState,
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2835
2311
|
|
|
2836
|
-
|
|
2837
|
-
|
|
2312
|
+
throw new Error(
|
|
2313
|
+
requestedSlug
|
|
2314
|
+
? `No valid WebMCP verification target found for page "${requestedSlug}".`
|
|
2315
|
+
: 'No valid WebMCP verification target found in manifest index.'
|
|
2316
|
+
);
|
|
2838
2317
|
}
|
|
2839
2318
|
|
|
2840
|
-
function
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2319
|
+
async function main() {
|
|
2320
|
+
const { chromium } = await loadPlaywright();
|
|
2321
|
+
const target = await selectTarget();
|
|
2322
|
+
const nextValue = `${target.originalValue} WebMCP ${Date.now()}`;
|
|
2323
|
+
const browser = await chromium.launch({ headless: true });
|
|
2324
|
+
const page = await browser.newPage();
|
|
2325
|
+
const consoleEvents = [];
|
|
2326
|
+
let mutationApplied = false;
|
|
2327
|
+
|
|
2328
|
+
page.on('console', (message) => {
|
|
2329
|
+
if (message.type() === 'error' || message.type() === 'warning') {
|
|
2330
|
+
consoleEvents.push(`[console:${message.type()}] ${message.text()}`);
|
|
2331
|
+
}
|
|
2332
|
+
});
|
|
2333
|
+
page.on('pageerror', (error) => {
|
|
2334
|
+
consoleEvents.push(`[pageerror] ${error.message}`);
|
|
2335
|
+
});
|
|
2846
2336
|
|
|
2847
|
-
|
|
2848
|
-
let input = value;
|
|
2849
|
-
if (typeof input === 'string') {
|
|
2337
|
+
const restoreOriginal = async () => {
|
|
2850
2338
|
try {
|
|
2851
|
-
|
|
2339
|
+
await page.evaluate(
|
|
2340
|
+
async ({ toolName, slug, sectionId, fieldKey, value }) => {
|
|
2341
|
+
const runtime = navigator.modelContextTesting;
|
|
2342
|
+
if (!runtime?.executeTool) return;
|
|
2343
|
+
await runtime.executeTool(
|
|
2344
|
+
toolName,
|
|
2345
|
+
JSON.stringify({
|
|
2346
|
+
slug,
|
|
2347
|
+
sectionId,
|
|
2348
|
+
fieldKey,
|
|
2349
|
+
value,
|
|
2350
|
+
})
|
|
2351
|
+
);
|
|
2352
|
+
},
|
|
2353
|
+
{
|
|
2354
|
+
toolName: target.toolName,
|
|
2355
|
+
slug: target.slug,
|
|
2356
|
+
sectionId: target.sectionId,
|
|
2357
|
+
fieldKey: target.fieldKey,
|
|
2358
|
+
value: target.originalValue,
|
|
2359
|
+
}
|
|
2360
|
+
);
|
|
2361
|
+
await waitForFileFieldValue(target.slug, target.sectionId, target.fieldKey, target.originalValue);
|
|
2852
2362
|
} catch {
|
|
2853
|
-
|
|
2363
|
+
await fs.writeFile(target.originalState.pageFilePath, target.originalState.raw, 'utf8');
|
|
2854
2364
|
}
|
|
2855
|
-
}
|
|
2856
|
-
if (!isObjectRecord(input) || !Array.isArray(input.sections)) return null;
|
|
2365
|
+
};
|
|
2857
2366
|
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2367
|
+
try {
|
|
2368
|
+
const pageManifest = await fetchJson(target.manifestHref, `Manifest request for ${target.slug}`);
|
|
2369
|
+
if (!Array.isArray(pageManifest.tools) || !pageManifest.tools.some((tool) => tool?.name === target.toolName)) {
|
|
2370
|
+
throw new Error(`Manifest does not expose ${target.toolName}.`);
|
|
2371
|
+
}
|
|
2863
2372
|
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
};
|
|
2871
|
-
}
|
|
2373
|
+
const pageContract = await fetchJson(target.contractHref, `Contract request for ${target.slug}`);
|
|
2374
|
+
if (!Array.isArray(pageContract.tools) || !pageContract.tools.some((tool) => tool?.name === target.toolName)) {
|
|
2375
|
+
throw new Error(`Page contract does not expose ${target.toolName}.`);
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
await page.goto(adminUrlFromSlug(target.slug), { waitUntil: 'networkidle' });
|
|
2872
2379
|
|
|
2873
|
-
function coerceSiteConfig(value: unknown): SiteConfig | null {
|
|
2874
|
-
let input = value;
|
|
2875
|
-
if (typeof input === 'string') {
|
|
2876
2380
|
try {
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2381
|
+
await page.waitForFunction(
|
|
2382
|
+
({ manifestHref, contractHref }) => {
|
|
2383
|
+
const manifestLink = document.head.querySelector('link[rel="mcp-manifest"]');
|
|
2384
|
+
const contractLink = document.head.querySelector('link[rel="olon-contract"]');
|
|
2385
|
+
return manifestLink?.getAttribute('href') === manifestHref
|
|
2386
|
+
&& contractLink?.getAttribute('href') === contractHref;
|
|
2387
|
+
},
|
|
2388
|
+
{ manifestHref: target.manifestHref, contractHref: target.contractHref },
|
|
2389
|
+
{ timeout: 10_000 }
|
|
2390
|
+
);
|
|
2391
|
+
} catch (error) {
|
|
2392
|
+
const diagnostics = await page.evaluate(() => ({
|
|
2393
|
+
head: document.head.innerHTML,
|
|
2394
|
+
bodyText: document.body.innerText,
|
|
2395
|
+
}));
|
|
2396
|
+
throw new Error(
|
|
2397
|
+
[
|
|
2398
|
+
error instanceof Error ? error.message : String(error),
|
|
2399
|
+
`head=${diagnostics.head}`,
|
|
2400
|
+
`body=${diagnostics.bodyText}`,
|
|
2401
|
+
...consoleEvents,
|
|
2402
|
+
].join('\n')
|
|
2403
|
+
);
|
|
2880
2404
|
}
|
|
2881
|
-
}
|
|
2882
|
-
if (!isObjectRecord(input)) return null;
|
|
2883
|
-
if (!isObjectRecord(input.identity)) return null;
|
|
2884
|
-
if (!Array.isArray(input.pages)) return null;
|
|
2885
2405
|
|
|
2886
|
-
|
|
2887
|
-
|
|
2406
|
+
const toolNames = await page.evaluate(() => {
|
|
2407
|
+
const runtime = navigator.modelContextTesting;
|
|
2408
|
+
return runtime?.listTools?.().map((tool) => tool.name) ?? [];
|
|
2409
|
+
});
|
|
2410
|
+
if (!toolNames.includes(target.toolName)) {
|
|
2411
|
+
throw new Error(`Runtime did not register ${target.toolName}. Found: ${toolNames.join(', ')}`);
|
|
2412
|
+
}
|
|
2888
2413
|
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2414
|
+
const rawResult = await page.evaluate(
|
|
2415
|
+
async ({ toolName, slug, sectionId, fieldKey, value }) => {
|
|
2416
|
+
const runtime = navigator.modelContextTesting;
|
|
2417
|
+
if (!runtime?.executeTool) {
|
|
2418
|
+
throw new Error('navigator.modelContextTesting.executeTool is unavailable.');
|
|
2419
|
+
}
|
|
2420
|
+
return runtime.executeTool(
|
|
2421
|
+
toolName,
|
|
2422
|
+
JSON.stringify({
|
|
2423
|
+
slug,
|
|
2424
|
+
sectionId,
|
|
2425
|
+
fieldKey,
|
|
2426
|
+
value,
|
|
2427
|
+
})
|
|
2428
|
+
);
|
|
2429
|
+
},
|
|
2430
|
+
{
|
|
2431
|
+
toolName: target.toolName,
|
|
2432
|
+
slug: target.slug,
|
|
2433
|
+
sectionId: target.sectionId,
|
|
2434
|
+
fieldKey: target.fieldKey,
|
|
2435
|
+
value: nextValue,
|
|
2436
|
+
}
|
|
2437
|
+
);
|
|
2895
2438
|
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2439
|
+
const parsedResult = JSON.parse(rawResult);
|
|
2440
|
+
if (parsedResult?.isError) {
|
|
2441
|
+
throw new Error(`WebMCP tool returned an error: ${rawResult}`);
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
mutationApplied = true;
|
|
2445
|
+
await waitForFileFieldValue(target.slug, target.sectionId, target.fieldKey, nextValue);
|
|
2446
|
+
await page.frameLocator('iframe').getByText(nextValue, { exact: true }).waitFor({ state: 'attached' });
|
|
2447
|
+
|
|
2448
|
+
console.log(
|
|
2449
|
+
JSON.stringify({
|
|
2450
|
+
ok: true,
|
|
2451
|
+
slug: target.slug,
|
|
2452
|
+
manifestHref: target.manifestHref,
|
|
2453
|
+
contractHref: target.contractHref,
|
|
2454
|
+
toolName: target.toolName,
|
|
2455
|
+
sectionId: target.sectionId,
|
|
2456
|
+
fieldKey: target.fieldKey,
|
|
2457
|
+
toolNames,
|
|
2458
|
+
})
|
|
2459
|
+
);
|
|
2460
|
+
} finally {
|
|
2461
|
+
if (mutationApplied) {
|
|
2462
|
+
await restoreOriginal();
|
|
2463
|
+
}
|
|
2464
|
+
await browser.close();
|
|
2905
2465
|
}
|
|
2906
|
-
return next;
|
|
2907
2466
|
}
|
|
2908
2467
|
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2468
|
+
main().catch((error) => {
|
|
2469
|
+
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
2470
|
+
process.exit(1);
|
|
2471
|
+
});
|
|
2912
2472
|
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
normalized[canonicalSlug] = { ...direct, slug: canonicalSlug };
|
|
2919
|
-
continue;
|
|
2920
|
-
}
|
|
2473
|
+
END_OF_FILE_CONTENT
|
|
2474
|
+
mkdir -p "specs"
|
|
2475
|
+
echo "Creating specs/olonjsSpecs_V.1.3.md..."
|
|
2476
|
+
cat << 'END_OF_FILE_CONTENT' > "specs/olonjsSpecs_V.1.3.md"
|
|
2477
|
+
# 📐 OlonJS Architecture Specifications v1.3
|
|
2921
2478
|
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
}
|
|
2926
|
-
}
|
|
2479
|
+
**Status:** Mandatory Standard
|
|
2480
|
+
**Version:** 1.3.0 (Sovereign Core Edition — Architecture + Studio/ICE UX, Path-Deterministic Nested Editing)
|
|
2481
|
+
**Target:** Senior Architects / AI Agents / Enterprise Governance
|
|
2927
2482
|
|
|
2928
|
-
|
|
2929
|
-
|
|
2483
|
+
**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.
|
|
2484
|
+
**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).
|
|
2930
2485
|
|
|
2931
|
-
|
|
2932
|
-
pagesSource: unknown;
|
|
2933
|
-
siteSource: unknown;
|
|
2934
|
-
} {
|
|
2935
|
-
// Canonical contract: { pages, siteConfig }
|
|
2936
|
-
if (isObjectRecord(payload) && isObjectRecord(payload.pages)) {
|
|
2937
|
-
return { pagesSource: payload.pages, siteSource: payload.siteConfig };
|
|
2938
|
-
}
|
|
2486
|
+
---
|
|
2939
2487
|
|
|
2940
|
-
|
|
2941
|
-
if (isObjectRecord(payload) && isObjectRecord(payload.items)) {
|
|
2942
|
-
const items = payload.items;
|
|
2943
|
-
let siteSource: unknown = null;
|
|
2944
|
-
const pageEntries: Record<string, unknown> = {};
|
|
2945
|
-
for (const [key, value] of Object.entries(items)) {
|
|
2946
|
-
if (/(_config_site|config_site|config:site)$/i.test(key)) {
|
|
2947
|
-
siteSource = value;
|
|
2948
|
-
continue;
|
|
2949
|
-
}
|
|
2950
|
-
if (/(_page_|^page_|page:)/i.test(key)) {
|
|
2951
|
-
pageEntries[key] = value;
|
|
2952
|
-
}
|
|
2953
|
-
}
|
|
2954
|
-
return { pagesSource: pageEntries, siteSource };
|
|
2955
|
-
}
|
|
2488
|
+
## 1. 📐 Modular Type Registry Pattern (MTRP) v1.2
|
|
2956
2489
|
|
|
2957
|
-
|
|
2958
|
-
return { pagesSource: payload, siteSource: null };
|
|
2959
|
-
}
|
|
2490
|
+
**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.
|
|
2960
2491
|
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
message: string;
|
|
2964
|
-
correlationId?: string;
|
|
2965
|
-
};
|
|
2492
|
+
### 1.1 The Sovereign Dependency Inversion
|
|
2493
|
+
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.
|
|
2966
2494
|
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
typeof value.message === 'string'
|
|
2972
|
-
);
|
|
2973
|
-
}
|
|
2495
|
+
### 1.2 Technical Implementation (`@olonjs/core/kernel`)
|
|
2496
|
+
```typescript
|
|
2497
|
+
export interface SectionDataRegistry {} // Augmented by Tenant
|
|
2498
|
+
export interface SectionSettingsRegistry {} // Augmented by Tenant
|
|
2974
2499
|
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2500
|
+
export interface BaseSection<K extends keyof SectionDataRegistry> {
|
|
2501
|
+
id: string;
|
|
2502
|
+
type: K;
|
|
2503
|
+
data: SectionDataRegistry[K];
|
|
2504
|
+
settings?: K extends keyof SectionSettingsRegistry
|
|
2505
|
+
? SectionSettingsRegistry[K]
|
|
2506
|
+
: BaseSectionSettings;
|
|
2981
2507
|
}
|
|
2982
2508
|
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
}
|
|
2509
|
+
export type Section = {
|
|
2510
|
+
[K in keyof SectionDataRegistry]: BaseSection<K>
|
|
2511
|
+
}[keyof SectionDataRegistry];
|
|
2512
|
+
```
|
|
2986
2513
|
|
|
2987
|
-
|
|
2988
|
-
return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
|
|
2989
|
-
}
|
|
2514
|
+
**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.
|
|
2990
2515
|
|
|
2991
|
-
|
|
2992
|
-
const base = 250 * Math.pow(2, attempt);
|
|
2993
|
-
const jitter = Math.floor(Math.random() * 120);
|
|
2994
|
-
return base + jitter;
|
|
2995
|
-
}
|
|
2516
|
+
**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`).
|
|
2996
2517
|
|
|
2997
|
-
|
|
2998
|
-
console.info('[boot]', { event, at: new Date().toISOString(), ...details });
|
|
2999
|
-
}
|
|
2518
|
+
---
|
|
3000
2519
|
|
|
3001
|
-
|
|
3002
|
-
return `${normalizeApiBase(apiBase)}::${apiKey.slice(-8)}`;
|
|
3003
|
-
}
|
|
2520
|
+
## 2. 📐 JsonPages Site Protocol (JSP) v1.8
|
|
3004
2521
|
|
|
3005
|
-
|
|
3006
|
-
return (
|
|
3007
|
-
slug
|
|
3008
|
-
.trim()
|
|
3009
|
-
.toLowerCase()
|
|
3010
|
-
.replace(/[^a-z0-9/_-]/g, '-')
|
|
3011
|
-
.replace(/^\/+|\/+$/g, '') || 'home'
|
|
3012
|
-
);
|
|
3013
|
-
}
|
|
2522
|
+
**Objective:** Define the deterministic file system and the **Sovereign Projection Engine** (CLI).
|
|
3014
2523
|
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
if (!parsed.savedAt || Date.now() - parsed.savedAt > CLOUD_CACHE_TTL_MS) return null;
|
|
3022
|
-
return parsed;
|
|
3023
|
-
} catch {
|
|
3024
|
-
return null;
|
|
3025
|
-
}
|
|
3026
|
-
}
|
|
2524
|
+
### 2.1 The File System Ontology (The Silo Contract)
|
|
2525
|
+
Every site must reside in an isolated directory. Global Governance is physically separated from Local Content.
|
|
2526
|
+
* **`/config/site.json`** — Global Identity & Reserved System Blocks (Header/Footer). See Appendix A for typed shape.
|
|
2527
|
+
* **`/config/menu.json`** — Navigation Tree (SSOT for System Header). See Appendix A.
|
|
2528
|
+
* **`/config/theme.json`** — Theme tokens (optional but recommended). See Appendix A.
|
|
2529
|
+
* **`/pages/[slug].json`** — Local Body Content per page. See Appendix A (PageConfig).
|
|
3027
2530
|
|
|
3028
|
-
|
|
3029
|
-
try {
|
|
3030
|
-
localStorage.setItem(CLOUD_CACHE_KEY, JSON.stringify(entry));
|
|
3031
|
-
} catch {
|
|
3032
|
-
// non-blocking cache path
|
|
3033
|
-
}
|
|
3034
|
-
}
|
|
2531
|
+
**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.
|
|
3035
2532
|
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
const primary = typeof fontFamily?.primary === 'string' ? fontFamily.primary : "'Instrument Sans', system-ui, sans-serif";
|
|
3042
|
-
const serif = typeof fontFamily?.serif === 'string' ? fontFamily.serif : "'Instrument Serif', Georgia, serif";
|
|
3043
|
-
const mono = typeof fontFamily?.mono === 'string' ? fontFamily.mono : "'JetBrains Mono', monospace";
|
|
3044
|
-
return `:root{--theme-font-primary:${primary};--theme-font-serif:${serif};--theme-font-mono:${mono};}`;
|
|
3045
|
-
}
|
|
2533
|
+
### 2.2 Deterministic Projection (CLI Workflow)
|
|
2534
|
+
The CLI (`@olonjs/cli`) creates new tenants by:
|
|
2535
|
+
1. **Infra Projection:** Generating `package.json`, `tsconfig.json`, and `vite.config.ts` (The Shell).
|
|
2536
|
+
2. **Source Projection:** Executing a deterministic script (`src_tenant_alpha.sh`) to reconstruct the `src` folder (The DNA).
|
|
2537
|
+
3. **Dependency Resolution:** Enforcing specific versions of React, Radix, and Tailwind v4.
|
|
3046
2538
|
|
|
3047
|
-
|
|
3048
|
-
if (typeof window !== 'undefined') {
|
|
3049
|
-
(window as Window & { __TENANT_PREVIEW_READY__?: boolean }).__TENANT_PREVIEW_READY__ = ready;
|
|
3050
|
-
}
|
|
3051
|
-
if (typeof document !== 'undefined' && document.body) {
|
|
3052
|
-
document.body.dataset.previewReady = ready ? '1' : '0';
|
|
3053
|
-
}
|
|
3054
|
-
}
|
|
2539
|
+
**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.
|
|
3055
2540
|
|
|
3056
|
-
|
|
3057
|
-
const isCloudMode = Boolean(CLOUD_API_URL && CLOUD_API_KEY);
|
|
3058
|
-
const localInitialData = useMemo(() => (isCloudMode ? null : getInitialData()), [isCloudMode]);
|
|
3059
|
-
const localInitialPages = useMemo(() => {
|
|
3060
|
-
if (!localInitialData) return {};
|
|
3061
|
-
const normalized = normalizePageRegistry(localInitialData.pages as unknown);
|
|
3062
|
-
return Object.keys(normalized).length > 0 ? normalized : localInitialData.pages;
|
|
3063
|
-
}, [localInitialData]);
|
|
3064
|
-
const [pages, setPages] = useState<Record<string, PageConfig>>(localInitialPages);
|
|
3065
|
-
const [siteConfig, setSiteConfig] = useState<SiteConfig>(
|
|
3066
|
-
localInitialData?.siteConfig ?? fileSiteConfig
|
|
3067
|
-
);
|
|
3068
|
-
const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
|
|
3069
|
-
const [cloudSaveUi, setCloudSaveUi] = useState<CloudSaveUiState>(getInitialCloudSaveUiState);
|
|
3070
|
-
const [contentMode, setContentMode] = useState<ContentMode>('cloud');
|
|
3071
|
-
const [contentFallback, setContentFallback] = useState<CloudLoadFailure | null>(null);
|
|
3072
|
-
const [showTopProgress, setShowTopProgress] = useState(false);
|
|
3073
|
-
const [hasInitialCloudResolved, setHasInitialCloudResolved] = useState(!isCloudMode);
|
|
3074
|
-
const [bootstrapRunId, setBootstrapRunId] = useState(0);
|
|
3075
|
-
const activeCloudSaveController = useRef<AbortController | null>(null);
|
|
3076
|
-
const contentLoadInFlight = useRef<Promise<void> | null>(null);
|
|
3077
|
-
const pendingCloudSave = useRef<{ state: ProjectState; slug: string } | null>(null);
|
|
3078
|
-
const cloudApiCandidates = useMemo(
|
|
3079
|
-
() => (isCloudMode && CLOUD_API_URL ? buildApiCandidates(CLOUD_API_URL) : []),
|
|
3080
|
-
[isCloudMode, CLOUD_API_URL]
|
|
3081
|
-
);
|
|
2541
|
+
---
|
|
3082
2542
|
|
|
3083
|
-
|
|
3084
|
-
if (isCloudMode && CLOUD_API_URL && CLOUD_API_KEY) {
|
|
3085
|
-
const apiBases = cloudApiCandidates.length > 0 ? cloudApiCandidates : [normalizeApiBase(CLOUD_API_URL)];
|
|
3086
|
-
for (const apiBase of apiBases) {
|
|
3087
|
-
try {
|
|
3088
|
-
const res = await fetch(`${apiBase}/assets/list?limit=200`, {
|
|
3089
|
-
method: 'GET',
|
|
3090
|
-
headers: {
|
|
3091
|
-
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
3092
|
-
},
|
|
3093
|
-
});
|
|
3094
|
-
const body = (await res.json().catch(() => ({}))) as { items?: LibraryImageEntry[] };
|
|
3095
|
-
if (!res.ok) continue;
|
|
3096
|
-
const items = Array.isArray(body.items) ? body.items : [];
|
|
3097
|
-
setAssetsManifest(items);
|
|
3098
|
-
return;
|
|
3099
|
-
} catch {
|
|
3100
|
-
// try next candidate
|
|
3101
|
-
}
|
|
3102
|
-
}
|
|
3103
|
-
setAssetsManifest([]);
|
|
3104
|
-
return;
|
|
3105
|
-
}
|
|
2543
|
+
## 3. 🧱 Tenant Block Protocol (TBP) v1.0
|
|
3106
2544
|
|
|
3107
|
-
|
|
3108
|
-
.then((r) => (r.ok ? r.json() : []))
|
|
3109
|
-
.then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
|
|
3110
|
-
.catch(() => setAssetsManifest([]));
|
|
3111
|
-
}, [isCloudMode, CLOUD_API_URL, CLOUD_API_KEY, cloudApiCandidates]);
|
|
2545
|
+
**Objective:** Standardize the "Capsule" structure for components to enable automated ingestion (Pull) by the SaaS.
|
|
3112
2546
|
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
2547
|
+
### 3.1 The Atomic Capsule Structure
|
|
2548
|
+
Components are self-contained directories under **`src/components/<sectionType>/`**:
|
|
2549
|
+
* **`View.tsx`** — The pure React component (Dumb View). Props: see Appendix A (SectionComponentPropsMap).
|
|
2550
|
+
* **`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).
|
|
2551
|
+
* **`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.
|
|
2552
|
+
* **`index.ts`** — Public API: re-exports View, schema(s), and types.
|
|
3116
2553
|
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
}, []);
|
|
2554
|
+
### 3.2 Reserved System Types
|
|
2555
|
+
* **`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[] }`.
|
|
2556
|
+
* **`type: 'footer'`** — Reserved for `site.json`. Props: `{ data: FooterData; settings?: FooterSettings }` only (no `menu`).
|
|
2557
|
+
* **`type: 'sectionHeader'`** — A standard local block. Must define its own `links` array in its local schema if used.
|
|
3122
2558
|
|
|
3123
|
-
|
|
3124
|
-
setTenantPreviewReady(false);
|
|
3125
|
-
return () => {
|
|
3126
|
-
setTenantPreviewReady(false);
|
|
3127
|
-
};
|
|
3128
|
-
}, []);
|
|
2559
|
+
**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.
|
|
3129
2560
|
|
|
3130
|
-
|
|
3131
|
-
if (!isCloudMode || !CLOUD_API_URL || !CLOUD_API_KEY) {
|
|
3132
|
-
setContentMode('cloud');
|
|
3133
|
-
setContentFallback(null);
|
|
3134
|
-
setShowTopProgress(false);
|
|
3135
|
-
setHasInitialCloudResolved(true);
|
|
3136
|
-
logBootstrapEvent('boot.local.ready', { mode: 'local' });
|
|
3137
|
-
return;
|
|
3138
|
-
}
|
|
3139
|
-
if (contentLoadInFlight.current) {
|
|
3140
|
-
return;
|
|
3141
|
-
}
|
|
2561
|
+
---
|
|
3142
2562
|
|
|
3143
|
-
|
|
3144
|
-
const maxRetryAttempts = 2;
|
|
3145
|
-
const startedAt = Date.now();
|
|
3146
|
-
const primaryApiBase = cloudApiCandidates[0] ?? normalizeApiBase(CLOUD_API_URL);
|
|
3147
|
-
const fingerprint = cloudFingerprint(primaryApiBase, CLOUD_API_KEY);
|
|
3148
|
-
const cached = readCachedCloudContent(fingerprint);
|
|
3149
|
-
const cachedPages = cached ? toPagesRecord(cached.pages) : null;
|
|
3150
|
-
const cachedSite = cached ? coerceSiteConfig(cached.siteConfig) : null;
|
|
3151
|
-
const hasCachedFallback = Boolean((cachedPages && Object.keys(cachedPages).length > 0) || cachedSite);
|
|
3152
|
-
if (cached) {
|
|
3153
|
-
logBootstrapEvent('boot.cloud.cache_hit', { ageMs: Date.now() - cached.savedAt });
|
|
3154
|
-
}
|
|
3155
|
-
setContentMode('cloud');
|
|
3156
|
-
setContentFallback(null);
|
|
3157
|
-
setShowTopProgress(true);
|
|
3158
|
-
setHasInitialCloudResolved(false);
|
|
3159
|
-
logBootstrapEvent('boot.start', { mode: 'cloud', apiCandidates: cloudApiCandidates.length });
|
|
2563
|
+
## 4. 🧱 Component Implementation Protocol (CIP) v1.5
|
|
3160
2564
|
|
|
3161
|
-
|
|
3162
|
-
try {
|
|
3163
|
-
let payload: ContentResponse | null = null;
|
|
3164
|
-
let lastFailure: CloudLoadFailure | null = null;
|
|
2565
|
+
**Objective:** Ensure system-wide stability and Admin UI integrity.
|
|
3165
2566
|
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
const res = await fetch(`${apiBase}/content`, {
|
|
3170
|
-
method: 'GET',
|
|
3171
|
-
cache: 'no-store',
|
|
3172
|
-
headers: {
|
|
3173
|
-
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
3174
|
-
},
|
|
3175
|
-
signal: controller.signal,
|
|
3176
|
-
});
|
|
2567
|
+
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).
|
|
2568
|
+
2. **Z-Index Neutrality:** Components must not use `z-index > 1`. Layout delegation (sticky/fixed) is managed by the `SectionRenderer`.
|
|
2569
|
+
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`).
|
|
3177
2570
|
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
lastFailure = {
|
|
3181
|
-
reasonCode: 'NON_JSON_RESPONSE',
|
|
3182
|
-
message: `Non-JSON response from ${apiBase}/content`,
|
|
3183
|
-
};
|
|
3184
|
-
break;
|
|
3185
|
-
}
|
|
2571
|
+
### 4.4 Local Design Tokens (v1.2)
|
|
2572
|
+
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.
|
|
3186
2573
|
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
lastFailure = {
|
|
3190
|
-
reasonCode: parsed.code || `HTTP_${res.status}`,
|
|
3191
|
-
message: parsed.error || `Cloud content read failed: ${res.status} (${apiBase}/content)`,
|
|
3192
|
-
correlationId: parsed.correlationId,
|
|
3193
|
-
};
|
|
3194
|
-
if (isRetryableStatus(res.status) && attempt < maxRetryAttempts) {
|
|
3195
|
-
await sleep(backoffDelayMs(attempt));
|
|
3196
|
-
continue;
|
|
3197
|
-
}
|
|
3198
|
-
break;
|
|
3199
|
-
}
|
|
2574
|
+
### 4.5 Z-Index & Overlay Governance (v1.2)
|
|
2575
|
+
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.
|
|
3200
2576
|
|
|
3201
|
-
|
|
3202
|
-
break;
|
|
3203
|
-
} catch (error: unknown) {
|
|
3204
|
-
if (controller.signal.aborted) throw error;
|
|
3205
|
-
const message = error instanceof Error ? error.message : 'Network error';
|
|
3206
|
-
lastFailure = {
|
|
3207
|
-
reasonCode: 'NETWORK_TRANSIENT',
|
|
3208
|
-
message: `${message} (${apiBase}/content)`,
|
|
3209
|
-
};
|
|
3210
|
-
if (attempt < maxRetryAttempts) {
|
|
3211
|
-
await sleep(backoffDelayMs(attempt));
|
|
3212
|
-
continue;
|
|
3213
|
-
}
|
|
3214
|
-
}
|
|
3215
|
-
}
|
|
3216
|
-
if (payload) {
|
|
3217
|
-
break;
|
|
3218
|
-
}
|
|
3219
|
-
}
|
|
2577
|
+
**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.
|
|
3220
2578
|
|
|
3221
|
-
|
|
3222
|
-
throw (
|
|
3223
|
-
lastFailure || {
|
|
3224
|
-
reasonCode: 'CLOUD_ENDPOINT_UNREACHABLE',
|
|
3225
|
-
message: 'Cloud content endpoint not reachable as JSON.',
|
|
3226
|
-
}
|
|
3227
|
-
);
|
|
3228
|
-
}
|
|
2579
|
+
---
|
|
3229
2580
|
|
|
3230
|
-
|
|
3231
|
-
const remotePages = toPagesRecord(pagesSource);
|
|
3232
|
-
const remoteSite = coerceSiteConfig(siteSource);
|
|
3233
|
-
const remotePageCount = remotePages ? Object.keys(remotePages).length : 0;
|
|
3234
|
-
if (remotePageCount === 0 && !remoteSite) {
|
|
3235
|
-
throw {
|
|
3236
|
-
reasonCode: payload.contentStatus === 'empty_namespace' ? 'EMPTY_NAMESPACE' : 'EMPTY_PAYLOAD',
|
|
3237
|
-
message: 'Cloud payload is empty for this tenant namespace.',
|
|
3238
|
-
correlationId: payload.correlationId,
|
|
3239
|
-
} satisfies CloudLoadFailure;
|
|
3240
|
-
}
|
|
3241
|
-
if (import.meta.env.DEV) {
|
|
3242
|
-
console.info('[content] cloud diagnostics', {
|
|
3243
|
-
contentStatus: payload.contentStatus ?? 'ok',
|
|
3244
|
-
namespace: payload.namespace,
|
|
3245
|
-
namespaceMatchedKeys: payload.namespaceMatchedKeys,
|
|
3246
|
-
usedUnscopedFallback: payload.usedUnscopedFallback,
|
|
3247
|
-
correlationId: payload.correlationId,
|
|
3248
|
-
});
|
|
3249
|
-
}
|
|
3250
|
-
if (remotePages && remotePageCount > 0) {
|
|
3251
|
-
setPages(remotePages);
|
|
3252
|
-
}
|
|
3253
|
-
if (remoteSite) {
|
|
3254
|
-
setSiteConfig(remoteSite);
|
|
3255
|
-
}
|
|
3256
|
-
writeCachedCloudContent({
|
|
3257
|
-
keyFingerprint: fingerprint,
|
|
3258
|
-
savedAt: Date.now(),
|
|
3259
|
-
siteConfig: remoteSite ?? null,
|
|
3260
|
-
pages: (remotePages ?? {}) as Record<string, unknown>,
|
|
3261
|
-
});
|
|
3262
|
-
setContentMode('cloud');
|
|
3263
|
-
setContentFallback(null);
|
|
3264
|
-
setHasInitialCloudResolved(true);
|
|
3265
|
-
logBootstrapEvent('boot.cloud.success', {
|
|
3266
|
-
mode: 'cloud',
|
|
3267
|
-
elapsedMs: Date.now() - startedAt,
|
|
3268
|
-
contentStatus: payload.contentStatus ?? 'ok',
|
|
3269
|
-
correlationId: payload.correlationId ?? null,
|
|
3270
|
-
});
|
|
3271
|
-
} catch (error: unknown) {
|
|
3272
|
-
if (controller.signal.aborted) return;
|
|
3273
|
-
const failure = toCloudLoadFailure(error);
|
|
3274
|
-
if (hasCachedFallback) {
|
|
3275
|
-
if (cachedPages && Object.keys(cachedPages).length > 0) {
|
|
3276
|
-
setPages(cachedPages);
|
|
3277
|
-
}
|
|
3278
|
-
if (cachedSite) {
|
|
3279
|
-
setSiteConfig(cachedSite);
|
|
3280
|
-
}
|
|
3281
|
-
setContentMode('cloud');
|
|
3282
|
-
setContentFallback({
|
|
3283
|
-
reasonCode: 'CLOUD_REFRESH_FAILED',
|
|
3284
|
-
message: failure.message,
|
|
3285
|
-
correlationId: failure.correlationId,
|
|
3286
|
-
});
|
|
3287
|
-
setHasInitialCloudResolved(true);
|
|
3288
|
-
} else {
|
|
3289
|
-
setContentMode('error');
|
|
3290
|
-
setContentFallback(failure);
|
|
3291
|
-
setHasInitialCloudResolved(true);
|
|
3292
|
-
}
|
|
3293
|
-
logBootstrapEvent('boot.cloud.error', {
|
|
3294
|
-
mode: 'cloud',
|
|
3295
|
-
elapsedMs: Date.now() - startedAt,
|
|
3296
|
-
reasonCode: failure.reasonCode,
|
|
3297
|
-
correlationId: failure.correlationId ?? null,
|
|
3298
|
-
});
|
|
3299
|
-
}
|
|
3300
|
-
};
|
|
2581
|
+
## 5. 🛠️ Editor Component Implementation Protocol (ECIP) v1.5
|
|
3301
2582
|
|
|
3302
|
-
|
|
3303
|
-
inFlight = loadCloudContent().finally(() => {
|
|
3304
|
-
setShowTopProgress(false);
|
|
3305
|
-
if (contentLoadInFlight.current === inFlight) {
|
|
3306
|
-
contentLoadInFlight.current = null;
|
|
3307
|
-
}
|
|
3308
|
-
});
|
|
3309
|
-
contentLoadInFlight.current = inFlight;
|
|
3310
|
-
return () => controller.abort();
|
|
3311
|
-
}, [isCloudMode, CLOUD_API_KEY, CLOUD_API_URL, cloudApiCandidates, bootstrapRunId]);
|
|
2583
|
+
**Objective:** Standardize the Polymorphic ICE engine.
|
|
3312
2584
|
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
rejectOnError: boolean
|
|
3317
|
-
): Promise<void> => {
|
|
3318
|
-
if (!CLOUD_API_URL || !CLOUD_API_KEY) {
|
|
3319
|
-
const noCloudError = new Error('Cloud mode is not configured.');
|
|
3320
|
-
if (rejectOnError) throw noCloudError;
|
|
3321
|
-
return;
|
|
3322
|
-
}
|
|
2585
|
+
1. **Recursive Form Factory:** The Admin UI builds forms by traversing the Zod ontology.
|
|
2586
|
+
2. **UI Metadata:** Use `.describe('ui:[widget]')` in schemas to pass instructions to the Form Factory.
|
|
2587
|
+
3. **Deterministic IDs:** Every object in a `ZodArray` must extend `BaseArrayItem` (containing an `id`) to ensure React reconciliation stability during reordering.
|
|
3323
2588
|
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
const controller = new AbortController();
|
|
3327
|
-
activeCloudSaveController.current = controller;
|
|
2589
|
+
### 5.4 UI Metadata Vocabulary (v1.2)
|
|
2590
|
+
Standard keys for the Form Factory:
|
|
3328
2591
|
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
2592
|
+
| Key | Use case |
|
|
2593
|
+
|-----|----------|
|
|
2594
|
+
| `ui:text` | Single-line text input. |
|
|
2595
|
+
| `ui:textarea` | Multi-line text. |
|
|
2596
|
+
| `ui:select` | Enum / single choice. |
|
|
2597
|
+
| `ui:number` | Numeric input. |
|
|
2598
|
+
| `ui:list` | Array of items; list editor (add/remove/reorder). |
|
|
2599
|
+
| `ui:icon-picker` | Icon selection. |
|
|
3336
2600
|
|
|
3337
|
-
|
|
3338
|
-
await startCloudSaveStream({
|
|
3339
|
-
apiBaseUrl: CLOUD_API_URL,
|
|
3340
|
-
apiKey: CLOUD_API_KEY,
|
|
3341
|
-
path: `src/data/pages/${payload.slug}.json`,
|
|
3342
|
-
content: payload.state.page,
|
|
3343
|
-
message: `Content update for ${payload.slug} via Visual Editor`,
|
|
3344
|
-
signal: controller.signal,
|
|
3345
|
-
onStep: (event) => {
|
|
3346
|
-
setCloudSaveUi((prev) => {
|
|
3347
|
-
if (event.status === 'running') {
|
|
3348
|
-
return {
|
|
3349
|
-
...prev,
|
|
3350
|
-
isOpen: true,
|
|
3351
|
-
phase: 'running',
|
|
3352
|
-
currentStepId: event.id,
|
|
3353
|
-
errorMessage: undefined,
|
|
3354
|
-
};
|
|
3355
|
-
}
|
|
2601
|
+
Unknown keys may be treated as `ui:text`. Array fields must use `BaseArrayItem` for items.
|
|
3356
2602
|
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
}
|
|
2603
|
+
### 5.5 Path-Only Nested Selection & Expansion (v1.3, breaking)
|
|
2604
|
+
In strict v1.3 Studio/Inspector behavior, nested editing targets are represented by **path segments from root to leaf**.
|
|
3360
2605
|
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
phase: 'running',
|
|
3366
|
-
currentStepId: event.id,
|
|
3367
|
-
doneSteps: nextDone,
|
|
3368
|
-
progress: stepProgress(nextDone),
|
|
3369
|
-
};
|
|
3370
|
-
});
|
|
3371
|
-
},
|
|
3372
|
-
onDone: (event) => {
|
|
3373
|
-
const completed = DEPLOY_STEPS.map((step) => step.id);
|
|
3374
|
-
setCloudSaveUi({
|
|
3375
|
-
isOpen: true,
|
|
3376
|
-
phase: 'done',
|
|
3377
|
-
currentStepId: 'live',
|
|
3378
|
-
doneSteps: completed,
|
|
3379
|
-
progress: 100,
|
|
3380
|
-
deployUrl: event.deployUrl,
|
|
3381
|
-
});
|
|
3382
|
-
},
|
|
3383
|
-
});
|
|
3384
|
-
} catch (error: unknown) {
|
|
3385
|
-
const message = error instanceof Error ? error.message : 'Cloud save failed.';
|
|
3386
|
-
setCloudSaveUi((prev) => ({
|
|
3387
|
-
...prev,
|
|
3388
|
-
isOpen: true,
|
|
3389
|
-
phase: 'error',
|
|
3390
|
-
errorMessage: message,
|
|
3391
|
-
}));
|
|
3392
|
-
if (rejectOnError) throw new Error(message);
|
|
3393
|
-
} finally {
|
|
3394
|
-
if (activeCloudSaveController.current === controller) {
|
|
3395
|
-
activeCloudSaveController.current = null;
|
|
3396
|
-
}
|
|
3397
|
-
}
|
|
3398
|
-
},
|
|
3399
|
-
[]
|
|
3400
|
-
);
|
|
2606
|
+
```typescript
|
|
2607
|
+
export type SelectionPathSegment = { fieldKey: string; itemId?: string };
|
|
2608
|
+
export type SelectionPath = SelectionPathSegment[];
|
|
2609
|
+
```
|
|
3401
2610
|
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
2611
|
+
Rules:
|
|
2612
|
+
* Expansion and focus for nested arrays **must** be computed from `SelectionPath` (root → leaf), not from a single flat pair.
|
|
2613
|
+
* Matching by `fieldKey` alone is non-compliant for nested structures.
|
|
2614
|
+
* Legacy flat payload fields **`itemField`** and **`itemId`** are removed from strict v1.3 selection protocol.
|
|
3405
2615
|
|
|
3406
|
-
|
|
3407
|
-
if (!pendingCloudSave.current) return;
|
|
3408
|
-
void runCloudSave(pendingCloudSave.current, false);
|
|
3409
|
-
}, [runCloudSave]);
|
|
2616
|
+
**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.
|
|
3410
2617
|
|
|
3411
|
-
|
|
3412
|
-
tenantId: TENANT_ID,
|
|
3413
|
-
registry: ComponentRegistry as JsonPagesConfig['registry'],
|
|
3414
|
-
schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
|
|
3415
|
-
pages,
|
|
3416
|
-
siteConfig,
|
|
3417
|
-
themeConfig,
|
|
3418
|
-
menuConfig,
|
|
3419
|
-
refDocuments,
|
|
3420
|
-
themeCss: { tenant: `${buildThemeFontVarsCss(themeConfig)}\n${tenantCss}` },
|
|
3421
|
-
addSection: addSectionConfig,
|
|
3422
|
-
persistence: {
|
|
3423
|
-
async saveToFile(state: ProjectState, slug: string): Promise<void> {
|
|
3424
|
-
// 💻 LOCAL FILESYSTEM (Development / legacy fallback)
|
|
3425
|
-
console.log(`💻 Saving ${slug} to Local Filesystem...`);
|
|
3426
|
-
const res = await fetch('/api/save-to-file', {
|
|
3427
|
-
method: 'POST',
|
|
3428
|
-
headers: { 'Content-Type': 'application/json' },
|
|
3429
|
-
body: JSON.stringify({ projectState: state, slug }),
|
|
3430
|
-
});
|
|
3431
|
-
|
|
3432
|
-
const body = (await res.json().catch(() => ({}))) as { error?: string };
|
|
3433
|
-
if (!res.ok) throw new Error(body.error ?? `Save to file failed: ${res.status}`);
|
|
3434
|
-
},
|
|
3435
|
-
async hotSave(state: ProjectState, slug: string): Promise<void> {
|
|
3436
|
-
if (!isCloudMode || !CLOUD_API_URL || !CLOUD_API_KEY) {
|
|
3437
|
-
throw new Error('Cloud mode is not configured for hot save.');
|
|
3438
|
-
}
|
|
3439
|
-
const apiBase = CLOUD_API_URL.replace(/\/$/, '');
|
|
3440
|
-
const res = await fetch(`${apiBase}/hotSave`, {
|
|
3441
|
-
method: 'POST',
|
|
3442
|
-
headers: {
|
|
3443
|
-
'Content-Type': 'application/json',
|
|
3444
|
-
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
3445
|
-
},
|
|
3446
|
-
body: JSON.stringify({
|
|
3447
|
-
slug,
|
|
3448
|
-
page: state.page,
|
|
3449
|
-
siteConfig: state.site,
|
|
3450
|
-
}),
|
|
3451
|
-
});
|
|
3452
|
-
const body = (await res.json().catch(() => ({}))) as { error?: string; code?: string };
|
|
3453
|
-
if (!res.ok) {
|
|
3454
|
-
throw new Error(body.error || body.code || `Hot save failed: ${res.status}`);
|
|
3455
|
-
}
|
|
3456
|
-
const keyFingerprint = cloudFingerprint(apiBase, CLOUD_API_KEY);
|
|
3457
|
-
const normalizedSlug = normalizeSlugForCache(slug);
|
|
3458
|
-
const existing = readCachedCloudContent(keyFingerprint);
|
|
3459
|
-
writeCachedCloudContent({
|
|
3460
|
-
keyFingerprint,
|
|
3461
|
-
savedAt: Date.now(),
|
|
3462
|
-
siteConfig: state.site ?? null,
|
|
3463
|
-
pages: {
|
|
3464
|
-
...(existing?.pages ?? {}),
|
|
3465
|
-
[normalizedSlug]: state.page,
|
|
3466
|
-
},
|
|
3467
|
-
});
|
|
3468
|
-
},
|
|
3469
|
-
showLegacySave: !isCloudMode,
|
|
3470
|
-
showHotSave: isCloudMode,
|
|
3471
|
-
},
|
|
3472
|
-
assets: {
|
|
3473
|
-
assetsBaseUrl: '/assets',
|
|
3474
|
-
assetsManifest,
|
|
3475
|
-
async onAssetUpload(file: File): Promise<string> {
|
|
3476
|
-
if (!file.type.startsWith('image/')) throw new Error('Invalid file type.');
|
|
3477
|
-
if (!ALLOWED_IMAGE_MIME_TYPES.has(file.type)) {
|
|
3478
|
-
throw new Error('Unsupported image format. Allowed: jpeg, png, webp, gif, avif.');
|
|
3479
|
-
}
|
|
3480
|
-
if (file.size > MAX_UPLOAD_SIZE_BYTES) throw new Error(`File too large. Max ${MAX_UPLOAD_SIZE_BYTES / 1024 / 1024}MB.`);
|
|
2618
|
+
---
|
|
3481
2619
|
|
|
3482
|
-
|
|
3483
|
-
const apiBases = cloudApiCandidates.length > 0 ? cloudApiCandidates : [normalizeApiBase(CLOUD_API_URL)];
|
|
3484
|
-
let lastError: Error | null = null;
|
|
3485
|
-
for (const apiBase of apiBases) {
|
|
3486
|
-
for (let attempt = 0; attempt <= ASSET_UPLOAD_MAX_RETRIES; attempt += 1) {
|
|
3487
|
-
try {
|
|
3488
|
-
const formData = new FormData();
|
|
3489
|
-
formData.append('file', file);
|
|
3490
|
-
formData.append('filename', file.name);
|
|
3491
|
-
const controller = new AbortController();
|
|
3492
|
-
const timeout = window.setTimeout(() => controller.abort(), ASSET_UPLOAD_TIMEOUT_MS);
|
|
3493
|
-
const res = await fetch(`${apiBase}/assets/upload`, {
|
|
3494
|
-
method: 'POST',
|
|
3495
|
-
headers: {
|
|
3496
|
-
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
3497
|
-
'X-Correlation-Id': crypto.randomUUID(),
|
|
3498
|
-
},
|
|
3499
|
-
body: formData,
|
|
3500
|
-
signal: controller.signal,
|
|
3501
|
-
}).finally(() => window.clearTimeout(timeout));
|
|
3502
|
-
const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string; code?: string };
|
|
3503
|
-
if (res.ok && typeof body.url === 'string') {
|
|
3504
|
-
await loadAssetsManifest().catch(() => undefined);
|
|
3505
|
-
return body.url;
|
|
3506
|
-
}
|
|
3507
|
-
lastError = new Error(body.error || body.code || `Cloud upload failed: ${res.status}`);
|
|
3508
|
-
if (isRetryableStatus(res.status) && attempt < ASSET_UPLOAD_MAX_RETRIES) {
|
|
3509
|
-
await sleep(backoffDelayMs(attempt));
|
|
3510
|
-
continue;
|
|
3511
|
-
}
|
|
3512
|
-
break;
|
|
3513
|
-
} catch (error: unknown) {
|
|
3514
|
-
const message = error instanceof Error ? error.message : 'Cloud upload failed.';
|
|
3515
|
-
lastError = new Error(message);
|
|
3516
|
-
if (attempt < ASSET_UPLOAD_MAX_RETRIES) {
|
|
3517
|
-
await sleep(backoffDelayMs(attempt));
|
|
3518
|
-
continue;
|
|
3519
|
-
}
|
|
3520
|
-
break;
|
|
3521
|
-
}
|
|
3522
|
-
}
|
|
3523
|
-
}
|
|
3524
|
-
throw lastError ?? new Error('Cloud upload failed.');
|
|
3525
|
-
}
|
|
2620
|
+
## 6. 🎯 ICE Data Attribute Contract (IDAC) v1.1
|
|
3526
2621
|
|
|
3527
|
-
|
|
3528
|
-
const reader = new FileReader();
|
|
3529
|
-
reader.onload = () => resolve((reader.result as string).split(',')[1] ?? '');
|
|
3530
|
-
reader.onerror = () => reject(reader.error);
|
|
3531
|
-
reader.readAsDataURL(file);
|
|
3532
|
-
});
|
|
2622
|
+
**Objective:** Mandatory data attributes so the Stage (iframe) and Inspector can bind selection and field/item editing without coupling to Tenant DOM.
|
|
3533
2623
|
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
});
|
|
3539
|
-
const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string };
|
|
3540
|
-
if (!res.ok) throw new Error(body.error || `Upload failed: ${res.status}`);
|
|
3541
|
-
if (typeof body.url !== 'string') throw new Error('Invalid server response: missing url');
|
|
3542
|
-
await loadAssetsManifest().catch(() => undefined);
|
|
3543
|
-
return body.url;
|
|
3544
|
-
},
|
|
3545
|
-
},
|
|
3546
|
-
};
|
|
2624
|
+
### 6.1 Section-Level Markup (Core-Provided)
|
|
2625
|
+
**SectionRenderer** (Core) wraps each section root with:
|
|
2626
|
+
* **`data-section-id`** — Section instance ID (e.g. UUID). On the wrapper that contains content + overlay.
|
|
2627
|
+
* Sibling overlay element **`data-jp-section-overlay`** — Selection ring and type label. **Tenant does not add this;** Core injects it.
|
|
3547
2628
|
|
|
3548
|
-
|
|
2629
|
+
Tenant Views render the **content** root only (e.g. `<section>` or `<div>`), placed **inside** the Core wrapper.
|
|
3549
2630
|
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
setTenantPreviewReady(false);
|
|
3553
|
-
return;
|
|
3554
|
-
}
|
|
3555
|
-
let cancelled = false;
|
|
3556
|
-
let raf1 = 0;
|
|
3557
|
-
let raf2 = 0;
|
|
3558
|
-
raf1 = window.requestAnimationFrame(() => {
|
|
3559
|
-
raf2 = window.requestAnimationFrame(() => {
|
|
3560
|
-
if (!cancelled) setTenantPreviewReady(true);
|
|
3561
|
-
});
|
|
3562
|
-
});
|
|
3563
|
-
return () => {
|
|
3564
|
-
cancelled = true;
|
|
3565
|
-
window.cancelAnimationFrame(raf1);
|
|
3566
|
-
window.cancelAnimationFrame(raf2);
|
|
3567
|
-
setTenantPreviewReady(false);
|
|
3568
|
-
};
|
|
3569
|
-
}, [shouldRenderEngine, pages, siteConfig]);
|
|
2631
|
+
### 6.2 Field-Level Binding (Tenant-Provided)
|
|
2632
|
+
For every **editable scalar field** the View **must** attach **`data-jp-field="<fieldKey>"`** (key matches schema path: e.g. `title`, `description`, `sectionTitle`, `label`).
|
|
3570
2633
|
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
2634
|
+
### 6.3 Array-Item Binding (Tenant-Provided)
|
|
2635
|
+
For every **editable array item** the View **must** attach:
|
|
2636
|
+
* **`data-jp-item-id="<stableId>"`** — Prefer `item.id`; fallback e.g. `legacy-${index}` only outside strict mode.
|
|
2637
|
+
* **`data-jp-item-field="<arrayKey>"`** — e.g. `cards`, `layers`, `products`, `paragraphs`.
|
|
2638
|
+
|
|
2639
|
+
### 6.4 Compliance
|
|
2640
|
+
**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.
|
|
2641
|
+
|
|
2642
|
+
### 6.5 Strict Path Extraction for Nested Arrays (v1.3, breaking)
|
|
2643
|
+
For nested array targets, the Core/Inspector contract is path-based:
|
|
2644
|
+
* The runtime selection target is expressed as `itemPath: SelectionPath` (root → leaf).
|
|
2645
|
+
* Flat identity (`itemField` + `itemId`) is not sufficient for nested structures and is removed in strict v1.3 payloads.
|
|
2646
|
+
* In strict mode, index-based identity fallback is non-compliant for editable object arrays.
|
|
2647
|
+
|
|
2648
|
+
**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.
|
|
2649
|
+
|
|
2650
|
+
---
|
|
2651
|
+
|
|
2652
|
+
## 7. 🎨 Tenant Overlay CSS Contract (TOCC) v1.0
|
|
2653
|
+
|
|
2654
|
+
**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.
|
|
2655
|
+
|
|
2656
|
+
### 7.1 Required Selectors (Tenant global CSS)
|
|
2657
|
+
1. **`[data-jp-section-overlay]`** — `position: absolute; inset: 0`; `pointer-events: none`; base state transparent.
|
|
2658
|
+
2. **`[data-section-id]:hover [data-jp-section-overlay]`** — Hover: e.g. dashed border, subtle tint.
|
|
2659
|
+
3. **`[data-section-id][data-jp-selected] [data-jp-section-overlay]`** — Selected: solid border, optional tint.
|
|
2660
|
+
4. **`[data-jp-section-overlay] > div`** (type label) — Position and visibility (e.g. visible on hover/selected).
|
|
2661
|
+
|
|
2662
|
+
### 7.2 Z-Index
|
|
2663
|
+
Overlay **z-index** high (e.g. 9999). Section content at or below CIP limit (§4.5).
|
|
2664
|
+
|
|
2665
|
+
### 7.3 Responsibility
|
|
2666
|
+
**Core:** Injects wrapper and overlay DOM; sets `data-jp-selected`. **Tenant:** All overlay **visual** rules.
|
|
2667
|
+
|
|
2668
|
+
**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.
|
|
2669
|
+
|
|
2670
|
+
---
|
|
2671
|
+
|
|
2672
|
+
## 8. 📦 Base Section Data & Settings (BSDS) v1.0
|
|
2673
|
+
|
|
2674
|
+
**Objective:** Standardize base schema fragments for anchors, array items, and section settings.
|
|
2675
|
+
|
|
2676
|
+
### 8.1 BaseSectionData
|
|
2677
|
+
Every section data schema **must** extend a base with at least **`anchorId`** (optional string). Canonical Zod (Tenant `lib/base-schemas.ts` or equivalent):
|
|
2678
|
+
|
|
2679
|
+
```typescript
|
|
2680
|
+
export const BaseSectionData = z.object({
|
|
2681
|
+
anchorId: z.string().optional().describe('ui:text'),
|
|
2682
|
+
});
|
|
2683
|
+
```
|
|
2684
|
+
|
|
2685
|
+
### 8.2 BaseArrayItem
|
|
2686
|
+
Every array item schema editable in the Inspector **must** include **`id`** (optional string minimum). Canonical Zod:
|
|
2687
|
+
|
|
2688
|
+
```typescript
|
|
2689
|
+
export const BaseArrayItem = z.object({
|
|
2690
|
+
id: z.string().optional(),
|
|
2691
|
+
});
|
|
2692
|
+
```
|
|
2693
|
+
|
|
2694
|
+
Recommended: required UUID for new items. Used by `data-jp-item-id` and React reconciliation.
|
|
2695
|
+
|
|
2696
|
+
### 8.3 BaseSectionSettings (Optional)
|
|
2697
|
+
Common section-level settings. Canonical Zod (name **BaseSectionSettingsSchema** or as exported by Core):
|
|
2698
|
+
|
|
2699
|
+
```typescript
|
|
2700
|
+
export const BaseSectionSettingsSchema = z.object({
|
|
2701
|
+
paddingTop: z.enum(['none', 'sm', 'md', 'lg', 'xl', '2xl']).default('md').describe('ui:select'),
|
|
2702
|
+
paddingBottom: z.enum(['none', 'sm', 'md', 'lg', 'xl', '2xl']).default('md').describe('ui:select'),
|
|
2703
|
+
theme: z.enum(['dark', 'light', 'accent']).default('dark').describe('ui:select'),
|
|
2704
|
+
container: z.enum(['boxed', 'fluid']).default('boxed').describe('ui:select'),
|
|
2705
|
+
});
|
|
2706
|
+
```
|
|
2707
|
+
|
|
2708
|
+
Capsules may extend this for type-specific settings. Core may export **BaseSectionSettings** as the TypeScript type inferred from this or a superset.
|
|
2709
|
+
|
|
2710
|
+
**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.
|
|
2711
|
+
|
|
2712
|
+
---
|
|
2713
|
+
|
|
2714
|
+
## 9. 📌 AddSectionConfig (ASC) v1.0
|
|
2715
|
+
|
|
2716
|
+
**Objective:** Formalize the "Add Section" contract used by the Studio.
|
|
2717
|
+
|
|
2718
|
+
**Type (Core exports `AddSectionConfig`):**
|
|
2719
|
+
```typescript
|
|
2720
|
+
interface AddSectionConfig {
|
|
2721
|
+
addableSectionTypes: readonly string[];
|
|
2722
|
+
sectionTypeLabels: Record<string, string>;
|
|
2723
|
+
getDefaultSectionData(sectionType: string): Record<string, unknown>;
|
|
2724
|
+
}
|
|
2725
|
+
```
|
|
2726
|
+
|
|
2727
|
+
**Shape:** Tenant provides one object (e.g. `addSectionConfig`) with:
|
|
2728
|
+
* **`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.
|
|
2729
|
+
* **`sectionTypeLabels`** — Map type key → display string (e.g. `{ hero: 'Hero', 'cta-banner': 'CTA Banner' }`).
|
|
2730
|
+
* **`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.
|
|
2731
|
+
|
|
2732
|
+
Core creates a new section with deterministic UUID, `type`, and `data` from `getDefaultSectionData(type)`.
|
|
2733
|
+
|
|
2734
|
+
**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.
|
|
2735
|
+
|
|
2736
|
+
---
|
|
2737
|
+
|
|
2738
|
+
## 10. ⚙️ JsonPagesConfig & Engine Bootstrap (JEB) v1.1
|
|
2739
|
+
|
|
2740
|
+
**Objective:** Bootstrap contract between Tenant app and `@olonjs/core`.
|
|
2741
|
+
|
|
2742
|
+
### 10.1 JsonPagesConfig (required fields)
|
|
2743
|
+
The Tenant passes a single **config** object to **JsonPagesEngine**. Required fields:
|
|
2744
|
+
|
|
2745
|
+
| Field | Type | Description |
|
|
2746
|
+
|-------|------|-------------|
|
|
2747
|
+
| **tenantId** | string | Passed to `resolveAssetUrl(path, tenantId)`; resolved asset URLs are **`/assets/...`** with no tenantId segment in the path. |
|
|
2748
|
+
| **registry** | `{ [K in SectionType]: React.FC<SectionComponentPropsMap[K]> }` | Component registry. Must match MTRP keys. See Appendix A. |
|
|
2749
|
+
| **schemas** | `Record<SectionType, ZodType>` or equivalent | SECTION_SCHEMAS: type → **data** Zod schema. Form Factory uses this. See Appendix A. |
|
|
2750
|
+
| **pages** | `Record<string, PageConfig>` | Slug → page config. See Appendix A. |
|
|
2751
|
+
| **siteConfig** | SiteConfig | Global site (identity, header/footer blocks). See Appendix A. |
|
|
2752
|
+
| **themeConfig** | ThemeConfig | Theme tokens. See Appendix A. |
|
|
2753
|
+
| **menuConfig** | MenuConfig | Navigation tree (SSOT for header menu). See Appendix A. |
|
|
2754
|
+
| **themeCss** | `{ tenant: string }` | At least **tenant**: string (inline CSS or URL) for Stage iframe injection. |
|
|
2755
|
+
| **addSection** | AddSectionConfig | Add-section config (§9). |
|
|
2756
|
+
|
|
2757
|
+
Core may define optional fields. The Tenant must not omit required fields.
|
|
2758
|
+
|
|
2759
|
+
### 10.2 JsonPagesEngine
|
|
2760
|
+
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.
|
|
2761
|
+
|
|
2762
|
+
### 10.3 Studio Selection Event Contract (v1.3, breaking)
|
|
2763
|
+
In strict v1.3 Studio, section selection payload for nested targets is path-based:
|
|
2764
|
+
|
|
2765
|
+
```typescript
|
|
2766
|
+
type SectionSelectMessage = {
|
|
2767
|
+
type: 'SECTION_SELECT';
|
|
2768
|
+
section: { id: string; type: string; scope: 'global' | 'local' };
|
|
2769
|
+
itemPath?: SelectionPath; // root -> leaf
|
|
2770
|
+
};
|
|
2771
|
+
```
|
|
2772
|
+
|
|
2773
|
+
Removed from strict protocol:
|
|
2774
|
+
* `itemField`
|
|
2775
|
+
* `itemId`
|
|
2776
|
+
|
|
2777
|
+
**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.
|
|
2778
|
+
|
|
2779
|
+
---
|
|
2780
|
+
|
|
2781
|
+
# 🏛️ OlonJS_ADMIN_PROTOCOL (JAP) v1.2
|
|
2782
|
+
|
|
2783
|
+
**Status:** Mandatory Standard
|
|
2784
|
+
**Version:** 1.2.0 (Sovereign Shell Edition — Path/Nested Strictness)
|
|
2785
|
+
**Objective:** Deterministic orchestration of the "Studio" environment (ICE Level 1).
|
|
2786
|
+
|
|
2787
|
+
---
|
|
2788
|
+
|
|
2789
|
+
## 1. The Sovereign Shell Topology
|
|
2790
|
+
The Admin interface is a **Sovereign Shell** from `@olonjs/core`.
|
|
2791
|
+
1. **The Stage (Canvas):** Isolated Iframe; postMessage for data updates and selection mirroring. Section markup follows **IDAC** (§6); overlay styling follows **TOCC** (§7).
|
|
2792
|
+
2. **The Inspector (Sidebar):** Consumes Tenant Zod schemas to generate editors; binding via `data-jp-field` and `data-jp-item-*`.
|
|
2793
|
+
3. **The Studio Actions:** Save to file, Hot Save, Add Section.
|
|
2794
|
+
|
|
2795
|
+
## 2. State Orchestration & Persistence
|
|
2796
|
+
* **Working Draft:** Reactive local state for unsaved changes.
|
|
2797
|
+
* **Sync Law:** Inspector changes → Working Draft → Stage via `STUDIO_EVENTS.UPDATE_DRAFTS`.
|
|
2798
|
+
* **Persistence Protocol:** Studio invokes tenant-provided `saveToFile` and `hotSave` callbacks for editorial persistence.
|
|
2799
|
+
|
|
2800
|
+
## 3. Context Switching (Global vs. Local)
|
|
2801
|
+
* **Header/Footer** selection → Global Mode, `site.json`.
|
|
2802
|
+
* Any other section → Page Mode, current `[slug].json`.
|
|
2803
|
+
|
|
2804
|
+
## 4. Section Lifecycle Management
|
|
2805
|
+
1. **Add Section:** Modal from Tenant `SECTION_SCHEMAS`; UUID + default data via **AddSectionConfig** (§9).
|
|
2806
|
+
2. **Reorder:** Inspector or Stage Overlay; array mutation in Working Draft.
|
|
2807
|
+
3. **Delete:** Confirmation; remove from array, clear selection.
|
|
2808
|
+
|
|
2809
|
+
## 5. Stage Isolation & Overlay
|
|
2810
|
+
* **CSS Shielding:** Stage in Iframe; Tenant CSS does not leak into Admin.
|
|
2811
|
+
* **Sovereign Overlay:** Selection ring and type labels injected per **IDAC** (§6); Tenant styles them per **TOCC** (§7).
|
|
2812
|
+
|
|
2813
|
+
## 6. "Green Build" Validation
|
|
2814
|
+
Studio enforces `tsc && vite build`. No Studio or SSG build should proceed with TypeScript errors.
|
|
2815
|
+
|
|
2816
|
+
## 7. Path-Deterministic Selection & Sidebar Expansion (v1.3, breaking)
|
|
2817
|
+
* Section/item focus synchronization uses `itemPath` (root → leaf), not flat `itemField/itemId`.
|
|
2818
|
+
* Sidebar expansion state for nested arrays must be derived from all path segments.
|
|
2819
|
+
* Flat-only matching may open/close wrong branches and is non-compliant in strict mode.
|
|
2820
|
+
|
|
2821
|
+
**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.
|
|
2822
|
+
|
|
2823
|
+
---
|
|
2824
|
+
|
|
2825
|
+
## Compliance: Legacy vs Full UX (v1.3)
|
|
2826
|
+
|
|
2827
|
+
| Dimension | Legacy / Less UX | Full UX (Core-aligned) |
|
|
2828
|
+
|-----------|-------------------|-------------------------|
|
|
2829
|
+
| **ICE binding** | No `data-jp-*`; Inspector cannot bind. | IDAC (§6) on every editable section/field/item. |
|
|
2830
|
+
| **Section wrapper** | Plain `<section>`; no overlay contract. | Core wrapper + overlay; Tenant CSS per TOCC (§7). |
|
|
2831
|
+
| **Design tokens** | Raw BEM / fixed classes. | Local tokens (§4.4); `var(--local-*)` only. |
|
|
2832
|
+
| **Base schemas** | Ad hoc. | BSDS (§8): BaseSectionData, BaseArrayItem, BaseSectionSettings. |
|
|
2833
|
+
| **Add Section** | Ad hoc defaults. | ASC (§9): addableSectionTypes, labels, getDefaultSectionData. |
|
|
2834
|
+
| **Bootstrap** | Implicit. | JEB (§10): JsonPagesConfig + JsonPagesEngine. |
|
|
2835
|
+
| **Selection payload** | Flat `itemField/itemId`. | Path-only `itemPath: SelectionPath` (JEB §10.3). |
|
|
2836
|
+
| **Nested array expansion** | Single-segment or field-only heuristics. | Root-to-leaf path expansion (ECIP §5.5, JAP §7). |
|
|
2837
|
+
| **Array item identity (strict)** | Index fallback tolerated. | Stable `id` required for editable object arrays. |
|
|
2838
|
+
|
|
2839
|
+
**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.
|
|
2840
|
+
|
|
2841
|
+
---
|
|
2842
|
+
|
|
2843
|
+
## Summary of v1.3 Additions
|
|
2844
|
+
|
|
2845
|
+
| § | Title | Purpose |
|
|
2846
|
+
|---|--------|--------|
|
|
2847
|
+
| 5.5 | Path-Only Nested Selection & Expansion | ECIP: root→leaf `SelectionPath`; remove flat matching in strict mode. |
|
|
2848
|
+
| 6.5 | Strict Path Extraction for Nested Arrays | IDAC: path-based nested targeting; no strict flat fallback. |
|
|
2849
|
+
| 10.3 | Studio Selection Event Contract | JEB: `SECTION_SELECT` uses `itemPath`; remove `itemField/itemId`. |
|
|
2850
|
+
| JAP §7 | Path-Deterministic Selection & Sidebar Expansion | Studio state synchronization for nested arrays. |
|
|
2851
|
+
| Compliance | Legacy vs Full UX (v1.3) | Explicit breaking delta for flat protocol removal and strict IDs. |
|
|
2852
|
+
| **Appendix A.6** | **v1.3 Path/Nested Strictness Addendum** | Type/export and migration checklist for path-only protocol. |
|
|
2853
|
+
|
|
2854
|
+
---
|
|
2855
|
+
|
|
2856
|
+
# Appendix A — Tenant Type & Code-Generation Annex
|
|
2857
|
+
|
|
2858
|
+
**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.
|
|
2859
|
+
|
|
2860
|
+
**Status:** Mandatory for code-generation and governance. Compliance ensures generated tenants are typed and wired like the reference implementation.
|
|
2861
|
+
|
|
2862
|
+
---
|
|
2863
|
+
|
|
2864
|
+
## A.1 Core-Provided Types (from `@olonjs/core`)
|
|
2865
|
+
|
|
2866
|
+
The following are assumed to be exported by Core. The Tenant augments **SectionDataRegistry** and **SectionSettingsRegistry**; all other types are consumed as-is.
|
|
2867
|
+
|
|
2868
|
+
| Type | Description |
|
|
2869
|
+
|------|-------------|
|
|
2870
|
+
| **SectionType** | `keyof SectionDataRegistry` (after Tenant augmentation). Union of all section type keys. |
|
|
2871
|
+
| **Section** | Union of `BaseSection<K>` for all K in SectionDataRegistry. See MTRP §1.2. |
|
|
2872
|
+
| **BaseSectionSettings** | Optional base type for section settings (may align with BSDS §8.3). |
|
|
2873
|
+
| **MenuItem** | Navigation item. **Minimum shape:** `{ label: string; href: string }`. Core may extend (e.g. `children?: MenuItem[]`). |
|
|
2874
|
+
| **AddSectionConfig** | See §9. |
|
|
2875
|
+
| **JsonPagesConfig** | See §10.1. |
|
|
2876
|
+
|
|
2877
|
+
**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.
|
|
2878
|
+
|
|
2879
|
+
---
|
|
2880
|
+
|
|
2881
|
+
## A.2 Tenant-Provided Types (single source: `src/types.ts` or equivalent)
|
|
2882
|
+
|
|
2883
|
+
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.
|
|
2884
|
+
|
|
2885
|
+
### A.2.1 SectionComponentPropsMap
|
|
2886
|
+
|
|
2887
|
+
Maps each section type to the props of its React component. **Header** is the only type that receives **menu**.
|
|
2888
|
+
|
|
2889
|
+
**Option A — Explicit (recommended for clarity and tooling):** For each section type K, add one entry. Header receives **menu**.
|
|
2890
|
+
|
|
2891
|
+
```typescript
|
|
2892
|
+
import type { MenuItem } from '@olonjs/core';
|
|
2893
|
+
// Import Data/Settings from each capsule.
|
|
2894
|
+
|
|
2895
|
+
export type SectionComponentPropsMap = {
|
|
2896
|
+
'header': { data: HeaderData; settings?: HeaderSettings; menu: MenuItem[] };
|
|
2897
|
+
'footer': { data: FooterData; settings?: FooterSettings };
|
|
2898
|
+
'hero': { data: HeroData; settings?: HeroSettings };
|
|
2899
|
+
// ... one entry per SectionType, e.g. 'feature-grid', 'cta-banner', etc.
|
|
2900
|
+
};
|
|
2901
|
+
```
|
|
2902
|
+
|
|
2903
|
+
**Option B — Mapped type (DRY, requires SectionDataRegistry/SectionSettingsRegistry in scope):**
|
|
2904
|
+
|
|
2905
|
+
```typescript
|
|
2906
|
+
import type { MenuItem } from '@olonjs/core';
|
|
2907
|
+
|
|
2908
|
+
export type SectionComponentPropsMap = {
|
|
2909
|
+
[K in SectionType]: K extends 'header'
|
|
2910
|
+
? { data: SectionDataRegistry[K]; settings?: SectionSettingsRegistry[K]; menu: MenuItem[] }
|
|
2911
|
+
: { data: SectionDataRegistry[K]; settings?: K extends keyof SectionSettingsRegistry ? SectionSettingsRegistry[K] : BaseSectionSettings };
|
|
2912
|
+
};
|
|
2913
|
+
```
|
|
2914
|
+
|
|
2915
|
+
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.
|
|
2916
|
+
|
|
2917
|
+
**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.
|
|
2918
|
+
|
|
2919
|
+
### A.2.2 ComponentRegistry type
|
|
2920
|
+
|
|
2921
|
+
The registry object **must** be typed as:
|
|
2922
|
+
|
|
2923
|
+
```typescript
|
|
2924
|
+
import type { SectionType } from '@olonjs/core';
|
|
2925
|
+
import type { SectionComponentPropsMap } from '@/types';
|
|
2926
|
+
|
|
2927
|
+
export const ComponentRegistry: {
|
|
2928
|
+
[K in SectionType]: React.FC<SectionComponentPropsMap[K]>;
|
|
2929
|
+
} = { /* ... */ };
|
|
2930
|
+
```
|
|
2931
|
+
|
|
2932
|
+
File: **`src/lib/ComponentRegistry.tsx`** (or equivalent). Imports one View per section type and assigns it to the corresponding key.
|
|
2933
|
+
|
|
2934
|
+
### A.2.3 PageConfig
|
|
2935
|
+
|
|
2936
|
+
Minimum shape for a single page (used in **pages** and in each **`[slug].json`**):
|
|
2937
|
+
|
|
2938
|
+
```typescript
|
|
2939
|
+
export interface PageConfig {
|
|
2940
|
+
id?: string;
|
|
2941
|
+
slug: string;
|
|
2942
|
+
meta?: {
|
|
2943
|
+
title?: string;
|
|
2944
|
+
description?: string;
|
|
2945
|
+
};
|
|
2946
|
+
sections: Section[];
|
|
2947
|
+
}
|
|
2948
|
+
```
|
|
2949
|
+
|
|
2950
|
+
**Section** is the union type from MTRP (§1.2). Each element of **sections** has **id**, **type**, **data**, **settings** and conforms to the capsule schemas.
|
|
2951
|
+
|
|
2952
|
+
### A.2.4 SiteConfig
|
|
2953
|
+
|
|
2954
|
+
Minimum shape for **site.json** (and for **siteConfig** in JsonPagesConfig):
|
|
2955
|
+
|
|
2956
|
+
```typescript
|
|
2957
|
+
export interface SiteConfigIdentity {
|
|
2958
|
+
title?: string;
|
|
2959
|
+
logoUrl?: string;
|
|
3696
2960
|
}
|
|
3697
2961
|
|
|
3698
|
-
export
|
|
2962
|
+
export interface SiteConfig {
|
|
2963
|
+
identity?: SiteConfigIdentity;
|
|
2964
|
+
pages?: Array<{ slug: string; label: string }>;
|
|
2965
|
+
header: {
|
|
2966
|
+
id: string;
|
|
2967
|
+
type: 'header';
|
|
2968
|
+
data: HeaderData;
|
|
2969
|
+
settings?: HeaderSettings;
|
|
2970
|
+
};
|
|
2971
|
+
footer: {
|
|
2972
|
+
id: string;
|
|
2973
|
+
type: 'footer';
|
|
2974
|
+
data: FooterData;
|
|
2975
|
+
settings?: FooterSettings;
|
|
2976
|
+
};
|
|
2977
|
+
}
|
|
2978
|
+
```
|
|
2979
|
+
|
|
2980
|
+
**HeaderData**, **FooterData**, **HeaderSettings**, **FooterSettings** are the types exported from the header and footer capsules.
|
|
2981
|
+
|
|
2982
|
+
### A.2.5 MenuConfig
|
|
2983
|
+
|
|
2984
|
+
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[]**.
|
|
2985
|
+
|
|
2986
|
+
```typescript
|
|
2987
|
+
export interface MenuConfig {
|
|
2988
|
+
main?: MenuItem[];
|
|
2989
|
+
[key: string]: MenuItem[] | undefined;
|
|
2990
|
+
}
|
|
2991
|
+
```
|
|
2992
|
+
|
|
2993
|
+
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).
|
|
2994
|
+
|
|
2995
|
+
### A.2.6 ThemeConfig
|
|
2996
|
+
|
|
2997
|
+
Minimum shape for **theme.json** (and for **themeConfig** in JsonPagesConfig). Tenant-defined; typically tokens for colors, typography, radius.
|
|
2998
|
+
|
|
2999
|
+
```typescript
|
|
3000
|
+
export interface ThemeConfig {
|
|
3001
|
+
name?: string;
|
|
3002
|
+
tokens?: {
|
|
3003
|
+
colors?: Record<string, string>;
|
|
3004
|
+
typography?: Record<string, string | Record<string, string>>;
|
|
3005
|
+
borderRadius?: Record<string, string>;
|
|
3006
|
+
};
|
|
3007
|
+
[key: string]: unknown;
|
|
3008
|
+
}
|
|
3009
|
+
```
|
|
3010
|
+
|
|
3011
|
+
---
|
|
3012
|
+
|
|
3013
|
+
## A.3 Schema Contract (SECTION_SCHEMAS)
|
|
3014
|
+
|
|
3015
|
+
**Location:** **`src/lib/schemas.ts`** (or equivalent).
|
|
3016
|
+
|
|
3017
|
+
**Contract:**
|
|
3018
|
+
* **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).
|
|
3019
|
+
* 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.
|
|
3020
|
+
* **SECTION_SCHEMAS** is typed as **`Record<SectionType, ZodType>`** or **`{ [K in SectionType]: ZodType }`** so that keys match the registry and SectionDataRegistry.
|
|
3021
|
+
|
|
3022
|
+
**Export:** The app imports **SECTION_SCHEMAS** and passes it as **config.schemas** to JsonPagesEngine. The Form Factory traverses these schemas to build editors.
|
|
3023
|
+
|
|
3024
|
+
**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.
|
|
3025
|
+
|
|
3026
|
+
---
|
|
3027
|
+
|
|
3028
|
+
## A.4 File Paths & Data Layout
|
|
3699
3029
|
|
|
3030
|
+
| Purpose | Path (conventional) | Description |
|
|
3031
|
+
|---------|---------------------|-------------|
|
|
3032
|
+
| Site config | **`src/data/config/site.json`** | SiteConfig (identity, header, footer, pages list). |
|
|
3033
|
+
| Menu config | **`src/data/config/menu.json`** | MenuConfig (e.g. main nav). |
|
|
3034
|
+
| Theme config | **`src/data/config/theme.json`** | ThemeConfig (tokens). |
|
|
3035
|
+
| Page data | **`src/data/pages/<slug>.json`** | One file per page; content is PageConfig (slug, meta, sections). |
|
|
3036
|
+
| Base schemas | **`src/lib/base-schemas.ts`** | BaseSectionData, BaseArrayItem, BaseSectionSettingsSchema. |
|
|
3037
|
+
| Schema aggregate | **`src/lib/schemas.ts`** | SECTION_SCHEMAS; re-exports base schemas. |
|
|
3038
|
+
| Registry | **`src/lib/ComponentRegistry.tsx`** | ComponentRegistry object. |
|
|
3039
|
+
| Add-section config | **`src/lib/addSectionConfig.ts`** | addSectionConfig (AddSectionConfig). |
|
|
3040
|
+
| Tenant types & augmentation | **`src/types.ts`** | SectionComponentPropsMap, PageConfig, SiteConfig, MenuConfig, ThemeConfig; **declare module '@olonjs/core'** for SectionDataRegistry and SectionSettingsRegistry; re-export from Core. |
|
|
3041
|
+
| Bootstrap | **`src/App.tsx`** | Imports config (site, theme, menu, pages), registry, schemas, addSection, themeCss; builds JsonPagesConfig; renders **<JsonPagesEngine config={config} />**. |
|
|
3042
|
+
|
|
3043
|
+
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.
|
|
3044
|
+
|
|
3045
|
+
**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.
|
|
3046
|
+
|
|
3047
|
+
---
|
|
3048
|
+
|
|
3049
|
+
## A.5 Integration Checklist (Code-Generation)
|
|
3050
|
+
|
|
3051
|
+
When generating or auditing a tenant, ensure the following in order:
|
|
3052
|
+
|
|
3053
|
+
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).
|
|
3054
|
+
2. **Base schemas** — **src/lib/base-schemas.ts** exports BaseSectionData, BaseArrayItem, BaseSectionSettingsSchema (and optional CtaSchema or similar shared fragments).
|
|
3055
|
+
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`.
|
|
3056
|
+
4. **ComponentRegistry** — Import every View; build object **{ [K in SectionType]: ViewComponent }**; type as **{ [K in SectionType]: React.FC<SectionComponentPropsMap[K]> }**.
|
|
3057
|
+
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.
|
|
3058
|
+
6. **addSectionConfig** — addableSectionTypes, sectionTypeLabels, getDefaultSectionData; export as AddSectionConfig.
|
|
3059
|
+
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.
|
|
3060
|
+
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.
|
|
3061
|
+
9. **Tenant CSS** — Include TOCC (§7) selectors in global CSS so the Stage overlay is visible.
|
|
3062
|
+
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.
|
|
3063
|
+
|
|
3064
|
+
**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.
|
|
3065
|
+
|
|
3066
|
+
---
|
|
3067
|
+
|
|
3068
|
+
## A.6 v1.3 Path/Nested Strictness Addendum (breaking)
|
|
3069
|
+
|
|
3070
|
+
This addendum extends Appendix A without removing prior v1.2 obligations:
|
|
3071
|
+
|
|
3072
|
+
1. **Type exports** — Core and/or shared types module should expose `SelectionPathSegment` and `SelectionPath` for Studio messaging and Inspector expansion logic.
|
|
3073
|
+
2. **Protocol migration** — Replace flat payload fields `itemField` / `itemId` with `itemPath?: SelectionPath` in strict v1.3 channels.
|
|
3074
|
+
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).
|
|
3075
|
+
4. **Backward compatibility policy** — Legacy flat fields may exist only in transitional adapters outside strict mode; normative v1.3 contract is path-only.
|
|
3076
|
+
|
|
3077
|
+
---
|
|
3078
|
+
|
|
3079
|
+
**Validation:** Align with current `@olonjs/core` exports (SectionType, MenuItem, AddSectionConfig, JsonPagesConfig, and in v1.3 path types for Studio selection).
|
|
3080
|
+
**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.
|
|
3700
3081
|
|
|
3701
3082
|
END_OF_FILE_CONTENT
|
|
3702
|
-
|
|
3703
|
-
|
|
3083
|
+
mkdir -p "src"
|
|
3084
|
+
echo "Creating src/App.tsx..."
|
|
3085
|
+
cat << 'END_OF_FILE_CONTENT' > "src/App.tsx"
|
|
3704
3086
|
/**
|
|
3705
3087
|
* Thin Entry Point (Tenant).
|
|
3706
3088
|
* Data from getHydratedData (file-backed or draft); assets from public/assets/images.
|
|
@@ -3723,6 +3105,7 @@ import menuData from '@/data/config/menu.json';
|
|
|
3723
3105
|
import { getFilePages } from '@/lib/getFilePages';
|
|
3724
3106
|
import { DopaDrawer } from '@/components/save-drawer/DopaDrawer';
|
|
3725
3107
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
3108
|
+
import { ThemeProvider } from '@/components/ThemeProvider';
|
|
3726
3109
|
|
|
3727
3110
|
import tenantCss from './index.css?inline';
|
|
3728
3111
|
|
|
@@ -3733,7 +3116,12 @@ const CLOUD_API_KEY =
|
|
|
3733
3116
|
import.meta.env.VITE_OLONJS_API_KEY ?? import.meta.env.VITE_JSONPAGES_API_KEY;
|
|
3734
3117
|
|
|
3735
3118
|
const themeConfig = themeData as unknown as ThemeConfig;
|
|
3736
|
-
const menuConfig =
|
|
3119
|
+
const menuConfig: MenuConfig = { main: [] };
|
|
3120
|
+
const refDocuments = {
|
|
3121
|
+
'menu.json': menuData,
|
|
3122
|
+
'config/menu.json': menuData,
|
|
3123
|
+
'src/data/config/menu.json': menuData,
|
|
3124
|
+
} satisfies NonNullable<JsonPagesConfig['refDocuments']>;
|
|
3737
3125
|
const TENANT_ID = 'alpha';
|
|
3738
3126
|
|
|
3739
3127
|
const filePages = getFilePages();
|
|
@@ -3817,6 +3205,13 @@ function asString(value: unknown, fallback: string): string {
|
|
|
3817
3205
|
return typeof value === 'string' && value.trim() ? value : fallback;
|
|
3818
3206
|
}
|
|
3819
3207
|
|
|
3208
|
+
function normalizeRouteSlug(value: string): string {
|
|
3209
|
+
return value
|
|
3210
|
+
.toLowerCase()
|
|
3211
|
+
.replace(/[^a-z0-9/_-]/g, '-')
|
|
3212
|
+
.replace(/^\/+|\/+$/g, '') || 'home';
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3820
3215
|
function coercePageConfig(slug: string, value: unknown): PageConfig | null {
|
|
3821
3216
|
let input = value;
|
|
3822
3217
|
if (typeof input === 'string') {
|
|
@@ -3862,11 +3257,8 @@ function coerceSiteConfig(value: unknown): SiteConfig | null {
|
|
|
3862
3257
|
function toPagesRecord(value: unknown): Record<string, PageConfig> | null {
|
|
3863
3258
|
const directPage = coercePageConfig('home', value);
|
|
3864
3259
|
if (directPage) {
|
|
3865
|
-
const directSlug = asString(directPage.slug, 'home')
|
|
3866
|
-
|
|
3867
|
-
.replace(/[^a-z0-9/_-]/g, '-')
|
|
3868
|
-
.replace(/^\/+|\/+$/g, '') || 'home';
|
|
3869
|
-
return { [directSlug]: directPage };
|
|
3260
|
+
const directSlug = normalizeRouteSlug(asString(directPage.slug, 'home'));
|
|
3261
|
+
return { [directSlug]: { ...directPage, slug: directSlug } };
|
|
3870
3262
|
}
|
|
3871
3263
|
|
|
3872
3264
|
if (!isObjectRecord(value)) return null;
|
|
@@ -3874,14 +3266,10 @@ function toPagesRecord(value: unknown): Record<string, PageConfig> | null {
|
|
|
3874
3266
|
for (const [rawKey, payload] of Object.entries(value)) {
|
|
3875
3267
|
const rawKeyTrimmed = rawKey.trim();
|
|
3876
3268
|
const slugFromNamespacedKey = rawKeyTrimmed.match(/^t_[a-z0-9-]+_page_(.+)$/i)?.[1];
|
|
3877
|
-
const
|
|
3878
|
-
.toLowerCase()
|
|
3879
|
-
.replace(/[^a-z0-9/_-]/g, '-')
|
|
3880
|
-
.replace(/^\/+|\/+$/g, '');
|
|
3881
|
-
const slug = normalizedSlug || 'home';
|
|
3269
|
+
const slug = normalizeRouteSlug(slugFromNamespacedKey ?? rawKeyTrimmed);
|
|
3882
3270
|
const page = coercePageConfig(slug, payload);
|
|
3883
3271
|
if (!page) continue;
|
|
3884
|
-
next[slug] = page;
|
|
3272
|
+
next[slug] = { ...page, slug };
|
|
3885
3273
|
}
|
|
3886
3274
|
return next;
|
|
3887
3275
|
}
|
|
@@ -3891,9 +3279,11 @@ function normalizePageRegistry(value: unknown): Record<string, PageConfig> {
|
|
|
3891
3279
|
const normalized: Record<string, PageConfig> = {};
|
|
3892
3280
|
|
|
3893
3281
|
for (const [registrySlug, rawPageValue] of Object.entries(value)) {
|
|
3894
|
-
const
|
|
3282
|
+
const canonicalSlug = normalizeRouteSlug(registrySlug);
|
|
3283
|
+
const direct = coercePageConfig(canonicalSlug, rawPageValue);
|
|
3895
3284
|
if (direct) {
|
|
3896
|
-
|
|
3285
|
+
// Canonical key comes from registry/path, not from page JSON internal slug.
|
|
3286
|
+
normalized[canonicalSlug] = { ...direct, slug: canonicalSlug };
|
|
3897
3287
|
continue;
|
|
3898
3288
|
}
|
|
3899
3289
|
|
|
@@ -4022,6 +3412,15 @@ function buildThemeFontVarsCss(input: unknown): string {
|
|
|
4022
3412
|
return `:root{--theme-font-primary:${primary};--theme-font-serif:${serif};--theme-font-mono:${mono};}`;
|
|
4023
3413
|
}
|
|
4024
3414
|
|
|
3415
|
+
function setTenantPreviewReady(ready: boolean): void {
|
|
3416
|
+
if (typeof window !== 'undefined') {
|
|
3417
|
+
(window as Window & { __TENANT_PREVIEW_READY__?: boolean }).__TENANT_PREVIEW_READY__ = ready;
|
|
3418
|
+
}
|
|
3419
|
+
if (typeof document !== 'undefined' && document.body) {
|
|
3420
|
+
document.body.dataset.previewReady = ready ? '1' : '0';
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
|
|
4025
3424
|
function App() {
|
|
4026
3425
|
const isCloudMode = Boolean(CLOUD_API_URL && CLOUD_API_KEY);
|
|
4027
3426
|
const localInitialData = useMemo(() => (isCloudMode ? null : getInitialData()), [isCloudMode]);
|
|
@@ -4089,6 +3488,13 @@ function App() {
|
|
|
4089
3488
|
};
|
|
4090
3489
|
}, []);
|
|
4091
3490
|
|
|
3491
|
+
useEffect(() => {
|
|
3492
|
+
setTenantPreviewReady(false);
|
|
3493
|
+
return () => {
|
|
3494
|
+
setTenantPreviewReady(false);
|
|
3495
|
+
};
|
|
3496
|
+
}, []);
|
|
3497
|
+
|
|
4092
3498
|
useEffect(() => {
|
|
4093
3499
|
if (!isCloudMode || !CLOUD_API_URL || !CLOUD_API_KEY) {
|
|
4094
3500
|
setContentMode('cloud');
|
|
@@ -4378,11 +3784,16 @@ function App() {
|
|
|
4378
3784
|
siteConfig,
|
|
4379
3785
|
themeConfig,
|
|
4380
3786
|
menuConfig,
|
|
3787
|
+
refDocuments,
|
|
4381
3788
|
themeCss: { tenant: `${buildThemeFontVarsCss(themeConfig)}\n${tenantCss}` },
|
|
4382
3789
|
addSection: addSectionConfig,
|
|
3790
|
+
webmcp: {
|
|
3791
|
+
enabled: true,
|
|
3792
|
+
namespace: typeof window !== 'undefined' ? window.location.href : '',
|
|
3793
|
+
},
|
|
4383
3794
|
persistence: {
|
|
4384
3795
|
async saveToFile(state: ProjectState, slug: string): Promise<void> {
|
|
4385
|
-
// 💻 LOCAL FILESYSTEM (
|
|
3796
|
+
// 💻 LOCAL FILESYSTEM (development path)
|
|
4386
3797
|
console.log(`💻 Saving ${slug} to Local Filesystem...`);
|
|
4387
3798
|
const res = await fetch('/api/save-to-file', {
|
|
4388
3799
|
method: 'POST',
|
|
@@ -4508,8 +3919,30 @@ function App() {
|
|
|
4508
3919
|
|
|
4509
3920
|
const shouldRenderEngine = !isCloudMode || hasInitialCloudResolved;
|
|
4510
3921
|
|
|
3922
|
+
useEffect(() => {
|
|
3923
|
+
if (!shouldRenderEngine) {
|
|
3924
|
+
setTenantPreviewReady(false);
|
|
3925
|
+
return;
|
|
3926
|
+
}
|
|
3927
|
+
let cancelled = false;
|
|
3928
|
+
let raf1 = 0;
|
|
3929
|
+
let raf2 = 0;
|
|
3930
|
+
raf1 = window.requestAnimationFrame(() => {
|
|
3931
|
+
raf2 = window.requestAnimationFrame(() => {
|
|
3932
|
+
if (!cancelled) setTenantPreviewReady(true);
|
|
3933
|
+
});
|
|
3934
|
+
});
|
|
3935
|
+
return () => {
|
|
3936
|
+
cancelled = true;
|
|
3937
|
+
window.cancelAnimationFrame(raf1);
|
|
3938
|
+
window.cancelAnimationFrame(raf2);
|
|
3939
|
+
setTenantPreviewReady(false);
|
|
3940
|
+
};
|
|
3941
|
+
}, [shouldRenderEngine, pages, siteConfig]);
|
|
3942
|
+
|
|
4511
3943
|
return (
|
|
4512
|
-
|
|
3944
|
+
<ThemeProvider>
|
|
3945
|
+
<>
|
|
4513
3946
|
{isCloudMode && showTopProgress ? (
|
|
4514
3947
|
<>
|
|
4515
3948
|
<style>
|
|
@@ -4629,7 +4062,8 @@ function App() {
|
|
|
4629
4062
|
onClose={closeCloudDrawer}
|
|
4630
4063
|
onRetry={retryCloudSave}
|
|
4631
4064
|
/>
|
|
4632
|
-
|
|
4065
|
+
</>
|
|
4066
|
+
</ThemeProvider>
|
|
4633
4067
|
);
|
|
4634
4068
|
}
|
|
4635
4069
|
|
|
@@ -10671,46 +10105,9 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/site.json"
|
|
|
10671
10105
|
"data": {
|
|
10672
10106
|
"logoText": "Olon",
|
|
10673
10107
|
"badge": "",
|
|
10674
|
-
"links":
|
|
10675
|
-
|
|
10676
|
-
|
|
10677
|
-
"href": "/platform",
|
|
10678
|
-
"children": [
|
|
10679
|
-
{
|
|
10680
|
-
"label": "Overview",
|
|
10681
|
-
"href": "/platform/overview"
|
|
10682
|
-
},
|
|
10683
|
-
{
|
|
10684
|
-
"label": "Architecture",
|
|
10685
|
-
"href": "/platform/architecture"
|
|
10686
|
-
},
|
|
10687
|
-
{
|
|
10688
|
-
"label": "Security",
|
|
10689
|
-
"href": "/platform/security"
|
|
10690
|
-
},
|
|
10691
|
-
{
|
|
10692
|
-
"label": "Integrations",
|
|
10693
|
-
"href": "/platform/integrations"
|
|
10694
|
-
},
|
|
10695
|
-
{
|
|
10696
|
-
"label": "Roadmap",
|
|
10697
|
-
"href": "/platform/roadmap"
|
|
10698
|
-
}
|
|
10699
|
-
]
|
|
10700
|
-
},
|
|
10701
|
-
{
|
|
10702
|
-
"label": "Solutions",
|
|
10703
|
-
"href": "/solutions"
|
|
10704
|
-
},
|
|
10705
|
-
{
|
|
10706
|
-
"label": "Pricing",
|
|
10707
|
-
"href": "/pricing"
|
|
10708
|
-
},
|
|
10709
|
-
{
|
|
10710
|
-
"label": "Resources",
|
|
10711
|
-
"href": "/resources"
|
|
10712
|
-
}
|
|
10713
|
-
],
|
|
10108
|
+
"links": {
|
|
10109
|
+
"$ref": "../config/menu.json#/main"
|
|
10110
|
+
},
|
|
10714
10111
|
"ctaLabel": "Get started →",
|
|
10715
10112
|
"ctaHref": "#contact",
|
|
10716
10113
|
"signinHref": "#login"
|
|
@@ -10723,10 +10120,6 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/site.json"
|
|
|
10723
10120
|
"brandText": "Olon",
|
|
10724
10121
|
"copyright": "© 2025 OlonJS · v1.4 · Holon",
|
|
10725
10122
|
"links": [
|
|
10726
|
-
{
|
|
10727
|
-
"label": "Docs",
|
|
10728
|
-
"href": "/docs"
|
|
10729
|
-
},
|
|
10730
10123
|
{
|
|
10731
10124
|
"label": "GitHub",
|
|
10732
10125
|
"href": "#"
|
|
@@ -10925,27 +10318,25 @@ END_OF_FILE_CONTENT
|
|
|
10925
10318
|
mkdir -p "src/data/pages"
|
|
10926
10319
|
echo "Creating src/data/pages/design-system.json..."
|
|
10927
10320
|
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/design-system.json"
|
|
10928
|
-
{
|
|
10929
|
-
"id": "design-system-page",
|
|
10930
|
-
"slug": "design-system",
|
|
10931
|
-
"
|
|
10932
|
-
|
|
10933
|
-
"
|
|
10934
|
-
|
|
10935
|
-
|
|
10936
|
-
|
|
10937
|
-
|
|
10938
|
-
|
|
10939
|
-
"
|
|
10940
|
-
|
|
10941
|
-
|
|
10942
|
-
|
|
10943
|
-
|
|
10944
|
-
|
|
10945
|
-
|
|
10946
|
-
|
|
10947
|
-
}
|
|
10948
|
-
|
|
10321
|
+
{
|
|
10322
|
+
"id": "design-system-page",
|
|
10323
|
+
"slug": "design-system",
|
|
10324
|
+
"meta": {
|
|
10325
|
+
"title": "Olon Design System — Design Language",
|
|
10326
|
+
"description": "Token reference, color system, typography, components and brand identity for the OlonJS design language."
|
|
10327
|
+
},
|
|
10328
|
+
"sections": [
|
|
10329
|
+
{
|
|
10330
|
+
"id": "ds-main",
|
|
10331
|
+
"type": "design-system",
|
|
10332
|
+
"data": {
|
|
10333
|
+
"title": "Olon"
|
|
10334
|
+
},
|
|
10335
|
+
"settings": {}
|
|
10336
|
+
}
|
|
10337
|
+
],
|
|
10338
|
+
"global-header": false
|
|
10339
|
+
}
|
|
10949
10340
|
END_OF_FILE_CONTENT
|
|
10950
10341
|
# SKIP: src/data/pages/design-system.json:Zone.Identifier is binary and cannot be embedded as text.
|
|
10951
10342
|
echo "Creating src/data/pages/docs.json..."
|
|
@@ -10954,20 +10345,21 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/docs.json"
|
|
|
10954
10345
|
"id": "docs-page",
|
|
10955
10346
|
"slug": "docs",
|
|
10956
10347
|
"meta": {
|
|
10957
|
-
"title": "OlonJS Architecture Specifications v1.
|
|
10958
|
-
"description": "Mandatory Standard — Sovereign Core Edition.
|
|
10348
|
+
"title": "OlonJS Architecture Specifications v1.5",
|
|
10349
|
+
"description": "Mandatory Standard — Sovereign Core Edition. Canonical Studio actions, SSG, Save to file, and Hot Save."
|
|
10959
10350
|
},
|
|
10960
10351
|
"sections": [
|
|
10961
10352
|
{
|
|
10962
10353
|
"id": "docs-main",
|
|
10963
10354
|
"type": "tiptap",
|
|
10964
10355
|
"data": {
|
|
10965
|
-
"content": "# 📐 OlonJS Architecture Specifications v1.5s\n\n**Status:** Mandatory Standard\\\n**Version:** 1.5.0 (Sovereign Core Edition — Architecture + Studio/ICE UX, Path-Deterministic Nested Editing, Deterministic Local Design Tokens, Three-Layer CSS Bridge Contract)\\\n**Target:** Senior Architects / AI Agents / Enterprise Governance\n\n**Scope v1.5:** This edition preserves the complete v1.4 architecture (MTRP, JSP, TBP, CIP, ECIP, JAP + Studio/ICE UX contract: IDAC, TOCC, BSDS, ASC, JEB + Tenant Type & Code-Generation Annex + strict path-based/nested-array behavior) as a **faithful superset**, and upgrades **Local Design Tokens** from a principle to a deterministic implementation contract.\\\n⚠️ **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).\\\nℹ️ **Scope note (clarification):** In v1.5, `theme.json` is the tenant theme source of truth for themed tenants; runtime theme publication is mandatory for compliant themed tenants; section-local tokens (`--local-*`) are the required scoping layer for section-owned color and radius concerns.\n\n---\n\n## 1. 📐 Modular Type Registry Pattern (MTRP) v1.2\n\n**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.\n\n### 1.1 The Sovereign Dependency Inversion\n\nThe **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.\n\n### 1.2 Technical Implementation (`@olonjs/core/kernel`)\n\n```typescript\nexport interface SectionDataRegistry {} // Augmented by Tenant\nexport interface SectionSettingsRegistry {} // Augmented by Tenant\n\nexport interface BaseSection<K extends keyof SectionDataRegistry> {\n id: string;\n type: K;\n data: SectionDataRegistry[K];\n settings?: K extends keyof SectionSettingsRegistry\n ? SectionSettingsRegistry[K]\n : BaseSectionSettings;\n}\n\nexport type Section = {\n [K in keyof SectionDataRegistry]: BaseSection<K>\n}[keyof SectionDataRegistry];\n```\n\n**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.\n\n**Why ❔:** The Core must be able to render section without knowing the concrete types to compile-time; the Tenant must be able to add new types without modifying the Core. Empty registry + module augmentation allow you to deploy Core as an NPM package and keep type-safety end-to-end (Section, registry, config). Without MTRP, each new type would require changes in the Core or weak types (any).\n\n---\n\n## 2. 📐 JsonPages Site Protocol (JSP) v1.8\n\n**Objective:** Define the deterministic file system and the **Sovereign Projection Engine** (CLI).\n\n### 2.1 The File System Ontology (The Silo Contract)\n\nEvery site must reside in an isolated directory. Global Governance is physically separated from Local Content.\n\n- `/config/site.json` — Global Identity & Reserved System Blocks (Header/Footer). See Appendix A for typed shape.\n- `/config/menu.json` — Navigation Tree (SSOT for System Header). See Appendix A.\n- `/config/theme.json` — Theme tokens for themed tenants. See Appendix A.\n- `/pages/[slug].json` — Local Body Content per page. See Appendix A (PageConfig).\n\n**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.\n\n**Rule:** For a tenant that claims v1.4 design-token compliance, `theme.json` is not optional in practice. If a tenant omits a physical `theme.json`, it must still provide an equivalent `ThemeConfig` object before bootstrap; otherwise the tenant is outside full v1.4 theme compliance.\n\n### 2.2 Deterministic Projection (CLI Workflow)\n\nThe CLI (`@olonjs/cli`) creates new tenants by:\n\n1. **Infra Projection:** Generating `package.json`, `tsconfig.json`, and `vite.config.ts` (The Shell).\n2. **Source Projection:** Executing a deterministic script (`src_tenant_alpha.sh`) to reconstruct the `src` folder (The DNA).\n3. **Dependency Resolution:** Enforcing specific versions of React, Radix, and Tailwind v4.\n\n**Why they are needed:** A deterministic file structure (config vs pages) separates global governance (site, menu, theme) from content per page; CLI can regenerate tenants and tooling can find data and schematics always in the same paths. Without JSP, each tenant would be an ad hoc structure and ingestion/export/Bake would be fragile.\n\n---\n\n## 3. 🧱 Tenant Block Protocol (TBP) v1.0\n\n**Objective:** Standardize the \"Capsule\" structure for components to enable automated ingestion (Pull) by the SaaS.\n\n### 3.1 The Atomic Capsule Structure\n\nComponents are self-contained directories under `src/components/<sectionType>/`:\n\n- `View.tsx` — The pure React component (Dumb View). Props: see Appendix A (SectionComponentPropsMap).\n- `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).\n- `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.\n- `index.ts` — Public API: re-exports View, schema(s), and types.\n\n### 3.2 Reserved System Types\n\n- `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[] }`.\n- `type: 'footer'` — Reserved for `site.json`. Props: `{ data: FooterData; settings?: FooterSettings }` only (no `menu`).\n- `type: 'sectionHeader'` — A standard local block. Must define its own `links` array in its local schema if used.\n\n**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.\n\n---\n\n## 4. 🧱 Component Implementation Protocol (CIP) v1.6\n\n**Objective:** Ensure system-wide stability and Admin UI integrity.\n\n1. **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).\n2. **Z-Index Neutrality:** Components must not use `z-index > 1`. Layout delegation (sticky/fixed) is managed by the `SectionRenderer`.\n3. **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`).\n\n### 4.4 Local Design Tokens (v1.4)\n\n**Objective:** Standardize how a section consumes tenant theme values without leaking global styling assumptions into the section implementation.\n\n#### 4.4.1 The Required Four-Layer Chain\n\nFor any section that controls background, text color, border color, accent color, or radii, the following chain is normative:\n\n1. **Tenant theme source of truth** — Values are declared in `src/data/config/theme.json`.\n2. **Runtime theme publication** — The Core and/or tenant bootstrap **must** publish those values as CSS custom properties.\n3. **Section-local scope** — The View root **must** define `--local-*` variables mapped to the published theme variables for the concerns the section owns.\n4. **Rendered classes** — Section-owned color/radius utilities **must** consume `var(--local-*)`.\n\n**Rule:** A section may not skip layer 3 when it visually owns those concerns. Directly using global theme variables throughout the JSX is non-canonical for a fully themed section and must be treated as non-compliant unless the usage falls under an explicitly allowed exception.\n\n#### 4.4.2 Source Of Truth: `theme.json`\n\n`theme.json` is the tenant-level source of truth for theme values. Example:\n\n```json\n{\n \"name\": \"JsonPages Landing\",\n \"tokens\": {\n \"colors\": {\n \"primary\": \"#3b82f6\",\n \"secondary\": \"#22d3ee\",\n \"accent\": \"#60a5fa\",\n \"background\": \"#060d1b\",\n \"surface\": \"#0b1529\",\n \"surfaceAlt\": \"#101e38\",\n \"text\": \"#e2e8f0\",\n \"textMuted\": \"#94a3b8\",\n \"border\": \"#162a4d\"\n },\n \"typography\": {\n \"fontFamily\": {\n \"primary\": \"'Instrument Sans', system-ui, sans-serif\",\n \"mono\": \"'JetBrains Mono', monospace\",\n \"display\": \"'Bricolage Grotesque', system-ui, sans-serif\"\n }\n },\n \"borderRadius\": {\n \"sm\": \"0px\",\n \"md\": \"0px\",\n \"lg\": \"2px\"\n }\n }\n}\n```\n\n**Rule:** For a themed tenant, `theme.json` must contain the canonical semantic keys defined in Appendix A. Extra brand-specific keys are allowed only as extensions to those canonical groups, not as replacements for them.\n\n#### 4.4.3 Runtime Theme Publication\n\nThe tenant and/or Core **must** expose theme values as CSS variables before section rendering. The compliant bridge is a **three-layer chain** implemented in the tenant's `index.css`. Runtime publication is mandatory for themed tenants.\n\n##### Layer architecture\n\n```\ntheme.json → engine injection → :root bridge → @theme (Tailwind) → JSX classes\n```\n\n**Layer 0 — Engine injection (Core-provided)** `@olonjs/core` reads `theme.json` and injects all token values as flattened CSS custom properties before section rendering. The naming convention is:\n\nJSON path Injected CSS var `tokens.colors.{name}` `--theme-colors-{name}` `tokens.typography.fontFamily.{role}` `--theme-font-{role}` `tokens.typography.scale.{step}` `--theme-typography-scale-{step}` `tokens.typography.tracking.{name}` `--theme-typography-tracking-{name}` `tokens.typography.leading.{name}` `--theme-typography-leading-{name}` `tokens.typography.wordmark.*` `--theme-typography-wordmark-*` `tokens.borderRadius.{name}` `--theme-border-radius-{name}` `tokens.spacing.{name}` `--theme-spacing-{name}` `tokens.zIndex.{name}` `--theme-z-index-{name}` `tokens.modes.{mode}.colors.{name}` `--theme-modes-{mode}-colors-{name}`\n\nThe engine also publishes shorthand aliases for the most common radius and font tokens (e.g. `--theme-radius-sm`, `--theme-font-primary`). Tokens not covered by the shorthand aliases must be bridged in the tenant `:root`.\n\n**Layer 1 —** `:root` **semantic bridge (Tenant-provided,** `index.css`**)** The tenant maps engine-injected vars to its own semantic naming. **The naming in this layer is the tenant's sovereign choice** — it is not imposed by the Core. Any naming convention is valid as long as it is consistent throughout the tenant.\n\n```css\n:root {\n /* Backgrounds */\n --background: var(--theme-colors-background);\n --card: var(--theme-colors-card);\n --elevated: var(--theme-colors-elevated);\n --overlay: var(--theme-colors-overlay);\n --popover: var(--theme-colors-popover);\n --popover-foreground: var(--theme-colors-popover-foreground);\n\n /* Foregrounds */\n --foreground: var(--theme-colors-foreground);\n --card-foreground: var(--theme-colors-card-foreground);\n --muted-foreground: var(--theme-colors-muted-foreground);\n --placeholder: var(--theme-colors-placeholder);\n\n /* Brand ramp */\n --primary: var(--theme-colors-primary);\n --primary-foreground: var(--theme-colors-primary-foreground);\n --primary-light: var(--theme-colors-primary-light);\n --primary-dark: var(--theme-colors-primary-dark);\n /* ... full ramp --primary-50 through --primary-900 ... */\n\n /* Accent, secondary, muted, border, input, ring */\n --accent: var(--theme-colors-accent);\n --accent-foreground: var(--theme-colors-accent-foreground);\n --secondary: var(--theme-colors-secondary);\n --secondary-foreground: var(--theme-colors-secondary-foreground);\n --muted: var(--theme-colors-muted);\n --border: var(--theme-colors-border);\n --border-strong: var(--theme-colors-border-strong);\n --input: var(--theme-colors-input);\n --ring: var(--theme-colors-ring);\n\n /* Feedback */\n --destructive: var(--theme-colors-destructive);\n --destructive-foreground: var(--theme-colors-destructive-foreground);\n --success: var(--theme-colors-success);\n --success-foreground: var(--theme-colors-success-foreground);\n --warning: var(--theme-colors-warning);\n --warning-foreground: var(--theme-colors-warning-foreground);\n --info: var(--theme-colors-info);\n --info-foreground: var(--theme-colors-info-foreground);\n\n /* Typography scale, tracking, leading */\n --theme-text-xs: var(--theme-typography-scale-xs);\n --theme-text-sm: var(--theme-typography-scale-sm);\n /* ... full scale ... */\n --theme-tracking-tight: var(--theme-typography-tracking-tight);\n --theme-leading-normal: var(--theme-typography-leading-normal);\n /* ... */\n\n /* Spacing */\n --theme-container-max: var(--theme-spacing-container-max);\n --theme-section-y: var(--theme-spacing-section-y);\n --theme-header-h: var(--theme-spacing-header-h);\n --theme-sidebar-w: var(--theme-spacing-sidebar-w);\n\n /* Z-index */\n --z-base: var(--theme-z-index-base);\n --z-elevated: var(--theme-z-index-elevated);\n --z-dropdown: var(--theme-z-index-dropdown);\n --z-sticky: var(--theme-z-index-sticky);\n --z-overlay: var(--theme-z-index-overlay);\n --z-modal: var(--theme-z-index-modal);\n --z-toast: var(--theme-z-index-toast);\n}\n```\n\n**Layer 2 —** `@theme` **Tailwind v4 bridge (Tenant-provided,** `index.css`**)** Every semantic variable from Layer 1 is re-exposed under the Tailwind v4 `@theme` namespace so it becomes a utility class. Pattern: `--color-{slug}: var(--{slug})`.\n\n```css\n@theme {\n --color-background: var(--background);\n --color-card: var(--card);\n --color-foreground: var(--foreground);\n --color-primary: var(--primary);\n --color-accent: var(--accent);\n --color-border: var(--border);\n /* ... full token set ... */\n\n --font-primary: var(--theme-font-primary);\n --font-mono: var(--theme-font-mono);\n --font-display: var(--theme-font-display);\n\n --radius-sm: var(--theme-radius-sm);\n --radius-md: var(--theme-radius-md);\n --radius-lg: var(--theme-radius-lg);\n --radius-xl: var(--theme-radius-xl);\n --radius-full: var(--theme-radius-full);\n}\n```\n\nAfter this bridge, the full Tailwind utility vocabulary (`bg-primary`, `text-foreground`, `rounded-lg`, `font-display`, etc.) resolves to live theme values — with no hardcoded hex anywhere in the React layer.\n\n**Light mode / additional modes** are bridged by overriding the Layer 1 semantic vars under a `[data-theme=\"light\"]` selector (or equivalent), pointing to the engine-injected mode vars (`--theme-modes-light-colors-*`). The `@theme` layer requires no changes.\n\n**Rule:** A tenant `index.css` must implement all three layers. Skipping Layer 2 breaks Tailwind utility resolution. Skipping Layer 1 couples sections to engine-internal naming. Hardcoding values in either layer is non-compliant.\n\n#### 4.4.4 Section-Local Scope\n\nIf a section controls its own visual language, it **shall** establish a local token scope on the section root. Example:\n\n```tsx\n<section\n style={{\n '--local-bg': 'var(--background)',\n '--local-text': 'var(--foreground)',\n '--local-text-muted': 'var(--muted-foreground)',\n '--local-primary': 'var(--primary)',\n '--local-border': 'var(--border)',\n '--local-surface': 'var(--card)',\n '--local-radius-sm': 'var(--theme-radius-sm)',\n '--local-radius-md': 'var(--theme-radius-md)',\n '--local-radius-lg': 'var(--theme-radius-lg)',\n } as React.CSSProperties}\n>\n```\n\n**Rule:** `--local-*` values must map to published theme variables. They must **not** be defined as hardcoded brand values such as `#fff`, `#111827`, `12px`, or `Inter, sans-serif` if those values belong to the tenant theme layer.\n\n**Rule:** Local tokens are **mandatory** for section-owned color and radius concerns. They are **optional** for font-family concerns unless the section must remap or isolate font roles locally.\n\n#### 4.4.5 Canonical Typography Rule\n\nTypography follows a deterministic rule distinct from color/radius:\n\n1. **Canonical font publication** — Tenant/Core must publish semantic font variables such as `--theme-font-primary`, `--theme-font-mono`, and `--theme-font-display` when those roles exist in the theme.\n2. **Canonical font consumption** — Sections must consume typography through semantic tenant font utilities or variables backed by those published theme roles (for example `.font-display` backed by `--font-display`, itself backed by `--theme-font-display`).\n3. **Local font tokens** — `--local-font-*` is optional and should be used only when a section needs to remap a font role locally rather than simply consume the canonical tenant font role.\n\nExample of canonical global semantic bridge:\n\n```css\n:root {\n --font-primary: var(--theme-font-primary);\n --font-display: var(--theme-font-display);\n}\n\n.font-display {\n font-family: var(--font-display, var(--font-primary));\n}\n```\n\n**Rule:** A section is compliant if it consumes themed fonts through this published semantic chain. It is **not** required to define `--local-font-display` unless the section needs local remapping. This closes the ambiguity between global semantic typography utilities and local color/radius scoping.\n\n#### 4.4.6 View Consumption\n\nAll section-owned classes that affect color or radius must consume local variables. Font consumption must follow the typography rule above. Example:\n\n```tsx\n<section\n style={{\n '--local-bg': 'var(--background)',\n '--local-text': 'var(--foreground)',\n '--local-primary': 'var(--primary)',\n '--local-border': 'var(--border)',\n '--local-radius-md': 'var(--theme-radius-md)',\n '--local-radius-lg': 'var(--theme-radius-lg)',\n } as React.CSSProperties}\n className=\"bg-[var(--local-bg)]\"\n>\n <h1 className=\"font-display text-[var(--local-text)]\">Build Tenant DNA</h1>\n\n <a className=\"bg-[var(--local-primary)] rounded-[var(--local-radius-md)] text-white\">\n Read the Docs\n </a>\n\n <div className=\"border border-[var(--local-border)] rounded-[var(--local-radius-lg)]\">\n {/* illustration / mockup / card */}\n </div>\n</section>\n```\n\n#### 4.4.7 Compliance Rules\n\nA section is compliant when all of the following are true:\n\n1. `theme.json` is the source of truth for the theme values being used.\n2. Those values are published at runtime as CSS custom properties before the section renders.\n3. The section root defines a local token scope for the color/radius concerns it controls.\n4. Local color/radius tokens map to published theme variables rather than hardcoded literals.\n5. JSX classes use `var(--local-*)` for section-owned color/radius concerns.\n6. Fonts are consumed through the published semantic font chain, and only use local font tokens when local remapping is required.\n7. Hardcoded colors/radii are absent from the primary visual contract of the section.\n\n#### 4.4.8 Allowed Exceptions\n\nThe following are acceptable if documented and intentionally limited:\n\n- Tiny decorative one-off values that are not part of the tenant theme contract (for example an isolated translucent pixel-grid overlay).\n- Temporary compatibility shims during migration, provided the section still exposes a clear compliant path and the literal is not the primary themed value.\n- Semantic alias bridges in tenant CSS (for example `--font-display: var(--theme-font-display)`), as long as the source remains the theme layer.\n\n#### 4.4.9 Non-Compliant Patterns\n\nThe following are non-compliant:\n\n- `style={{ '--local-bg': '#060d1b' }}` when that background belongs to tenant theme.\n- Buttons using `rounded-[7px]`, `bg-blue-500`, `text-zinc-100`, or similar hardcoded utilities inside a section that claims to be theme-driven.\n- A section root that defines `--local-*`, but child elements still use raw `bg-*`, `text-*`, or `rounded-*` utilities for the same owned concerns.\n- Reading `theme.json` directly inside a View instead of consuming published runtime theme variables.\n- Treating brand-specific extension keys as a replacement for canonical semantic keys such as `primary`, `background`, `text`, `border`, or `fontFamily.primary`.\n\n#### 4.4.10 Practical Interpretation\n\n`--local-*` is not the source of truth. It is the **local scoping layer** between tenant theme and section implementation.\n\nCanonical chain:\n\n`theme.json` → published runtime theme vars → section `--local-*` → JSX classes\\`\n\nCanonical font chain:\n\n`theme.json` → published semantic font vars → tenant font utility/variable → section typography\\`\n\n### 4.5 Z-Index & Overlay Governance (v1.2)\n\nSection 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.\n\n**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). In v1.4 la catena `theme.json -> runtime vars -> --local-* -> JSX classes` rende i tenant temabili, riproducibili e compatibili con la Studio UX; senza questa separazione, stili “nudi” o valori hardcoded creano drift visivo, rompono il contratto del brand, e rendono ambiguo ciò che appartiene al tema contro ciò che appartiene alla section.\n\n---\n\n## 5. 🛠️ Editor Component Implementation Protocol (ECIP) v1.5\n\n**Objective:** Standardize the Polymorphic ICE engine.\n\n1. **Recursive Form Factory:** The Admin UI builds forms by traversing the Zod ontology.\n2. **UI Metadata:** Use `.describe('ui:[widget]')` in schemas to pass instructions to the Form Factory.\n3. **Deterministic IDs:** Every object in a `ZodArray` must extend `BaseArrayItem` (containing an `id`) to ensure React reconciliation stability during reordering.\n\n### 5.4 UI Metadata Vocabulary (v1.2)\n\nStandard keys for the Form Factory:\n\nKey Use case `ui:text` Single-line text input. `ui:textarea` Multi-line text. `ui:select` Enum / single choice. `ui:number` Numeric input. `ui:list` Array of items; list editor (add/remove/reorder). `ui:icon-picker` Icon selection.\n\nUnknown keys may be treated as `ui:text`. Array fields must use `BaseArrayItem` for items.\n\n### 5.5 Path-Only Nested Selection & Expansion (v1.3, breaking)\n\nIn strict v1.3 Studio/Inspector behavior, nested editing targets are represented by **path segments from root to leaf**.\n\n```typescript\nexport type SelectionPathSegment = { fieldKey: string; itemId?: string };\nexport type SelectionPath = SelectionPathSegment[];\n```\n\nRules:\n\n- Expansion and focus for nested arrays **must** be computed from `SelectionPath` (root → leaf), not from a single flat pair.\n- Matching by `fieldKey` alone is non-compliant for nested structures.\n- Legacy flat payload fields `itemField` and `itemId` are removed in strict v1.3 selection protocol.\n\n**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.\n\n---\n\n## 6. 🎯 ICE Data Attribute Contract (IDAC) v1.1\n\n**Objective:** Mandatory data attributes so the Stage (iframe) and Inspector can bind selection and field/item editing without coupling to Tenant DOM.\n\n### 6.1 Section-Level Markup (Core-Provided)\n\n**SectionRenderer** (Core) wraps each section root with:\n\n- `data-section-id` — Section instance ID (e.g. UUID). On the wrapper that contains content + overlay.\n- Sibling overlay element `data-jp-section-overlay` — Selection ring and type label. **Tenant does not add this;** Core injects it.\n\nTenant Views render the **content** root only (e.g. `<section>` or `<div>`), placed **inside** the Core wrapper.\n\n### 6.2 Field-Level Binding (Tenant-Provided)\n\nFor every **editable scalar field** the View **must** attach `data-jp-field=\"<fieldKey>\"` (key matches schema path: e.g. `title`, `description`, `sectionTitle`, `label`).\n\n### 6.3 Array-Item Binding (Tenant-Provided)\n\nFor every **editable array item** the View **must** attach:\n\n- `data-jp-item-id=\"<stableId>\"` — Prefer `item.id`; fallback e.g. `legacy-${index}` only outside strict mode.\n- `data-jp-item-field=\"<arrayKey>\"` — e.g. `cards`, `layers`, `products`, `paragraphs`.\n\n### 6.4 Compliance\n\n**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.\n\n### 6.5 Strict Path Extraction for Nested Arrays (v1.3, breaking)\n\nFor nested array targets, the Core/Inspector contract is path-based:\n\n- The runtime selection target is expressed as `itemPath: SelectionPath` (root → leaf).\n- Flat identity (`itemField` + `itemId`) is not sufficient for nested structures and is removed in strict v1.3 payloads.\n- In strict mode, index-based identity fallback is non-compliant for editable object arrays.\n\n**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.\n\n---\n\n## 7. 🎨 Tenant Overlay CSS Contract (TOCC) v1.0\n\n**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.\n\n### 7.1 Required Selectors (Tenant global CSS)\n\n1. `[data-jp-section-overlay]` — `position: absolute; inset: 0`; `pointer-events: none`; base state transparent.\n2. `[data-section-id]:hover [data-jp-section-overlay]` — Hover: e.g. dashed border, subtle tint.\n3. `[data-section-id][data-jp-selected] [data-jp-section-overlay]` — Selected: solid border, optional tint.\n4. `[data-jp-section-overlay] > div` (type label) — Position and visibility (e.g. visible on hover/selected).\n\n### 7.2 Z-Index\n\nOverlay **z-index** high (e.g. 9999). Section content at or below CIP limit (§4.5).\n\n### 7.3 Responsibility\n\n**Core:** Injects wrapper and overlay DOM; sets `data-jp-selected`. **Tenant:** All overlay **visual** rules.\n\n**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.\n\n---\n\n## 8. 📦 Base Section Data & Settings (BSDS) v1.0\n\n**Objective:** Standardize base schema fragments for anchors, array items, and section settings.\n\n### 8.1 BaseSectionData\n\nEvery section data schema **must** extend a base with at least `anchorId` (optional string). Canonical Zod (Tenant `lib/base-schemas.ts` or equivalent):\n\n```typescript\nexport const BaseSectionData = z.object({\n anchorId: z.string().optional().describe('ui:text'),\n});\n```\n\n### 8.2 BaseArrayItem\n\nEvery array item schema editable in the Inspector **must** include `id` (optional string minimum). Canonical Zod:\n\n```typescript\nexport const BaseArrayItem = z.object({\n id: z.string().optional(),\n});\n```\n\nRecommended: required UUID for new items. Used by `data-jp-item-id` and React reconciliation.\n\n### 8.3 BaseSectionSettings (Optional)\n\nCommon section-level settings. Canonical Zod (name **BaseSectionSettingsSchema** or as exported by Core):\n\n```typescript\nexport const BaseSectionSettingsSchema = z.object({\n paddingTop: z.enum(['none', 'sm', 'md', 'lg', 'xl', '2xl']).default('md').describe('ui:select'),\n paddingBottom: z.enum(['none', 'sm', 'md', 'lg', 'xl', '2xl']).default('md').describe('ui:select'),\n theme: z.enum(['dark', 'light', 'accent']).default('dark').describe('ui:select'),\n container: z.enum(['boxed', 'fluid']).default('boxed').describe('ui:select'),\n});\n```\n\nCapsules may extend this for type-specific settings. Core may export **BaseSectionSettings** as the TypeScript type inferred from this or a superset.\n\n**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.\n\n---\n\n## 9. 📌 AddSectionConfig (ASC) v1.0\n\n**Objective:** Formalize the \"Add Section\" contract used by the Studio.\n\n**Type (Core exports** `AddSectionConfig`**):**\n\n```typescript\ninterface AddSectionConfig {\n addableSectionTypes: readonly string[];\n sectionTypeLabels: Record<string, string>;\n getDefaultSectionData(sectionType: string): Record<string, unknown>;\n}\n```\n\n**Shape:** Tenant provides one object (e.g. `addSectionConfig`) with:\n\n- `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.\n- `sectionTypeLabels` — Map type key → display string (e.g. `{ hero: 'Hero', 'cta-banner': 'CTA Banner' }`).\n- `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.\n\nCore creates a new section with deterministic UUID, `type`, and `data` from `getDefaultSectionData(type)`.\n\n**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.\n\n---\n\n## 10. ⚙️ JsonPagesConfig & Engine Bootstrap (JEB) v1.1\n\n**Objective:** Bootstrap contract between Tenant app and `@olonjs/core`.\n\n### 10.1 JsonPagesConfig (required fields)\n\nThe Tenant passes a single **config** object to **JsonPagesEngine**. Required fields:\n\nField Type Description **tenantId** string Passed to `resolveAssetUrl(path, tenantId)`; resolved asset URLs are `/assets/...` with no tenantId segment in the path. **registry** `{ [K in SectionType]: React.FC<SectionComponentPropsMap[K]> }` Component registry. Must match MTRP keys. See Appendix A. **schemas** `Record<SectionType, ZodType>` or equivalent SECTION_SCHEMAS: type → **data** Zod schema. Form Factory uses this. See Appendix A. **pages** `Record<string, PageConfig>` Slug → page config. See Appendix A. **siteConfig** SiteConfig Global site (identity, header/footer blocks). See Appendix A. **themeConfig** ThemeConfig Theme tokens. See Appendix A. **menuConfig** MenuConfig Navigation tree (SSOT for header menu). See Appendix A. **themeCss** `{ tenant: string }` At least **tenant**: string (inline CSS or URL) for Stage iframe injection. **addSection** AddSectionConfig Add-section config (§9).\n\nCore may define optional fields. The Tenant must not omit required fields.\n\n### 10.2 JsonPagesEngine\n\nRoot 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.\n\n### 10.3 Studio Selection Event Contract (v1.3, breaking)\n\nIn strict v1.3 Studio, section selection payload for nested targets is path-based:\n\n```typescript\ntype SectionSelectMessage = {\n type: 'SECTION_SELECT';\n section: { id: string; type: string; scope: 'global' | 'local' };\n itemPath?: SelectionPath; // root -> leaf\n};\n```\n\nRemoved from strict protocol:\n\n- `itemField`\n- `itemId`\n\n**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.\n\n---\n\n# 🏛️ OlonJS_ADMIN_PROTOCOL (JAP) v1.2\n\n**Status:** Mandatory Standard\\\n**Version:** 1.2.0 (Sovereign Shell Edition — Path/Nested Strictness)\\\n**Objective:** Deterministic orchestration of the \"Studio\" environment (ICE Level 1).\n\n---\n\n## 1. The Sovereign Shell Topology\n\nThe Admin interface is a **Sovereign Shell** from `@olonjs/core`.\n\n1. **The Stage (Canvas):** Isolated Iframe; postMessage for data updates and selection mirroring. Section markup follows **IDAC** (§6); overlay styling follows **TOCC** (§7).\n2. **The Inspector (Sidebar):** Consumes Tenant Zod schemas to generate editors; binding via `data-jp-field` and `data-jp-item-*`.\n3. **The Control Bar:** Save, Export, Add Section.\n\n## 2. State Orchestration & Persistence\n\n- **Working Draft:** Reactive local state for unsaved changes.\n- **Sync Law:** Inspector changes → Working Draft → Stage via `STUDIO_EVENTS.UPDATE_DRAFTS`.\n- **Bake Protocol:** \"Bake HTML\" requests snapshot from Iframe, injects `ProjectState` as JSON, triggers download.\n\n## 3. Context Switching (Global vs. Local)\n\n- **Header/Footer** selection → Global Mode, `site.json`.\n- Any other section → Page Mode, current `[slug].json`.\n\n## 4. Section Lifecycle Management\n\n1. **Add Section:** Modal from Tenant `SECTION_SCHEMAS`; UUID + default data via **AddSectionConfig** (§9).\n2. **Reorder:** Inspector or Stage Overlay; array mutation in Working Draft.\n3. **Delete:** Confirmation; remove from array, clear selection.\n\n## 5. Stage Isolation & Overlay\n\n- **CSS Shielding:** Stage in Iframe; Tenant CSS does not leak into Admin.\n- **Sovereign Overlay:** Selection ring and type labels injected per **IDAC** (§6); Tenant styles them per **TOCC** (§7).\n\n## 6. \"Green Build\" Validation\n\nStudio enforces `tsc && vite build`. No export with TypeScript errors.\n\n## 7. Path-Deterministic Selection & Sidebar Expansion (v1.3, breaking)\n\n- Section/item focus synchronization uses `itemPath` (root → leaf), not flat `itemField/itemId`.\n- Sidebar expansion state for nested arrays must be derived from all path segments.\n- Flat-only matching may open/close wrong branches and is non-compliant in strict mode.\n\n**Perché servono (JAP):** Stage in iframe + Inspector + Control Bar separano il contesto di editing dal sito; postMessage e Working Draft permettono modifiche senza toccare subito i file. Bake ed Export 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 ciò che si esporta compili. In v1.3, il path completo elimina ambiguità nella sincronizzazione Stage↔Sidebar su strutture annidate.\n\n---\n\n## Compliance: Legacy vs Full UX (v1.4)\n\nDimension Legacy / Less UX Full UX (Core-aligned) **ICE binding** No `data-jp-*`; Inspector cannot bind. IDAC (§6) on every editable section/field/item. **Section wrapper** Plain `<section>`; no overlay contract. Core wrapper + overlay; Tenant CSS per TOCC (§7). **Design tokens** Raw BEM / fixed classes, or local vars fed by literals. `theme.json` as source of truth, mandatory runtime publication, local color/radius scope via `--local-*`, typography via canonical semantic font chain, no primary hardcoded themed values. **Base schemas** Ad hoc. BSDS (§8): BaseSectionData, BaseArrayItem, BaseSectionSettings. **Add Section** Ad hoc defaults. ASC (§9): addableSectionTypes, labels, getDefaultSectionData. **Bootstrap** Implicit. JEB (§10): JsonPagesConfig + JsonPagesEngine. **Selection payload** Flat `itemField/itemId`. Path-only `itemPath: SelectionPath` (JEB §10.3). **Nested array expansion** Single-segment or field-only heuristics. Root-to-leaf path expansion (ECIP §5.5, JAP §7). **Array item identity (strict)** Index fallback tolerated. Stable `id` required for editable object arrays.\n\n**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.\n\n---\n\n## Summary of v1.5 Additions\n\n§ Title Purpose 4.4.3 Three-Layer CSS Bridge Replaces the informal \"publish CSS vars\" rule with the deterministic Layer 0 (engine injection) → Layer 1 (`:root` semantic bridge) → Layer 2 (`@theme` Tailwind bridge) architecture. Documents the engine's `--theme-colors-{name}` naming convention and the tenant's sovereign naming freedom in Layer 1. A.2.6 ThemeConfig (v1.5) Replaces the incorrect `surface/surfaceAlt/text/textMuted` canonical keys with the actual schema-aligned keys (`card`, `elevated`, `foreground`, `muted-foreground`, etc.). Adds `spacing`, `zIndex`, full typography sub-interfaces (`scale`, `tracking`, `leading`, `wordmark`), and `modes`. Establishes `theme.json` as SOT with schema as the formalisation layer.\n\n---\n\n## Summary of v1.4 Additions\n\n§ Title Purpose 4.4 Local Design Tokens Makes the `theme.json -> runtime vars -> --local-* -> JSX classes` chain explicit and normative. 4.4.3 Runtime Theme Publication Makes runtime CSS publication mandatory for themed tenants. 4.4.5 Canonical Typography Rule Removes ambiguity between global semantic font utilities and local token scoping. 4.4.7 Compliance Rules Turns Local Design Tokens into a checklist-grade compliance contract. 4.4.9 Non-Compliant Patterns Makes hardcoded token anti-patterns explicit. **Appendix A.2.6** **Deterministic ThemeConfig** Aligns the spec-level theme contract with the core’s structured semantic keys plus extension policy. **Appendix A.7** **Local Design Tokens Implementation Addendum** Operational checklist and implementation examples for compliant tenant sections.\n\n---\n\n# Appendix A — Tenant Type & Code-Generation Annex\n\n**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.\n\n**Status:** Mandatory for code-generation and governance. Compliance ensures generated tenants are typed and wired like the reference implementation.\n\n---\n\n## A.1 Core-Provided Types (from `@olonjs/core`)\n\nThe following are assumed to be exported by Core. The Tenant augments **SectionDataRegistry** and **SectionSettingsRegistry**; all other types are consumed as-is.\n\nType Description **SectionType** `keyof SectionDataRegistry` (after Tenant augmentation). Union of all section type keys. **Section** Union of `BaseSection<K>` for all K in SectionDataRegistry. See MTRP §1.2. **BaseSectionSettings** Optional base type for section settings (may align with BSDS §8.3). **MenuItem** Navigation item. **Minimum shape:** `{ label: string; href: string }`. Core may extend (e.g. `children?: MenuItem[]`). **AddSectionConfig** See §9. **JsonPagesConfig** See §10.1.\n\n**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.\n\n---\n\n## A.2 Tenant-Provided Types (single source: `src/types.ts` or equivalent)\n\nThe 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.\n\n### A.2.1 SectionComponentPropsMap\n\nMaps each section type to the props of its React component. **Header** is the only type that receives **menu**.\n\n**Option A — Explicit (recommended for clarity and tooling):** For each section type K, add one entry. Header receives **menu**.\n\n```typescript\nimport type { MenuItem } from '@olonjs/core';\n// Import Data/Settings from each capsule.\n\nexport type SectionComponentPropsMap = {\n 'header': { data: HeaderData; settings?: HeaderSettings; menu: MenuItem[] };\n 'footer': { data: FooterData; settings?: FooterSettings };\n 'hero': { data: HeroData; settings?: HeroSettings };\n // ... one entry per SectionType, e.g. 'feature-grid', 'cta-banner', etc.\n};\n```\n\n**Option B — Mapped type (DRY, requires SectionDataRegistry/SectionSettingsRegistry in scope):**\n\n```typescript\nimport type { MenuItem } from '@olonjs/core';\n\nexport type SectionComponentPropsMap = {\n [K in SectionType]: K extends 'header'\n ? { data: SectionDataRegistry[K]; settings?: SectionSettingsRegistry[K]; menu: MenuItem[] }\n : { data: SectionDataRegistry[K]; settings?: K extends keyof SectionSettingsRegistry ? SectionSettingsRegistry[K] : BaseSectionSettings };\n};\n```\n\nSectionType 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.\n\n**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.\n\n### A.2.2 ComponentRegistry type\n\nThe registry object **must** be typed as:\n\n```typescript\nimport type { SectionType } from '@olonjs/core';\nimport type { SectionComponentPropsMap } from '@/types';\n\nexport const ComponentRegistry: {\n [K in SectionType]: React.FC<SectionComponentPropsMap[K]>;\n} = { /* ... */ };\n```\n\nFile: `src/lib/ComponentRegistry.tsx` (or equivalent). Imports one View per section type and assigns it to the corresponding key.\n\n### A.2.3 PageConfig\n\nMinimum shape for a single page (used in **pages** and in each `[slug].json`):\n\n```typescript\nexport interface PageConfig {\n id?: string;\n slug: string;\n meta?: {\n title?: string;\n description?: string;\n };\n sections: Section[];\n}\n```\n\n**Section** is the union type from MTRP (§1.2). Each element of **sections** has **id**, **type**, **data**, **settings** and conforms to the capsule schemas.\n\n### A.2.4 SiteConfig\n\nMinimum shape for **site.json** (and for **siteConfig** in JsonPagesConfig):\n\n```typescript\nexport interface SiteConfigIdentity {\n title?: string;\n logoUrl?: string;\n}\n\nexport interface SiteConfig {\n identity?: SiteConfigIdentity;\n pages?: Array<{ slug: string; label: string }>;\n header: {\n id: string;\n type: 'header';\n data: HeaderData;\n settings?: HeaderSettings;\n };\n footer: {\n id: string;\n type: 'footer';\n data: FooterData;\n settings?: FooterSettings;\n };\n}\n```\n\n**HeaderData**, **FooterData**, **HeaderSettings**, **FooterSettings** are the types exported from the header and footer capsules.\n\n### A.2.5 MenuConfig\n\nMinimum 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\\[\\]**.\n\n```typescript\nexport interface MenuConfig {\n main?: MenuItem[];\n [key: string]: MenuItem[] | undefined;\n}\n```\n\nOr 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).\n\n### A.2.6 ThemeConfig\n\nMinimum shape for **theme.json** (and for **themeConfig** in JsonPagesConfig). `theme.json` is the **source of truth** for the entire visual contract of the tenant. The schema (`design-system.schema.json`) is the machine-readable formalisation of this contract — if the TypeScript interfaces and the JSON Schema diverge, the JSON Schema wins.\n\n**Naming policy:** The keys within `tokens.colors` are the tenant's sovereign choice. The engine flattens all keys to `--theme-colors-{name}` regardless of naming convention. The required keys listed below are the ones the engine's `:root` bridge and the `@theme` Tailwind bridge must be able to resolve. Extra brand-specific keys are always allowed as additive extensions.\n\n```typescript\nexport interface ThemeColors {\n /* Required — backgrounds */\n background: string;\n card: string;\n elevated: string;\n overlay: string;\n popover: string;\n 'popover-foreground': string;\n\n /* Required — foregrounds */\n foreground: string;\n 'card-foreground': string;\n 'muted-foreground': string;\n placeholder: string;\n\n /* Required — brand */\n primary: string;\n 'primary-foreground': string;\n 'primary-light': string;\n 'primary-dark': string;\n\n /* Optional — brand ramp (50–900) */\n 'primary-50'?: string;\n 'primary-100'?: string;\n 'primary-200'?: string;\n 'primary-300'?: string;\n 'primary-400'?: string;\n 'primary-500'?: string;\n 'primary-600'?: string;\n 'primary-700'?: string;\n 'primary-800'?: string;\n 'primary-900'?: string;\n\n /* Required — accent, secondary, muted */\n accent: string;\n 'accent-foreground': string;\n secondary: string;\n 'secondary-foreground': string;\n muted: string;\n\n /* Required — border, form */\n border: string;\n 'border-strong': string;\n input: string;\n ring: string;\n\n /* Required — feedback */\n destructive: string;\n 'destructive-foreground': string;\n 'destructive-border': string;\n 'destructive-ring': string;\n success: string;\n 'success-foreground': string;\n 'success-border': string;\n 'success-indicator': string;\n warning: string;\n 'warning-foreground': string;\n 'warning-border': string;\n info: string;\n 'info-foreground': string;\n 'info-border': string;\n\n [key: string]: string | undefined;\n}\n\nexport interface ThemeFontFamily {\n primary: string;\n mono: string;\n display?: string;\n [key: string]: string | undefined;\n}\n\nexport interface ThemeWordmark {\n fontFamily: string;\n weight: string;\n width: string;\n}\n\nexport interface ThemeTypography {\n fontFamily: ThemeFontFamily;\n wordmark?: ThemeWordmark;\n scale?: Record<string, string>; /* xs sm base md lg xl 2xl 3xl 4xl 5xl 6xl 7xl */\n tracking?: Record<string, string>; /* tight display normal wide label */\n leading?: Record<string, string>; /* none tight snug normal relaxed */\n}\n\nexport interface ThemeBorderRadius {\n sm: string;\n md: string;\n lg: string;\n xl?: string;\n full?: string;\n [key: string]: string | undefined;\n}\n\nexport interface ThemeSpacing {\n 'container-max'?: string;\n 'section-y'?: string;\n 'header-h'?: string;\n 'sidebar-w'?: string;\n [key: string]: string | undefined;\n}\n\nexport interface ThemeZIndex {\n base?: string;\n elevated?: string;\n dropdown?: string;\n sticky?: string;\n overlay?: string;\n modal?: string;\n toast?: string;\n [key: string]: string | undefined;\n}\n\nexport interface ThemeModes {\n [mode: string]: { colors: Partial<ThemeColors> };\n}\n\nexport interface ThemeTokens {\n colors: ThemeColors;\n typography: ThemeTypography;\n borderRadius: ThemeBorderRadius;\n spacing?: ThemeSpacing;\n zIndex?: ThemeZIndex;\n modes?: ThemeModes;\n}\n\nexport interface ThemeConfig {\n name: string;\n tokens: ThemeTokens;\n}\n```\n\n**Rule:** `theme.json` is the single source of truth. All layers downstream (engine injection, `:root` bridge, `@theme` bridge, React JSX) are read-only consumers. No layer below `theme.json` may hardcode a value that belongs to the theme contract.\n\n**Rule:** Brand-specific extension keys (e.g. `colors.primary-50` through `primary-900`, custom spacing tokens) are always allowed as additive extensions within the canonical groups. They must not replace the required semantic keys.\n\n---\n\n## A.3 Schema Contract (SECTION_SCHEMAS)\n\n**Location:** `src/lib/schemas.ts` (or equivalent).\n\n**Contract:**\n\n- **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).\n- 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.\n- **SECTION_SCHEMAS** is typed as `Record<SectionType, ZodType>` or `{ [K in SectionType]: ZodType }` so that keys match the registry and SectionDataRegistry.\n\n**Export:** The app imports **SECTION_SCHEMAS** and passes it as **config.schemas** to JsonPagesEngine. The Form Factory traverses these schemas to build editors.\n\n**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.\n\n---\n\n## A.4 File Paths & Data Layout\n\nPurpose Path (conventional) Description Site config `src/data/config/site.json` SiteConfig (identity, header, footer, pages list). Menu config `src/data/config/menu.json` MenuConfig (e.g. main nav). Theme config `src/data/config/theme.json` ThemeConfig (tokens). Page data `src/data/pages/<slug>.json` One file per page; content is PageConfig (slug, meta, sections). Base schemas `src/lib/base-schemas.ts` BaseSectionData, BaseArrayItem, BaseSectionSettingsSchema. Schema aggregate `src/lib/schemas.ts` SECTION_SCHEMAS; re-exports base schemas. Registry `src/lib/ComponentRegistry.tsx` ComponentRegistry object. Add-section config `src/lib/addSectionConfig.ts` addSectionConfig (AddSectionConfig). Tenant types & augmentation `src/types.ts` SectionComponentPropsMap, PageConfig, SiteConfig, MenuConfig, ThemeConfig; **declare module '@olonjs/core'** for SectionDataRegistry and SectionSettingsRegistry; re-export from `@olonjs/core`. Bootstrap `src/App.tsx` Imports config (site, theme, menu, pages), registry, schemas, addSection, themeCss; builds JsonPagesConfig; renders .\n\nThe 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.\n\n**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.\n\n---\n\n## A.5 Integration Checklist (Code-Generation)\n\nWhen generating or auditing a tenant, ensure the following in order:\n\n 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).\n 2. **Base schemas** — **src/lib/base-schemas.ts** exports BaseSectionData, BaseArrayItem, BaseSectionSettingsSchema (and optional CtaSchema or similar shared fragments).\n 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`.\n 4. **ComponentRegistry** — Import every View; build object **{ \\[K in SectionType\\]: ViewComponent }**; type as **{ \\[K in SectionType\\]: React.FC<SectionComponentPropsMap\\[K\\]> }**.\n 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.\n 6. **addSectionConfig** — addableSectionTypes, sectionTypeLabels, getDefaultSectionData; export as AddSectionConfig.\n 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.\n 8. **Data files** — Create or update site.json, menu.json, theme.json, and one or more **.json** under the paths in A.4. Ensure JSON shapes match SiteConfig, MenuConfig, ThemeConfig, PageConfig.\n 9. **Runtime theme publication** — Publish the theme contract as runtime CSS custom properties before themed sections render.\n10. **Tenant CSS** — Include TOCC (§7) selectors in global CSS so the Stage overlay is visible, and bridge semantic theme variables where needed.\n11. **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.\n\n**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.\n\n---\n\n## A.6 v1.3 Path/Nested Strictness Addendum (breaking)\n\nThis addendum extends Appendix A without removing prior v1.2 obligations:\n\n1. **Type exports** — Core and/or shared types module should expose `SelectionPathSegment` and `SelectionPath` for Studio messaging and Inspector expansion logic.\n2. **Protocol migration** — Replace flat payload fields `itemField` / `itemId` with `itemPath?: SelectionPath` in strict v1.3 channels.\n3. **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).\n4. **Backward compatibility policy** — Legacy flat fields may exist only in transitional adapters outside strict mode; normative v1.3 contract is path-only.\n\n---\n\n## A.7 v1.4 Local Design Tokens Implementation Addendum\n\nThis addendum extends Appendix A without removing prior v1.3 obligations:\n\n1. **Theme source of truth** — Tenant theme values belong in `src/data/config/theme.json`.\n2. **Runtime publication** — Core and/or tenant bootstrap **must** expose those values as runtime CSS custom properties before section rendering.\n3. **Local scope** — A themed section must define `--local-*` variables on its root for the color/radius concerns it owns.\n4. **Class consumption** — Section-owned color/radius utilities must consume `var(--local-*)`, not raw hardcoded theme values.\n5. **Typography policy** — Fonts must consume the published semantic font chain; local font tokens are optional and only for local remapping.\n6. **Migration policy** — Hardcoded colors/radii may exist only as temporary compatibility shims or purely decorative exceptions, not as the primary section contract.\n\nCanonical implementation pattern:\n\n```text\ntheme.json -> published runtime theme vars -> section --local-* -> JSX classes\n```\n\nCanonical typography pattern:\n\n```text\ntheme.json -> published semantic font vars -> tenant font utility/variable -> section typography\n```\n\nMinimal compliant example:\n\n```tsx\n<section\n style={{\n '--local-bg': 'var(--background)',\n '--local-text': 'var(--foreground)',\n '--local-primary': 'var(--primary)',\n '--local-radius-md': 'var(--theme-radius-md)',\n } as React.CSSProperties}\n className=\"bg-[var(--local-bg)]\"\n>\n <h2 className=\"font-display text-[var(--local-text)]\">Title</h2>\n <a className=\"bg-[var(--local-primary)] rounded-[var(--local-radius-md)]\">CTA</a>\n</section>\n```\n\nDeterministic compliance checklist:\n\n1. Canonical semantic theme keys exist.\n2. Runtime publication exists.\n3. Section-local color/radius scope exists.\n4. Section-owned color/radius classes consume `var(--local-*)`.\n5. Fonts consume the semantic published font chain.\n6. Primary themed values are not hardcoded.\n\n---\n\n**Validation:** Align with current `@olonjs/core` exports (SectionType, MenuItem, AddSectionConfig, JsonPagesConfig, and in v1.3+ path types for Studio selection), with the deterministic `ThemeConfig` contract, and with the runtime theme publication contract used by tenant CSS.\\\n**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."
|
|
10356
|
+
"content": "# 📐 OlonJS Architecture Specifications v1.5\n\n**Status:** Mandatory Standard\\\n**Version:** 1.5.0 (Sovereign Core Edition — Architecture + Studio/ICE UX, Path-Deterministic Nested Editing, Deterministic Local Design Tokens, Three-Layer CSS Bridge Contract)\\\n**Target:** Senior Architects / AI Agents / Enterprise Governance\n\nThis tenant follows the current OlonJS source-of-truth model: the tenant app owns content, schemas, theme, and persistence wiring; `@olonjs/core` owns the Studio shell, routing, preview, and editing engine.\n\n---\n\n## Canonical Editorial Flows\n\nThe supported Studio flows are now:\n\n- `SSG` for static HTML and route output.\n- `Save to file` for local JSON persistence back into tenant source files.\n- `Hot Save` for cloud/editorial persistence when the tenant config provides it.\n- `Add Section` for deterministic section lifecycle management inside Studio.\n\nPrevious one-off bake and JSON export paths are no longer part of Studio.\n\n---\n\n## Persistence Model\n\n`@olonjs/core` no longer performs HTML bake or ZIP export. Studio now invokes tenant-provided persistence callbacks:\n\n- `saveToFile(state, slug)`\n- `hotSave(state, slug)`\n\nThis keeps persistence explicit, tenant-owned, and aligned with the current `JsonPagesConfig` contract.\n\n---\n\n## Tenant Source Of Truth\n\n`apps/tenant-alpha` is the DNA source of truth for this tenant. Generated CLI templates are downstream artifacts and should be regenerated from source apps instead of being edited manually.\n\nThe canonical content and design files remain:\n\n- `src/data/config/site.json`\n- `src/data/config/menu.json`\n- `src/data/config/theme.json`\n- `src/data/pages/<slug>.json`\n\n---\n\n## Reference Specs\n\nUse these monorepo sources for the full protocol and architecture details:\n\n- `specs/olonjsSpecs_V_1_5.md`\n- `apps/tenant-alpha/specs/olonjsSpecs_V.1.3.md`\n\nThese source specs are the maintained references for architecture, Studio behavior, and tenant compliance."
|
|
10966
10357
|
},
|
|
10967
10358
|
"settings": {}
|
|
10968
10359
|
}
|
|
10969
10360
|
]
|
|
10970
10361
|
}
|
|
10362
|
+
|
|
10971
10363
|
END_OF_FILE_CONTENT
|
|
10972
10364
|
echo "Creating src/data/pages/home.json..."
|
|
10973
10365
|
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
@@ -10984,7 +10376,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
|
10984
10376
|
"type": "hero",
|
|
10985
10377
|
"data": {
|
|
10986
10378
|
"eyebrow": "Contract layer · v1.4 · Open Core",
|
|
10987
|
-
"title": "Start
|
|
10379
|
+
"title": "Start Building",
|
|
10988
10380
|
"titleHighlight": "for the agentic web.",
|
|
10989
10381
|
"description": "AI agents are becoming operational actors in commerce, marketing, and support. But websites are still built for humans first: HTML-heavy, CMS-fragmented, and inconsistent across properties. That makes agent integration slow, brittle, and expensive. Olon introduces a deterministic machine contract for websites OlonJS. This makes content reliably readable and operable by agents while preserving normal human UI.",
|
|
10990
10382
|
"ctas": [
|
|
@@ -11001,8 +10393,8 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
|
11001
10393
|
"variant": "secondary"
|
|
11002
10394
|
}
|
|
11003
10395
|
],
|
|
11004
|
-
"docsLabel": "
|
|
11005
|
-
"docsHref": "
|
|
10396
|
+
"docsLabel": "Explore platform",
|
|
10397
|
+
"docsHref": "/platform/overview",
|
|
11006
10398
|
"heroImage": {
|
|
11007
10399
|
"url": "https://bat5elmxofxdroan.public.blob.vercel-storage.com/tenant-assets/511f18d7-d8ac-4292-ad8a-b0efa99401a3/1774286598548-adac7c36-9001-451d-9b16-c3787ac27f57-signup-hero-olon-graded_1_.png",
|
|
11008
10400
|
"alt": ""
|
|
@@ -11160,8 +10552,8 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/platform/overview.json"
|
|
|
11160
10552
|
"id": "overview-page",
|
|
11161
10553
|
"slug": "platform/overview",
|
|
11162
10554
|
"meta": {
|
|
11163
|
-
"title": "OlonJS —
|
|
11164
|
-
"description": "
|
|
10555
|
+
"title": "OlonJS — Platform Overview",
|
|
10556
|
+
"description": "Overview of the OlonJS platform, architecture direction, and product surface."
|
|
11165
10557
|
},
|
|
11166
10558
|
"sections": [
|
|
11167
10559
|
{
|
|
@@ -11175,15 +10567,15 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/platform/overview.json"
|
|
|
11175
10567
|
"href": "/"
|
|
11176
10568
|
},
|
|
11177
10569
|
{
|
|
11178
|
-
"id": "crumb-
|
|
11179
|
-
"label": "
|
|
11180
|
-
"href": "/
|
|
10570
|
+
"id": "crumb-platform",
|
|
10571
|
+
"label": "Platform",
|
|
10572
|
+
"href": "/platform/overview"
|
|
11181
10573
|
}
|
|
11182
10574
|
],
|
|
11183
10575
|
"badge": "",
|
|
11184
10576
|
"title": "Platform",
|
|
11185
10577
|
"titleItalic": "Overview",
|
|
11186
|
-
"description": "
|
|
10578
|
+
"description": "High-level overview of the OlonJS platform."
|
|
11187
10579
|
}
|
|
11188
10580
|
}
|
|
11189
10581
|
]
|
|
@@ -11675,6 +11067,18 @@ export function getPageMeta(slug: string): { title: string; description: string
|
|
|
11675
11067
|
return { title, description };
|
|
11676
11068
|
}
|
|
11677
11069
|
|
|
11070
|
+
export function getWebMcpBuildState(): {
|
|
11071
|
+
pages: Record<string, PageConfig>;
|
|
11072
|
+
schemas: JsonPagesConfig['schemas'];
|
|
11073
|
+
siteConfig: SiteConfig;
|
|
11074
|
+
} {
|
|
11075
|
+
return {
|
|
11076
|
+
pages,
|
|
11077
|
+
schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
|
|
11078
|
+
siteConfig,
|
|
11079
|
+
};
|
|
11080
|
+
}
|
|
11081
|
+
|
|
11678
11082
|
END_OF_FILE_CONTENT
|
|
11679
11083
|
echo "Creating src/fonts.css..."
|
|
11680
11084
|
cat << 'END_OF_FILE_CONTENT' > "src/fonts.css"
|
|
@@ -13366,7 +12770,7 @@ import react from '@vitejs/plugin-react';
|
|
|
13366
12770
|
import tailwindcss from '@tailwindcss/vite';
|
|
13367
12771
|
import path from 'path';
|
|
13368
12772
|
import fs from 'fs';
|
|
13369
|
-
import { fileURLToPath } from 'url';
|
|
12773
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
13370
12774
|
|
|
13371
12775
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13372
12776
|
|
|
@@ -13414,6 +12818,20 @@ function isTenantPageJsonRequest(req, pathname) {
|
|
|
13414
12818
|
const viteOrStaticPrefixes = ['/api/', '/assets/', '/src/', '/node_modules/', '/public/', '/@'];
|
|
13415
12819
|
return !viteOrStaticPrefixes.some((prefix) => pathname.startsWith(prefix));
|
|
13416
12820
|
}
|
|
12821
|
+
|
|
12822
|
+
function normalizeManifestSlug(raw) {
|
|
12823
|
+
return decodeURIComponent(raw || '')
|
|
12824
|
+
.replace(/^\/+|\/+$/g, '')
|
|
12825
|
+
.replace(/\\/g, '/')
|
|
12826
|
+
.replace(/(\.schema)?\.json$/i, '');
|
|
12827
|
+
}
|
|
12828
|
+
|
|
12829
|
+
async function loadWebMcpBuilders() {
|
|
12830
|
+
const moduleUrl = pathToFileURL(
|
|
12831
|
+
path.resolve(__dirname, '..', '..', 'packages', 'core', 'src', 'lib', 'webmcp-contracts.mjs')
|
|
12832
|
+
).href;
|
|
12833
|
+
return import(moduleUrl);
|
|
12834
|
+
}
|
|
13417
12835
|
export default defineConfig({
|
|
13418
12836
|
plugins: [
|
|
13419
12837
|
react(),
|
|
@@ -13425,6 +12843,75 @@ export default defineConfig({
|
|
|
13425
12843
|
const pathname = (req.url || '').split('?')[0];
|
|
13426
12844
|
const isPageJsonRequest = isTenantPageJsonRequest(req, pathname);
|
|
13427
12845
|
|
|
12846
|
+
const handleManifestRequest = async () => {
|
|
12847
|
+
const { buildPageContract, buildPageManifest, buildSiteManifest } = await loadWebMcpBuilders();
|
|
12848
|
+
const ssrEntry = await server.ssrLoadModule('/src/entry-ssg.tsx');
|
|
12849
|
+
const buildState = ssrEntry.getWebMcpBuildState();
|
|
12850
|
+
|
|
12851
|
+
if (req.method === 'GET' && pathname === '/mcp-manifest.json') {
|
|
12852
|
+
sendJson(res, 200, buildSiteManifest({
|
|
12853
|
+
pages: buildState.pages,
|
|
12854
|
+
schemas: buildState.schemas,
|
|
12855
|
+
siteConfig: buildState.siteConfig,
|
|
12856
|
+
}));
|
|
12857
|
+
return true;
|
|
12858
|
+
}
|
|
12859
|
+
|
|
12860
|
+
const pageManifestMatch = pathname.match(/^\/mcp-manifests\/(.+)\.json$/i);
|
|
12861
|
+
if (pageManifestMatch && req.method === 'GET') {
|
|
12862
|
+
const slug = normalizeManifestSlug(pageManifestMatch[1]);
|
|
12863
|
+
const pageConfig = buildState.pages[slug];
|
|
12864
|
+
if (!pageConfig) {
|
|
12865
|
+
sendJson(res, 404, { error: 'Page manifest not found' });
|
|
12866
|
+
return true;
|
|
12867
|
+
}
|
|
12868
|
+
|
|
12869
|
+
sendJson(res, 200, buildPageManifest({
|
|
12870
|
+
slug,
|
|
12871
|
+
pageConfig,
|
|
12872
|
+
schemas: buildState.schemas,
|
|
12873
|
+
siteConfig: buildState.siteConfig,
|
|
12874
|
+
}));
|
|
12875
|
+
return true;
|
|
12876
|
+
}
|
|
12877
|
+
|
|
12878
|
+
const schemaMatch = pathname.match(/^\/schemas\/(.+)\.schema\.json$/i);
|
|
12879
|
+
if (!schemaMatch || req.method !== 'GET') return false;
|
|
12880
|
+
|
|
12881
|
+
const slug = normalizeManifestSlug(schemaMatch[1]);
|
|
12882
|
+
const pageConfig = buildState.pages[slug];
|
|
12883
|
+
if (!pageConfig) {
|
|
12884
|
+
sendJson(res, 404, { error: 'Schema contract not found' });
|
|
12885
|
+
return true;
|
|
12886
|
+
}
|
|
12887
|
+
|
|
12888
|
+
sendJson(res, 200, buildPageContract({
|
|
12889
|
+
slug,
|
|
12890
|
+
pageConfig,
|
|
12891
|
+
schemas: buildState.schemas,
|
|
12892
|
+
siteConfig: buildState.siteConfig,
|
|
12893
|
+
}));
|
|
12894
|
+
return true;
|
|
12895
|
+
};
|
|
12896
|
+
|
|
12897
|
+
if (
|
|
12898
|
+
req.method === 'GET' &&
|
|
12899
|
+
(
|
|
12900
|
+
pathname === '/mcp-manifest.json'
|
|
12901
|
+
|| /^\/mcp-manifests\/.+\.json$/i.test(pathname)
|
|
12902
|
+
|| /^\/schemas\/.+\.schema\.json$/i.test(pathname)
|
|
12903
|
+
)
|
|
12904
|
+
) {
|
|
12905
|
+
void handleManifestRequest()
|
|
12906
|
+
.then((handled) => {
|
|
12907
|
+
if (!handled) next();
|
|
12908
|
+
})
|
|
12909
|
+
.catch((error) => {
|
|
12910
|
+
sendJson(res, 500, { error: error?.message || 'Manifest generation failed' });
|
|
12911
|
+
});
|
|
12912
|
+
return;
|
|
12913
|
+
}
|
|
12914
|
+
|
|
13428
12915
|
if (isPageJsonRequest) {
|
|
13429
12916
|
const normalizedPath = decodeURIComponent(pathname).replace(/\\/g, '/');
|
|
13430
12917
|
const slug = normalizedPath.replace(/^\/+/, '').replace(/\.json$/i, '').replace(/^\/+|\/+$/g, '');
|
|
@@ -13490,9 +12977,21 @@ export default defineConfig({
|
|
|
13490
12977
|
resolve: {
|
|
13491
12978
|
alias: {
|
|
13492
12979
|
'@': path.resolve(__dirname, './src'),
|
|
12980
|
+
'@olonjs/core': path.resolve(__dirname, '..', '..', 'packages', 'core', 'src', 'index.ts'),
|
|
13493
12981
|
'next/link': path.resolve(__dirname, './src/shims/next-link.tsx'),
|
|
13494
12982
|
},
|
|
13495
12983
|
},
|
|
12984
|
+
server: {
|
|
12985
|
+
fs: {
|
|
12986
|
+
allow: [
|
|
12987
|
+
path.resolve(__dirname, '..', '..'),
|
|
12988
|
+
],
|
|
12989
|
+
},
|
|
12990
|
+
watch: {
|
|
12991
|
+
usePolling: true,
|
|
12992
|
+
interval: 300,
|
|
12993
|
+
},
|
|
12994
|
+
},
|
|
13496
12995
|
});
|
|
13497
12996
|
|
|
13498
12997
|
|