@olonjs/cli 3.0.144 → 3.0.145
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 +966 -39
- package/assets/templates/alpha/src_tenant.sh +966 -39
- package/package.json +1 -1
|
@@ -1817,7 +1817,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1817
1817
|
"@tiptap/extension-link": "^2.11.5",
|
|
1818
1818
|
"@tiptap/react": "^2.11.5",
|
|
1819
1819
|
"@tiptap/starter-kit": "^2.11.5",
|
|
1820
|
-
"@olonjs/core": "^1.1.
|
|
1820
|
+
"@olonjs/core": "^1.1.11",
|
|
1821
1821
|
"class-variance-authority": "^0.7.1",
|
|
1822
1822
|
"clsx": "^2.1.1",
|
|
1823
1823
|
"lucide-react": "^0.474.0",
|
|
@@ -2131,7 +2131,7 @@ import { build } from 'vite';
|
|
|
2131
2131
|
import path from 'path';
|
|
2132
2132
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
2133
2133
|
import fs from 'fs/promises';
|
|
2134
|
-
import { webmcp } from '@olonjs/core';
|
|
2134
|
+
import { resolvePageMatchFromRegistry, resolvePublicPageDocument, webmcp } from '@olonjs/core';
|
|
2135
2135
|
|
|
2136
2136
|
const {
|
|
2137
2137
|
buildPageContract,
|
|
@@ -2144,6 +2144,7 @@ const {
|
|
|
2144
2144
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
2145
2145
|
const root = path.resolve(__dirname, '..');
|
|
2146
2146
|
const pagesDir = path.resolve(root, 'src/data/pages');
|
|
2147
|
+
const collectionsDir = path.resolve(root, 'src/data/collections');
|
|
2147
2148
|
const publicDir = path.resolve(root, 'public');
|
|
2148
2149
|
const distDir = path.resolve(root, 'dist');
|
|
2149
2150
|
const distSsrDir = path.resolve(root, 'dist-ssr');
|
|
@@ -2184,6 +2185,42 @@ function toCanonicalSlug(relativeJsonPath) {
|
|
|
2184
2185
|
return slug;
|
|
2185
2186
|
}
|
|
2186
2187
|
|
|
2188
|
+
async function readJsonFile(filePath) {
|
|
2189
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
2190
|
+
return JSON.parse(raw);
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
async function expandCollectionTarget(slug, pageFilePath) {
|
|
2194
|
+
if (!/\[[^\]]+\]/.test(slug)) return [slug];
|
|
2195
|
+
|
|
2196
|
+
let pageConfig;
|
|
2197
|
+
try {
|
|
2198
|
+
pageConfig = await readJsonFile(pageFilePath);
|
|
2199
|
+
} catch {
|
|
2200
|
+
return [slug];
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
const binding = pageConfig?.collection;
|
|
2204
|
+
if (!binding || typeof binding.source !== 'string' || typeof binding.paramKey !== 'string') {
|
|
2205
|
+
return [slug];
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
const token = `[${binding.paramKey}]`;
|
|
2209
|
+
if (!slug.includes(token)) return [slug];
|
|
2210
|
+
|
|
2211
|
+
const collectionPath = path.resolve(collectionsDir, binding.source, `${binding.source}.json`);
|
|
2212
|
+
let collection;
|
|
2213
|
+
try {
|
|
2214
|
+
collection = await readJsonFile(collectionPath);
|
|
2215
|
+
} catch {
|
|
2216
|
+
return [slug];
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
if (!collection || typeof collection !== 'object' || Array.isArray(collection)) return [slug];
|
|
2220
|
+
const itemIds = Object.keys(collection).sort((a, b) => a.localeCompare(b));
|
|
2221
|
+
return itemIds.length > 0 ? itemIds.map((itemId) => slug.replace(token, itemId)) : [slug];
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2187
2224
|
async function listJsonFilesRecursive(dir) {
|
|
2188
2225
|
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
2189
2226
|
const files = [];
|
|
@@ -2206,7 +2243,14 @@ async function discoverTargets() {
|
|
|
2206
2243
|
files = [];
|
|
2207
2244
|
}
|
|
2208
2245
|
|
|
2209
|
-
const rawSlugs =
|
|
2246
|
+
const rawSlugs = (
|
|
2247
|
+
await Promise.all(
|
|
2248
|
+
files.map(async (fullPath) => {
|
|
2249
|
+
const slug = toCanonicalSlug(path.relative(pagesDir, fullPath));
|
|
2250
|
+
return expandCollectionTarget(slug, fullPath);
|
|
2251
|
+
})
|
|
2252
|
+
)
|
|
2253
|
+
).flat();
|
|
2210
2254
|
const slugs = Array.from(new Set(rawSlugs)).sort((a, b) => a.localeCompare(b));
|
|
2211
2255
|
|
|
2212
2256
|
return slugs.map((slug) => {
|
|
@@ -2264,15 +2308,26 @@ const remoteStylesheetTags = getRemoteStylesheets()
|
|
|
2264
2308
|
const webMcpBuildState = getWebMcpBuildState();
|
|
2265
2309
|
|
|
2266
2310
|
for (const { slug } of targets) {
|
|
2267
|
-
const pageConfig = webMcpBuildState.pages
|
|
2311
|
+
const pageConfig = resolvePageMatchFromRegistry(webMcpBuildState.pages, slug)?.page;
|
|
2268
2312
|
if (!pageConfig) continue;
|
|
2313
|
+
const resolvedPageDocument = resolvePublicPageDocument({
|
|
2314
|
+
slug,
|
|
2315
|
+
pages: webMcpBuildState.pages,
|
|
2316
|
+
siteConfig: webMcpBuildState.siteConfig,
|
|
2317
|
+
themeConfig: webMcpBuildState.themeConfig,
|
|
2318
|
+
menuConfig: webMcpBuildState.menuConfig,
|
|
2319
|
+
collections: webMcpBuildState.collections,
|
|
2320
|
+
collectionSchemas: webMcpBuildState.collectionSchemas,
|
|
2321
|
+
refDocuments: webMcpBuildState.refDocuments,
|
|
2322
|
+
});
|
|
2323
|
+
const publicPageConfig = resolvedPageDocument?.page ?? pageConfig;
|
|
2269
2324
|
|
|
2270
|
-
// Export the
|
|
2271
|
-
await writeJsonTargets(`pages/${slug}.json`,
|
|
2325
|
+
// Export the resolved public JSON data for the agentic web (so readResource matches runtime).
|
|
2326
|
+
await writeJsonTargets(`pages/${slug}.json`, publicPageConfig);
|
|
2272
2327
|
|
|
2273
2328
|
const contract = buildPageContract({
|
|
2274
2329
|
slug,
|
|
2275
|
-
pageConfig,
|
|
2330
|
+
pageConfig: publicPageConfig,
|
|
2276
2331
|
schemas: webMcpBuildState.schemas,
|
|
2277
2332
|
submissionSchemas: webMcpBuildState.submissionSchemas,
|
|
2278
2333
|
siteConfig: webMcpBuildState.siteConfig,
|
|
@@ -2280,7 +2335,7 @@ for (const { slug } of targets) {
|
|
|
2280
2335
|
await writeSsrJson(`schemas/${slug}.schema.json`, contract);
|
|
2281
2336
|
const pageManifest = buildPageManifest({
|
|
2282
2337
|
slug,
|
|
2283
|
-
pageConfig,
|
|
2338
|
+
pageConfig: publicPageConfig,
|
|
2284
2339
|
schemas: webMcpBuildState.schemas,
|
|
2285
2340
|
siteConfig: webMcpBuildState.siteConfig,
|
|
2286
2341
|
});
|
|
@@ -2549,6 +2604,8 @@ const __dirname = path.dirname(__filename);
|
|
|
2549
2604
|
const rootDir = path.resolve(__dirname, '..');
|
|
2550
2605
|
const sourceDir = path.join(rootDir, 'src', 'data', 'pages');
|
|
2551
2606
|
const targetDir = path.join(rootDir, 'public', 'pages');
|
|
2607
|
+
const sourceCollectionsDir = path.join(rootDir, 'src', 'data', 'collections');
|
|
2608
|
+
const targetCollectionsDir = path.join(rootDir, 'public', 'collections');
|
|
2552
2609
|
const sourceSiteConfigPath = path.join(rootDir, 'src', 'data', 'config', 'site.json');
|
|
2553
2610
|
const targetConfigDir = path.join(rootDir, 'public', 'config');
|
|
2554
2611
|
const targetSiteConfigPath = path.join(targetConfigDir, 'site.json');
|
|
@@ -2562,12 +2619,18 @@ fs.rmSync(targetDir, { recursive: true, force: true });
|
|
|
2562
2619
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
2563
2620
|
fs.cpSync(sourceDir, targetDir, { recursive: true });
|
|
2564
2621
|
|
|
2622
|
+
fs.rmSync(targetCollectionsDir, { recursive: true, force: true });
|
|
2623
|
+
if (fs.existsSync(sourceCollectionsDir)) {
|
|
2624
|
+
fs.mkdirSync(targetCollectionsDir, { recursive: true });
|
|
2625
|
+
fs.cpSync(sourceCollectionsDir, targetCollectionsDir, { recursive: true });
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2565
2628
|
if (fs.existsSync(sourceSiteConfigPath)) {
|
|
2566
2629
|
fs.mkdirSync(targetConfigDir, { recursive: true });
|
|
2567
2630
|
fs.cpSync(sourceSiteConfigPath, targetSiteConfigPath);
|
|
2568
2631
|
}
|
|
2569
2632
|
|
|
2570
|
-
console.log('[sync-pages-to-public] Synced pages
|
|
2633
|
+
console.log('[sync-pages-to-public] Synced pages, collections, and site config to public/');
|
|
2571
2634
|
|
|
2572
2635
|
END_OF_FILE_CONTENT
|
|
2573
2636
|
echo "Creating scripts/webmcp-feature-check.mjs..."
|
|
@@ -2885,6 +2948,7 @@ import { JsonPagesEngine } from '@olonjs/core';
|
|
|
2885
2948
|
import type { JsonPagesConfig, LibraryImageEntry, ProjectState } from '@olonjs/core';
|
|
2886
2949
|
import { normalizeBasePath, withBasePath } from '@olonjs/core';
|
|
2887
2950
|
import { ComponentRegistry } from '@/lib/ComponentRegistry';
|
|
2951
|
+
import { CollectionRegistry } from '@/lib/CollectionRegistry';
|
|
2888
2952
|
import { SECTION_SCHEMAS } from '@/lib/schemas';
|
|
2889
2953
|
import { addSectionConfig } from '@/lib/addSectionConfig';
|
|
2890
2954
|
import { getHydratedData } from '@/lib/draftStorage';
|
|
@@ -2895,6 +2959,7 @@ import { startCloudSaveStream } from '@olonjs/core';
|
|
|
2895
2959
|
import siteData from '@/data/config/site.json';
|
|
2896
2960
|
import themeData from '@/data/config/theme.json';
|
|
2897
2961
|
import menuData from '@/data/config/menu.json';
|
|
2962
|
+
import libriData from '@/data/collections/libri/libri.json';
|
|
2898
2963
|
import { getFilePages } from '@/lib/getFilePages';
|
|
2899
2964
|
import { DopaDrawer } from '@/components/save-drawer/DopaDrawer';
|
|
2900
2965
|
import { EmptyTenantView } from '@/components/empty-tenant';
|
|
@@ -2926,6 +2991,9 @@ const TENANT_ID = 'alpha';
|
|
|
2926
2991
|
|
|
2927
2992
|
const filePages = getFilePages();
|
|
2928
2993
|
const fileSiteConfig = siteData as unknown as SiteConfig;
|
|
2994
|
+
const collections = {
|
|
2995
|
+
libri: libriData as unknown as Record<string, unknown>,
|
|
2996
|
+
} satisfies NonNullable<JsonPagesConfig['collections']>;
|
|
2929
2997
|
const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024;
|
|
2930
2998
|
const ASSET_UPLOAD_MAX_RETRIES = 2;
|
|
2931
2999
|
const ASSET_UPLOAD_TIMEOUT_MS = 20_000;
|
|
@@ -2963,6 +3031,7 @@ type CachedCloudContent = {
|
|
|
2963
3031
|
savedAt: number;
|
|
2964
3032
|
siteConfig: unknown | null;
|
|
2965
3033
|
pages: Record<string, unknown>;
|
|
3034
|
+
collections?: JsonPagesConfig['collections'];
|
|
2966
3035
|
};
|
|
2967
3036
|
|
|
2968
3037
|
const CLOUD_CACHE_KEY = 'jp_cloud_content_cache_v1';
|
|
@@ -3008,7 +3077,7 @@ function asString(value: unknown, fallback: string): string {
|
|
|
3008
3077
|
function normalizeRouteSlug(value: string): string {
|
|
3009
3078
|
return value
|
|
3010
3079
|
.toLowerCase()
|
|
3011
|
-
.replace(/[^a-z0-9/_-]/g, '-')
|
|
3080
|
+
.replace(/[^a-z0-9/_[\]-]/g, '-')
|
|
3012
3081
|
.replace(/^\/+|\/+$/g, '') || 'home';
|
|
3013
3082
|
}
|
|
3014
3083
|
|
|
@@ -3034,6 +3103,7 @@ function coercePageConfig(slug: string, value: unknown): PageConfig | null {
|
|
|
3034
3103
|
slug: normalizedSlug,
|
|
3035
3104
|
meta: { title, description },
|
|
3036
3105
|
sections: input.sections as PageConfig['sections'],
|
|
3106
|
+
...(isObjectRecord(input.collection) ? { collection: input.collection as unknown as PageConfig['collection'] } : {}),
|
|
3037
3107
|
...(typeof input['global-header'] === 'boolean' ? { 'global-header': input['global-header'] } : {}),
|
|
3038
3108
|
};
|
|
3039
3109
|
}
|
|
@@ -3175,7 +3245,7 @@ function normalizeSlugForCache(slug: string): string {
|
|
|
3175
3245
|
slug
|
|
3176
3246
|
.trim()
|
|
3177
3247
|
.toLowerCase()
|
|
3178
|
-
.replace(/[^a-z0-9/_-]/g, '-')
|
|
3248
|
+
.replace(/[^a-z0-9/_[\]-]/g, '-')
|
|
3179
3249
|
.replace(/^\/+|\/+$/g, '') || 'home'
|
|
3180
3250
|
);
|
|
3181
3251
|
}
|
|
@@ -3740,10 +3810,12 @@ function App() {
|
|
|
3740
3810
|
basePath: APP_BASE_PATH,
|
|
3741
3811
|
registry: ComponentRegistry as JsonPagesConfig['registry'],
|
|
3742
3812
|
schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
|
|
3813
|
+
collectionSchemas: CollectionRegistry as unknown as JsonPagesConfig['collectionSchemas'],
|
|
3743
3814
|
pages,
|
|
3744
3815
|
siteConfig,
|
|
3745
3816
|
themeConfig,
|
|
3746
3817
|
menuConfig,
|
|
3818
|
+
collections,
|
|
3747
3819
|
refDocuments,
|
|
3748
3820
|
themeCss: { tenant: resolvedTenantCss },
|
|
3749
3821
|
iconRegistry: iconMap,
|
|
@@ -3780,6 +3852,7 @@ function App() {
|
|
|
3780
3852
|
slug,
|
|
3781
3853
|
page: state.page,
|
|
3782
3854
|
siteConfig: state.site,
|
|
3855
|
+
collections: state.collections,
|
|
3783
3856
|
}),
|
|
3784
3857
|
});
|
|
3785
3858
|
const body = (await res.json().catch(() => ({}))) as { error?: string; code?: string };
|
|
@@ -3793,6 +3866,7 @@ function App() {
|
|
|
3793
3866
|
keyFingerprint,
|
|
3794
3867
|
savedAt: Date.now(),
|
|
3795
3868
|
siteConfig: state.site ?? null,
|
|
3869
|
+
collections: state.collections,
|
|
3796
3870
|
pages: {
|
|
3797
3871
|
...(existing?.pages ?? {}),
|
|
3798
3872
|
[normalizedSlug]: state.page,
|
|
@@ -4039,6 +4113,39 @@ export default App;
|
|
|
4039
4113
|
|
|
4040
4114
|
END_OF_FILE_CONTENT
|
|
4041
4115
|
# SKIP: src/App.tsx:Zone.Identifier is binary and cannot be embedded as text.
|
|
4116
|
+
mkdir -p "src/collections"
|
|
4117
|
+
mkdir -p "src/collections/libri"
|
|
4118
|
+
echo "Creating src/collections/libri/index.ts..."
|
|
4119
|
+
cat << 'END_OF_FILE_CONTENT' > "src/collections/libri/index.ts"
|
|
4120
|
+
export { LibroSchema, LibriCollectionSchema } from './schema';
|
|
4121
|
+
export type { Libro, LibriCollection } from './types';
|
|
4122
|
+
|
|
4123
|
+
END_OF_FILE_CONTENT
|
|
4124
|
+
echo "Creating src/collections/libri/schema.ts..."
|
|
4125
|
+
cat << 'END_OF_FILE_CONTENT' > "src/collections/libri/schema.ts"
|
|
4126
|
+
import { z } from 'zod';
|
|
4127
|
+
import { BaseCollectionItem } from '@olonjs/core';
|
|
4128
|
+
|
|
4129
|
+
export const LibroSchema = BaseCollectionItem.extend({
|
|
4130
|
+
title: z.string().describe('ui:text'),
|
|
4131
|
+
author: z.string().describe('ui:text'),
|
|
4132
|
+
year: z.number().describe('ui:number'),
|
|
4133
|
+
genre: z.string().describe('ui:text'),
|
|
4134
|
+
summary: z.string().describe('ui:textarea'),
|
|
4135
|
+
});
|
|
4136
|
+
|
|
4137
|
+
export const LibriCollectionSchema = z.record(z.string(), LibroSchema);
|
|
4138
|
+
|
|
4139
|
+
END_OF_FILE_CONTENT
|
|
4140
|
+
echo "Creating src/collections/libri/types.ts..."
|
|
4141
|
+
cat << 'END_OF_FILE_CONTENT' > "src/collections/libri/types.ts"
|
|
4142
|
+
import { z } from 'zod';
|
|
4143
|
+
import { LibroSchema, LibriCollectionSchema } from './schema';
|
|
4144
|
+
|
|
4145
|
+
export type Libro = z.infer<typeof LibroSchema>;
|
|
4146
|
+
export type LibriCollection = z.infer<typeof LibriCollectionSchema>;
|
|
4147
|
+
|
|
4148
|
+
END_OF_FILE_CONTENT
|
|
4042
4149
|
mkdir -p "src/components"
|
|
4043
4150
|
echo "Creating src/components/ThemeProvider.tsx..."
|
|
4044
4151
|
cat << 'END_OF_FILE_CONTENT' > "src/components/ThemeProvider.tsx"
|
|
@@ -4102,6 +4209,221 @@ export function useTheme() {
|
|
|
4102
4209
|
return useContext(ThemeContext)
|
|
4103
4210
|
}
|
|
4104
4211
|
|
|
4212
|
+
END_OF_FILE_CONTENT
|
|
4213
|
+
mkdir -p "src/components/book-detail"
|
|
4214
|
+
echo "Creating src/components/book-detail/View.tsx..."
|
|
4215
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/book-detail/View.tsx"
|
|
4216
|
+
import type { BookDetailData } from './types';
|
|
4217
|
+
|
|
4218
|
+
type BookDetailViewProps = {
|
|
4219
|
+
data: BookDetailData;
|
|
4220
|
+
};
|
|
4221
|
+
|
|
4222
|
+
export function BookDetailView({ data }: BookDetailViewProps) {
|
|
4223
|
+
const book = data.item;
|
|
4224
|
+
|
|
4225
|
+
return (
|
|
4226
|
+
<main className="min-h-screen bg-background text-foreground px-6 py-16">
|
|
4227
|
+
<article
|
|
4228
|
+
data-jp-field="item"
|
|
4229
|
+
data-jp-item-id={book.id}
|
|
4230
|
+
data-jp-item-field="item"
|
|
4231
|
+
className="mx-auto w-full max-w-3xl rounded-2xl border border-border bg-card p-8 shadow-sm"
|
|
4232
|
+
>
|
|
4233
|
+
<a
|
|
4234
|
+
href="/libri"
|
|
4235
|
+
className="text-sm font-medium text-muted-foreground hover:text-foreground"
|
|
4236
|
+
>
|
|
4237
|
+
{data.backLabel}
|
|
4238
|
+
</a>
|
|
4239
|
+
<p className="mt-10 text-sm font-medium uppercase tracking-[0.18em] text-muted-foreground">
|
|
4240
|
+
{book.genre} · {book.year}
|
|
4241
|
+
</p>
|
|
4242
|
+
<h1 className="mt-3 text-4xl font-semibold tracking-tight sm:text-5xl">
|
|
4243
|
+
{book.title}
|
|
4244
|
+
</h1>
|
|
4245
|
+
<p className="mt-4 text-lg text-muted-foreground">
|
|
4246
|
+
{book.author}
|
|
4247
|
+
</p>
|
|
4248
|
+
<p className="mt-8 text-base leading-8 text-muted-foreground">
|
|
4249
|
+
{book.summary}
|
|
4250
|
+
</p>
|
|
4251
|
+
</article>
|
|
4252
|
+
</main>
|
|
4253
|
+
);
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
END_OF_FILE_CONTENT
|
|
4257
|
+
echo "Creating src/components/book-detail/index.ts..."
|
|
4258
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/book-detail/index.ts"
|
|
4259
|
+
export * from './View';
|
|
4260
|
+
export * from './schema';
|
|
4261
|
+
export * from './types';
|
|
4262
|
+
|
|
4263
|
+
END_OF_FILE_CONTENT
|
|
4264
|
+
echo "Creating src/components/book-detail/schema.ts..."
|
|
4265
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/book-detail/schema.ts"
|
|
4266
|
+
import { z } from 'zod';
|
|
4267
|
+
import { BaseSectionData } from '@olonjs/core';
|
|
4268
|
+
import { LibroSchema } from '@/collections/libri';
|
|
4269
|
+
|
|
4270
|
+
export const BookDetailSchema = BaseSectionData.extend({
|
|
4271
|
+
item: LibroSchema.describe('ui:collection-ref'),
|
|
4272
|
+
backLabel: z.string().default('Torna ai libri').describe('ui:text'),
|
|
4273
|
+
});
|
|
4274
|
+
|
|
4275
|
+
export const BookDetailSettingsSchema = z.object({});
|
|
4276
|
+
|
|
4277
|
+
END_OF_FILE_CONTENT
|
|
4278
|
+
echo "Creating src/components/book-detail/types.ts..."
|
|
4279
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/book-detail/types.ts"
|
|
4280
|
+
import { z } from 'zod';
|
|
4281
|
+
import { BookDetailSchema, BookDetailSettingsSchema } from './schema';
|
|
4282
|
+
|
|
4283
|
+
export type BookDetailData = z.infer<typeof BookDetailSchema>;
|
|
4284
|
+
export type BookDetailSettings = z.infer<typeof BookDetailSettingsSchema>;
|
|
4285
|
+
|
|
4286
|
+
END_OF_FILE_CONTENT
|
|
4287
|
+
mkdir -p "src/components/books-list"
|
|
4288
|
+
echo "Creating src/components/books-list/View.tsx..."
|
|
4289
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/books-list/View.tsx"
|
|
4290
|
+
import { useMemo, useState } from 'react';
|
|
4291
|
+
import type { Libro } from '@/collections/libri';
|
|
4292
|
+
import type { BooksListData } from './types';
|
|
4293
|
+
|
|
4294
|
+
type BooksListViewProps = {
|
|
4295
|
+
data: BooksListData;
|
|
4296
|
+
};
|
|
4297
|
+
|
|
4298
|
+
function toBooks(items: BooksListData['items']): Libro[] {
|
|
4299
|
+
return Object.values(items ?? {}).sort((a, b) => a.title.localeCompare(b.title));
|
|
4300
|
+
}
|
|
4301
|
+
|
|
4302
|
+
export function BooksListView({ data }: BooksListViewProps) {
|
|
4303
|
+
const books = useMemo(() => toBooks(data.items), [data.items]);
|
|
4304
|
+
const pageSize = Math.max(1, Math.floor(data.pageSize || 10));
|
|
4305
|
+
const totalPages = Math.max(1, Math.ceil(books.length / pageSize));
|
|
4306
|
+
const [page, setPage] = useState(1);
|
|
4307
|
+
const currentPage = Math.min(page, totalPages);
|
|
4308
|
+
const startIndex = (currentPage - 1) * pageSize;
|
|
4309
|
+
const visibleBooks = books.slice(startIndex, startIndex + pageSize);
|
|
4310
|
+
|
|
4311
|
+
return (
|
|
4312
|
+
<main className="min-h-screen bg-background text-foreground px-6 py-16">
|
|
4313
|
+
<section className="mx-auto flex w-full max-w-5xl flex-col gap-10">
|
|
4314
|
+
<div className="max-w-2xl">
|
|
4315
|
+
{data.eyebrow && (
|
|
4316
|
+
<p
|
|
4317
|
+
data-jp-field="eyebrow"
|
|
4318
|
+
className="text-sm font-medium uppercase tracking-[0.18em] text-muted-foreground"
|
|
4319
|
+
>
|
|
4320
|
+
{data.eyebrow}
|
|
4321
|
+
</p>
|
|
4322
|
+
)}
|
|
4323
|
+
<h1
|
|
4324
|
+
data-jp-field="title"
|
|
4325
|
+
className="mt-3 text-4xl font-semibold tracking-tight sm:text-5xl"
|
|
4326
|
+
>
|
|
4327
|
+
{data.title}
|
|
4328
|
+
</h1>
|
|
4329
|
+
{data.description && (
|
|
4330
|
+
<p
|
|
4331
|
+
data-jp-field="description"
|
|
4332
|
+
className="mt-4 text-base leading-7 text-muted-foreground"
|
|
4333
|
+
>
|
|
4334
|
+
{data.description}
|
|
4335
|
+
</p>
|
|
4336
|
+
)}
|
|
4337
|
+
</div>
|
|
4338
|
+
|
|
4339
|
+
<div data-jp-field="items" className="grid gap-4">
|
|
4340
|
+
{visibleBooks.map((book) => (
|
|
4341
|
+
<article
|
|
4342
|
+
key={book.id}
|
|
4343
|
+
data-jp-item-id={book.id}
|
|
4344
|
+
data-jp-item-field="items"
|
|
4345
|
+
className="rounded-xl border border-border bg-card p-5 shadow-sm"
|
|
4346
|
+
>
|
|
4347
|
+
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
4348
|
+
<div>
|
|
4349
|
+
<h2 className="text-xl font-semibold">{book.title}</h2>
|
|
4350
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
4351
|
+
{book.author} · {book.year} · {book.genre}
|
|
4352
|
+
</p>
|
|
4353
|
+
<p className="mt-3 max-w-3xl text-sm leading-6 text-muted-foreground">
|
|
4354
|
+
{book.summary}
|
|
4355
|
+
</p>
|
|
4356
|
+
</div>
|
|
4357
|
+
<a
|
|
4358
|
+
href={`/libri/${book.id}`}
|
|
4359
|
+
className="inline-flex shrink-0 items-center justify-center rounded-md border border-border px-3 py-2 text-sm font-medium hover:bg-muted"
|
|
4360
|
+
>
|
|
4361
|
+
Apri scheda
|
|
4362
|
+
</a>
|
|
4363
|
+
</div>
|
|
4364
|
+
</article>
|
|
4365
|
+
))}
|
|
4366
|
+
</div>
|
|
4367
|
+
|
|
4368
|
+
<nav className="flex items-center justify-between border-t border-border pt-6 text-sm">
|
|
4369
|
+
<button
|
|
4370
|
+
type="button"
|
|
4371
|
+
onClick={() => setPage((value) => Math.max(1, value - 1))}
|
|
4372
|
+
disabled={currentPage === 1}
|
|
4373
|
+
className="rounded-md border border-border px-3 py-2 font-medium disabled:cursor-not-allowed disabled:opacity-40 hover:bg-muted"
|
|
4374
|
+
>
|
|
4375
|
+
Precedente
|
|
4376
|
+
</button>
|
|
4377
|
+
<span className="text-muted-foreground">
|
|
4378
|
+
Pagina {currentPage} di {totalPages} · {books.length} libri
|
|
4379
|
+
</span>
|
|
4380
|
+
<button
|
|
4381
|
+
type="button"
|
|
4382
|
+
onClick={() => setPage((value) => Math.min(totalPages, value + 1))}
|
|
4383
|
+
disabled={currentPage === totalPages}
|
|
4384
|
+
className="rounded-md border border-border px-3 py-2 font-medium disabled:cursor-not-allowed disabled:opacity-40 hover:bg-muted"
|
|
4385
|
+
>
|
|
4386
|
+
Successiva
|
|
4387
|
+
</button>
|
|
4388
|
+
</nav>
|
|
4389
|
+
</section>
|
|
4390
|
+
</main>
|
|
4391
|
+
);
|
|
4392
|
+
}
|
|
4393
|
+
|
|
4394
|
+
END_OF_FILE_CONTENT
|
|
4395
|
+
echo "Creating src/components/books-list/index.ts..."
|
|
4396
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/books-list/index.ts"
|
|
4397
|
+
export * from './View';
|
|
4398
|
+
export * from './schema';
|
|
4399
|
+
export * from './types';
|
|
4400
|
+
|
|
4401
|
+
END_OF_FILE_CONTENT
|
|
4402
|
+
echo "Creating src/components/books-list/schema.ts..."
|
|
4403
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/books-list/schema.ts"
|
|
4404
|
+
import { z } from 'zod';
|
|
4405
|
+
import { BaseSectionData } from '@olonjs/core';
|
|
4406
|
+
import { LibroSchema } from '@/collections/libri';
|
|
4407
|
+
|
|
4408
|
+
export const BooksListSchema = BaseSectionData.extend({
|
|
4409
|
+
eyebrow: z.string().optional().describe('ui:text'),
|
|
4410
|
+
title: z.string().describe('ui:text'),
|
|
4411
|
+
description: z.string().optional().describe('ui:textarea'),
|
|
4412
|
+
items: z.record(z.string(), LibroSchema).describe('ui:collection-ref'),
|
|
4413
|
+
pageSize: z.number().default(10).describe('ui:number'),
|
|
4414
|
+
});
|
|
4415
|
+
|
|
4416
|
+
export const BooksListSettingsSchema = z.object({});
|
|
4417
|
+
|
|
4418
|
+
END_OF_FILE_CONTENT
|
|
4419
|
+
echo "Creating src/components/books-list/types.ts..."
|
|
4420
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/books-list/types.ts"
|
|
4421
|
+
import { z } from 'zod';
|
|
4422
|
+
import { BooksListSchema, BooksListSettingsSchema } from './schema';
|
|
4423
|
+
|
|
4424
|
+
export type BooksListData = z.infer<typeof BooksListSchema>;
|
|
4425
|
+
export type BooksListSettings = z.infer<typeof BooksListSettingsSchema>;
|
|
4426
|
+
|
|
4105
4427
|
END_OF_FILE_CONTENT
|
|
4106
4428
|
mkdir -p "src/components/empty-tenant"
|
|
4107
4429
|
echo "Creating src/components/empty-tenant/View.tsx..."
|
|
@@ -4155,6 +4477,123 @@ import { EmptyTenantSchema, EmptyTenantSettingsSchema } from './schema';
|
|
|
4155
4477
|
export type EmptyTenantData = z.infer<typeof EmptyTenantSchema>;
|
|
4156
4478
|
export type EmptyTenantSettings = z.infer<typeof EmptyTenantSettingsSchema>;
|
|
4157
4479
|
|
|
4480
|
+
END_OF_FILE_CONTENT
|
|
4481
|
+
mkdir -p "src/components/footer"
|
|
4482
|
+
echo "Creating src/components/footer/View.tsx..."
|
|
4483
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/footer/View.tsx"
|
|
4484
|
+
import type React from 'react';
|
|
4485
|
+
import type { FooterData, FooterSettings } from './types';
|
|
4486
|
+
|
|
4487
|
+
type FooterViewProps = {
|
|
4488
|
+
data: FooterData;
|
|
4489
|
+
settings?: FooterSettings;
|
|
4490
|
+
};
|
|
4491
|
+
|
|
4492
|
+
export function FooterView({ data, settings }: FooterViewProps) {
|
|
4493
|
+
const links = data.links ?? [];
|
|
4494
|
+
|
|
4495
|
+
return (
|
|
4496
|
+
<footer
|
|
4497
|
+
style={{
|
|
4498
|
+
'--local-bg': 'var(--background)',
|
|
4499
|
+
'--local-text': 'var(--foreground)',
|
|
4500
|
+
'--local-muted': 'var(--muted-foreground)',
|
|
4501
|
+
'--local-border': 'var(--border)',
|
|
4502
|
+
} as React.CSSProperties}
|
|
4503
|
+
className="border-t border-[var(--local-border)] bg-[var(--local-bg)] px-6 py-10 text-[var(--local-text)]"
|
|
4504
|
+
>
|
|
4505
|
+
<div className="mx-auto flex w-full max-w-5xl flex-col gap-8 md:flex-row md:items-end md:justify-between">
|
|
4506
|
+
<div className="max-w-md">
|
|
4507
|
+
<div className="flex items-center gap-3">
|
|
4508
|
+
{settings?.showLogo !== false && (
|
|
4509
|
+
<span className="flex h-9 w-9 items-center justify-center rounded-lg border border-[var(--local-border)] text-sm font-semibold">
|
|
4510
|
+
O
|
|
4511
|
+
</span>
|
|
4512
|
+
)}
|
|
4513
|
+
<p data-jp-field="brandText" className="text-lg font-semibold tracking-tight">
|
|
4514
|
+
{data.brandText}
|
|
4515
|
+
</p>
|
|
4516
|
+
</div>
|
|
4517
|
+
{data.description && (
|
|
4518
|
+
<p data-jp-field="description" className="mt-3 text-sm leading-6 text-[var(--local-muted)]">
|
|
4519
|
+
{data.description}
|
|
4520
|
+
</p>
|
|
4521
|
+
)}
|
|
4522
|
+
<p data-jp-field="copyright" className="mt-4 text-xs text-[var(--local-muted)]">
|
|
4523
|
+
{data.copyright}
|
|
4524
|
+
</p>
|
|
4525
|
+
</div>
|
|
4526
|
+
|
|
4527
|
+
<nav data-jp-field="links" className="flex flex-wrap gap-4 text-sm">
|
|
4528
|
+
{links.map((link) => (
|
|
4529
|
+
<a
|
|
4530
|
+
key={link.id}
|
|
4531
|
+
data-jp-item-id={link.id}
|
|
4532
|
+
data-jp-item-field="links"
|
|
4533
|
+
href={link.href}
|
|
4534
|
+
target={link.external ? '_blank' : undefined}
|
|
4535
|
+
rel={link.external ? 'noreferrer' : undefined}
|
|
4536
|
+
className="text-[var(--local-muted)] transition-colors hover:text-[var(--local-text)]"
|
|
4537
|
+
>
|
|
4538
|
+
{link.label}
|
|
4539
|
+
</a>
|
|
4540
|
+
))}
|
|
4541
|
+
{data.designSystemHref && (
|
|
4542
|
+
<a
|
|
4543
|
+
data-jp-field="designSystemHref"
|
|
4544
|
+
href={data.designSystemHref}
|
|
4545
|
+
className="text-[var(--local-muted)] transition-colors hover:text-[var(--local-text)]"
|
|
4546
|
+
>
|
|
4547
|
+
Design system
|
|
4548
|
+
</a>
|
|
4549
|
+
)}
|
|
4550
|
+
</nav>
|
|
4551
|
+
</div>
|
|
4552
|
+
</footer>
|
|
4553
|
+
);
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4556
|
+
END_OF_FILE_CONTENT
|
|
4557
|
+
echo "Creating src/components/footer/index.ts..."
|
|
4558
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/footer/index.ts"
|
|
4559
|
+
export * from './View';
|
|
4560
|
+
export * from './schema';
|
|
4561
|
+
export * from './types';
|
|
4562
|
+
|
|
4563
|
+
END_OF_FILE_CONTENT
|
|
4564
|
+
echo "Creating src/components/footer/schema.ts..."
|
|
4565
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/footer/schema.ts"
|
|
4566
|
+
import { z } from 'zod';
|
|
4567
|
+
import { BaseArrayItem, BaseSectionData } from '@olonjs/core';
|
|
4568
|
+
|
|
4569
|
+
export const FooterLinkSchema = BaseArrayItem.extend({
|
|
4570
|
+
id: z.string(),
|
|
4571
|
+
label: z.string().describe('ui:text'),
|
|
4572
|
+
href: z.string().describe('ui:text'),
|
|
4573
|
+
external: z.boolean().optional(),
|
|
4574
|
+
});
|
|
4575
|
+
|
|
4576
|
+
export const FooterSchema = BaseSectionData.extend({
|
|
4577
|
+
brandText: z.string().describe('ui:text'),
|
|
4578
|
+
description: z.string().optional().describe('ui:textarea'),
|
|
4579
|
+
copyright: z.string().describe('ui:text'),
|
|
4580
|
+
links: z.array(FooterLinkSchema).default([]).describe('ui:list'),
|
|
4581
|
+
designSystemHref: z.string().optional().describe('ui:text'),
|
|
4582
|
+
});
|
|
4583
|
+
|
|
4584
|
+
export const FooterSettingsSchema = z.object({
|
|
4585
|
+
showLogo: z.boolean().default(true),
|
|
4586
|
+
});
|
|
4587
|
+
|
|
4588
|
+
END_OF_FILE_CONTENT
|
|
4589
|
+
echo "Creating src/components/footer/types.ts..."
|
|
4590
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/footer/types.ts"
|
|
4591
|
+
import { z } from 'zod';
|
|
4592
|
+
import { FooterSchema, FooterSettingsSchema } from './schema';
|
|
4593
|
+
|
|
4594
|
+
export type FooterData = z.infer<typeof FooterSchema>;
|
|
4595
|
+
export type FooterSettings = z.infer<typeof FooterSettingsSchema>;
|
|
4596
|
+
|
|
4158
4597
|
END_OF_FILE_CONTENT
|
|
4159
4598
|
mkdir -p "src/components/form-demo"
|
|
4160
4599
|
echo "Creating src/components/form-demo/Untitled..."
|
|
@@ -8567,6 +9006,253 @@ export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
|
|
|
8567
9006
|
END_OF_FILE_CONTENT
|
|
8568
9007
|
# SKIP: src/components/ui/tooltip.tsx:Zone.Identifier is binary and cannot be embedded as text.
|
|
8569
9008
|
mkdir -p "src/data"
|
|
9009
|
+
mkdir -p "src/data/collections"
|
|
9010
|
+
mkdir -p "src/data/collections/libri"
|
|
9011
|
+
echo "Creating src/data/collections/libri/libri.json..."
|
|
9012
|
+
cat << 'END_OF_FILE_CONTENT' > "src/data/collections/libri/libri.json"
|
|
9013
|
+
{
|
|
9014
|
+
"1984": {
|
|
9015
|
+
"id": "1984",
|
|
9016
|
+
"title": "1984",
|
|
9017
|
+
"author": "Guido Seiero",
|
|
9018
|
+
"year": 1949,
|
|
9019
|
+
"genre": "Distopia",
|
|
9020
|
+
"summary": "Un regime totalitario controlla linguaggio, memoria e pensiero."
|
|
9021
|
+
},
|
|
9022
|
+
"il-nome-della-rosa": {
|
|
9023
|
+
"id": "il-nome-della-rosa",
|
|
9024
|
+
"title": "Il nome della rosa",
|
|
9025
|
+
"author": "Umberto Eco",
|
|
9026
|
+
"year": 1980,
|
|
9027
|
+
"genre": "Romanzo storico",
|
|
9028
|
+
"summary": "Un'indagine in un'abbazia medievale diventa una riflessione su conoscenza, potere e interpretazione."
|
|
9029
|
+
},
|
|
9030
|
+
"se-questo-e-un-uomo": {
|
|
9031
|
+
"id": "se-questo-e-un-uomo",
|
|
9032
|
+
"title": "Se questo e un uomo",
|
|
9033
|
+
"author": "Primo Levi",
|
|
9034
|
+
"year": 1947,
|
|
9035
|
+
"genre": "Memoria",
|
|
9036
|
+
"summary": "La testimonianza essenziale di Levi sull'esperienza del lager e sulla dignita umana."
|
|
9037
|
+
},
|
|
9038
|
+
"le-citta-invisibili": {
|
|
9039
|
+
"id": "le-citta-invisibili",
|
|
9040
|
+
"title": "Le citta invisibili",
|
|
9041
|
+
"author": "Italo Calvino",
|
|
9042
|
+
"year": 1972,
|
|
9043
|
+
"genre": "Letteratura fantastica",
|
|
9044
|
+
"summary": "Marco Polo racconta a Kublai Khan citta immaginarie che parlano di memoria, desiderio e linguaggio."
|
|
9045
|
+
},
|
|
9046
|
+
"il-gattopardo": {
|
|
9047
|
+
"id": "il-gattopardo",
|
|
9048
|
+
"title": "Il Gattopardo",
|
|
9049
|
+
"author": "Giuseppe Tomasi di Lampedusa",
|
|
9050
|
+
"year": 1958,
|
|
9051
|
+
"genre": "Romanzo storico",
|
|
9052
|
+
"summary": "Il tramonto dell'aristocrazia siciliana durante l'unificazione italiana."
|
|
9053
|
+
},
|
|
9054
|
+
"la-coscienza-di-zeno": {
|
|
9055
|
+
"id": "la-coscienza-di-zeno",
|
|
9056
|
+
"title": "La coscienza di Zeno",
|
|
9057
|
+
"author": "Italo Svevo",
|
|
9058
|
+
"year": 1923,
|
|
9059
|
+
"genre": "Romanzo psicologico",
|
|
9060
|
+
"summary": "Un diario ironico e nevrotico attraversa memoria, terapia e autoinganno."
|
|
9061
|
+
},
|
|
9062
|
+
"i-promessi-sposi": {
|
|
9063
|
+
"id": "i-promessi-sposi",
|
|
9064
|
+
"title": "I promessi sposi",
|
|
9065
|
+
"author": "Alessandro Manzoni",
|
|
9066
|
+
"year": 1842,
|
|
9067
|
+
"genre": "Classico",
|
|
9068
|
+
"summary": "La vicenda di Renzo e Lucia dentro carestia, guerra, peste e provvidenza."
|
|
9069
|
+
},
|
|
9070
|
+
"il-barone-rampante": {
|
|
9071
|
+
"id": "il-barone-rampante",
|
|
9072
|
+
"title": "Il barone rampante",
|
|
9073
|
+
"author": "Italo Calvino",
|
|
9074
|
+
"year": 1957,
|
|
9075
|
+
"genre": "Romanzo filosofico",
|
|
9076
|
+
"summary": "Cosimo sceglie di vivere sugli alberi e trasforma la distanza in una forma di liberta."
|
|
9077
|
+
},
|
|
9078
|
+
"gomorra": {
|
|
9079
|
+
"id": "gomorra",
|
|
9080
|
+
"title": "Gomorra",
|
|
9081
|
+
"author": "Roberto Saviano",
|
|
9082
|
+
"year": 2006,
|
|
9083
|
+
"genre": "Inchiesta",
|
|
9084
|
+
"summary": "Un reportage narrativo sulle economie e le violenze del sistema camorristico."
|
|
9085
|
+
},
|
|
9086
|
+
"oceano-mare": {
|
|
9087
|
+
"id": "oceano-mare",
|
|
9088
|
+
"title": "Oceano mare",
|
|
9089
|
+
"author": "Alessandro Baricco",
|
|
9090
|
+
"year": 1993,
|
|
9091
|
+
"genre": "Romanzo letterario",
|
|
9092
|
+
"summary": "Storie diverse si incontrano in una locanda sul mare, tra cura, naufragio e mistero."
|
|
9093
|
+
},
|
|
9094
|
+
"lessico-famigliare": {
|
|
9095
|
+
"id": "lessico-famigliare",
|
|
9096
|
+
"title": "Lessico famigliare",
|
|
9097
|
+
"author": "Natalia Ginzburg",
|
|
9098
|
+
"year": 1963,
|
|
9099
|
+
"genre": "Memoria narrativa",
|
|
9100
|
+
"summary": "Una famiglia prende forma attraverso parole, tic linguistici e memoria civile."
|
|
9101
|
+
},
|
|
9102
|
+
"cent-anni-di-solitudine": {
|
|
9103
|
+
"id": "cent-anni-di-solitudine",
|
|
9104
|
+
"title": "Cent'anni di solitudine",
|
|
9105
|
+
"author": "Gabriel Garcia Marquez",
|
|
9106
|
+
"year": 1967,
|
|
9107
|
+
"genre": "Realismo magico",
|
|
9108
|
+
"summary": "La saga dei Buendia e di Macondo intreccia mito, storia e destino."
|
|
9109
|
+
},
|
|
9110
|
+
"fahrenheit-451": {
|
|
9111
|
+
"id": "fahrenheit-451",
|
|
9112
|
+
"title": "Fahrenheit 451",
|
|
9113
|
+
"author": "Ray Bradbury",
|
|
9114
|
+
"year": 1953,
|
|
9115
|
+
"genre": "Distopia",
|
|
9116
|
+
"summary": "In un futuro dove i libri bruciano, leggere diventa un atto di resistenza."
|
|
9117
|
+
},
|
|
9118
|
+
"dune": {
|
|
9119
|
+
"id": "dune",
|
|
9120
|
+
"title": "Dune",
|
|
9121
|
+
"author": "Frank Herbert",
|
|
9122
|
+
"year": 1965,
|
|
9123
|
+
"genre": "Fantascienza",
|
|
9124
|
+
"summary": "Politica, ecologia e messianismo si scontrano sul pianeta desertico Arrakis."
|
|
9125
|
+
},
|
|
9126
|
+
"neuromancer": {
|
|
9127
|
+
"id": "neuromancer",
|
|
9128
|
+
"title": "Neuromancer",
|
|
9129
|
+
"author": "William Gibson",
|
|
9130
|
+
"year": 1984,
|
|
9131
|
+
"genre": "Cyberpunk",
|
|
9132
|
+
"summary": "Un hacker decaduto viene trascinato in un colpo che attraversa cyberspazio e intelligenze artificiali."
|
|
9133
|
+
},
|
|
9134
|
+
"il-signore-degli-anelli": {
|
|
9135
|
+
"id": "il-signore-degli-anelli",
|
|
9136
|
+
"title": "Il Signore degli Anelli",
|
|
9137
|
+
"author": "J. R. R. Tolkien",
|
|
9138
|
+
"year": 1954,
|
|
9139
|
+
"genre": "Fantasy",
|
|
9140
|
+
"summary": "La Compagnia affronta il potere dell'Anello in una delle grandi epopee moderne."
|
|
9141
|
+
},
|
|
9142
|
+
"harry-potter-e-la-pietra-filosofale": {
|
|
9143
|
+
"id": "harry-potter-e-la-pietra-filosofale",
|
|
9144
|
+
"title": "Harry Potter e la pietra filosofale",
|
|
9145
|
+
"author": "J. K. Rowling",
|
|
9146
|
+
"year": 1997,
|
|
9147
|
+
"genre": "Fantasy",
|
|
9148
|
+
"summary": "Un ragazzo scopre il mondo magico e il proprio posto in una storia piu grande."
|
|
9149
|
+
},
|
|
9150
|
+
"orgoglio-e-pregiudizio": {
|
|
9151
|
+
"id": "orgoglio-e-pregiudizio",
|
|
9152
|
+
"title": "Orgoglio e pregiudizio",
|
|
9153
|
+
"author": "Jane Austen",
|
|
9154
|
+
"year": 1813,
|
|
9155
|
+
"genre": "Classico",
|
|
9156
|
+
"summary": "Elizabeth Bennet e Mr. Darcy si misurano con classe, carattere e giudizio sociale."
|
|
9157
|
+
},
|
|
9158
|
+
"moby-dick": {
|
|
9159
|
+
"id": "moby-dick",
|
|
9160
|
+
"title": "Moby Dick",
|
|
9161
|
+
"author": "Herman Melville",
|
|
9162
|
+
"year": 1851,
|
|
9163
|
+
"genre": "Avventura",
|
|
9164
|
+
"summary": "La caccia alla balena bianca diventa ossessione metafisica e viaggio nell'abisso."
|
|
9165
|
+
},
|
|
9166
|
+
"delitto-e-castigo": {
|
|
9167
|
+
"id": "delitto-e-castigo",
|
|
9168
|
+
"title": "Delitto e castigo",
|
|
9169
|
+
"author": "Fedor Dostoevskij",
|
|
9170
|
+
"year": 1866,
|
|
9171
|
+
"genre": "Romanzo psicologico",
|
|
9172
|
+
"summary": "Raskolnikov attraversa colpa, febbre morale e possibilita di redenzione."
|
|
9173
|
+
},
|
|
9174
|
+
"anna-karenina": {
|
|
9175
|
+
"id": "anna-karenina",
|
|
9176
|
+
"title": "Anna Karenina",
|
|
9177
|
+
"author": "Lev Tolstoj",
|
|
9178
|
+
"year": 1877,
|
|
9179
|
+
"genre": "Classico",
|
|
9180
|
+
"summary": "Una storia d'amore e rovina dentro la societa russa dell'Ottocento."
|
|
9181
|
+
},
|
|
9182
|
+
"il-maestro-e-margherita": {
|
|
9183
|
+
"id": "il-maestro-e-margherita",
|
|
9184
|
+
"title": "Il maestro e Margherita",
|
|
9185
|
+
"author": "Michail Bulgakov",
|
|
9186
|
+
"year": 1967,
|
|
9187
|
+
"genre": "Satira fantastica",
|
|
9188
|
+
"summary": "Il diavolo visita Mosca in un romanzo visionario su arte, censura e amore."
|
|
9189
|
+
},
|
|
9190
|
+
"la-strada": {
|
|
9191
|
+
"id": "la-strada",
|
|
9192
|
+
"title": "La strada",
|
|
9193
|
+
"author": "Cormac McCarthy",
|
|
9194
|
+
"year": 2006,
|
|
9195
|
+
"genre": "Post-apocalittico",
|
|
9196
|
+
"summary": "Padre e figlio attraversano un mondo bruciato portando con se una fragile idea di bene."
|
|
9197
|
+
},
|
|
9198
|
+
"cloud-atlas": {
|
|
9199
|
+
"id": "cloud-atlas",
|
|
9200
|
+
"title": "Cloud Atlas",
|
|
9201
|
+
"author": "David Mitchell",
|
|
9202
|
+
"year": 2004,
|
|
9203
|
+
"genre": "Romanzo corale",
|
|
9204
|
+
"summary": "Sei storie in epoche diverse compongono una meditazione su potere, memoria e reincorrenza."
|
|
9205
|
+
},
|
|
9206
|
+
"kafka-sulla-spiaggia": {
|
|
9207
|
+
"id": "kafka-sulla-spiaggia",
|
|
9208
|
+
"title": "Kafka sulla spiaggia",
|
|
9209
|
+
"author": "Haruki Murakami",
|
|
9210
|
+
"year": 2002,
|
|
9211
|
+
"genre": "Surrealismo",
|
|
9212
|
+
"summary": "Fuga, destino e sogno si intrecciano in un romanzo sospeso tra reale e mitico."
|
|
9213
|
+
},
|
|
9214
|
+
"americanah": {
|
|
9215
|
+
"id": "americanah",
|
|
9216
|
+
"title": "Americanah",
|
|
9217
|
+
"author": "Chimamanda Ngozi Adichie",
|
|
9218
|
+
"year": 2013,
|
|
9219
|
+
"genre": "Romanzo contemporaneo",
|
|
9220
|
+
"summary": "Migrazione, razza e identita raccontate attraverso una storia d'amore tra Nigeria e Stati Uniti."
|
|
9221
|
+
},
|
|
9222
|
+
"la-casa-degli-spiriti": {
|
|
9223
|
+
"id": "la-casa-degli-spiriti",
|
|
9224
|
+
"title": "La casa degli spiriti",
|
|
9225
|
+
"author": "Isabel Allende",
|
|
9226
|
+
"year": 1982,
|
|
9227
|
+
"genre": "Saga familiare",
|
|
9228
|
+
"summary": "La storia della famiglia Trueba intreccia politica, memoria e realismo magico."
|
|
9229
|
+
},
|
|
9230
|
+
"beloved": {
|
|
9231
|
+
"id": "beloved",
|
|
9232
|
+
"title": "Beloved",
|
|
9233
|
+
"author": "Toni Morrison",
|
|
9234
|
+
"year": 1987,
|
|
9235
|
+
"genre": "Romanzo storico",
|
|
9236
|
+
"summary": "Il trauma della schiavitu ritorna come presenza viva nella casa di Sethe."
|
|
9237
|
+
},
|
|
9238
|
+
"la-breve-favolosa-vita-di-oscar-wao": {
|
|
9239
|
+
"id": "la-breve-favolosa-vita-di-oscar-wao",
|
|
9240
|
+
"title": "La breve favolosa vita di Oscar Wao",
|
|
9241
|
+
"author": "Junot Diaz",
|
|
9242
|
+
"year": 2007,
|
|
9243
|
+
"genre": "Romanzo contemporaneo",
|
|
9244
|
+
"summary": "Famiglia, diaspora dominicana e cultura pop si intrecciano nella storia di Oscar."
|
|
9245
|
+
},
|
|
9246
|
+
"le-correzioni": {
|
|
9247
|
+
"id": "le-correzioni",
|
|
9248
|
+
"title": "Le correzioni",
|
|
9249
|
+
"author": "Jonathan Franzen",
|
|
9250
|
+
"year": 2001,
|
|
9251
|
+
"genre": "Romanzo familiare",
|
|
9252
|
+
"summary": "Una famiglia americana tenta di ritrovarsi mentre ciascuno affronta le proprie fratture."
|
|
9253
|
+
}
|
|
9254
|
+
}
|
|
9255
|
+
END_OF_FILE_CONTENT
|
|
8570
9256
|
mkdir -p "src/data/config"
|
|
8571
9257
|
echo "Creating src/data/config/menu.json..."
|
|
8572
9258
|
cat << 'END_OF_FILE_CONTENT' > "src/data/config/menu.json"
|
|
@@ -8592,6 +9278,24 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/menu.json"
|
|
|
8592
9278
|
"label": "GitHub",
|
|
8593
9279
|
"href": "https://github.com/olonjs/core"
|
|
8594
9280
|
}
|
|
9281
|
+
],
|
|
9282
|
+
"footer": [
|
|
9283
|
+
{
|
|
9284
|
+
"id": "footer-home",
|
|
9285
|
+
"label": "Home",
|
|
9286
|
+
"href": "/"
|
|
9287
|
+
},
|
|
9288
|
+
{
|
|
9289
|
+
"id": "footer-libri",
|
|
9290
|
+
"label": "Libri",
|
|
9291
|
+
"href": "/libri"
|
|
9292
|
+
},
|
|
9293
|
+
{
|
|
9294
|
+
"id": "footer-github",
|
|
9295
|
+
"label": "GitHub",
|
|
9296
|
+
"href": "https://github.com/olonjs/core",
|
|
9297
|
+
"external": true
|
|
9298
|
+
}
|
|
8595
9299
|
]
|
|
8596
9300
|
}
|
|
8597
9301
|
END_OF_FILE_CONTENT
|
|
@@ -8647,7 +9351,6 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/site.json"
|
|
|
8647
9351
|
"title": "OlonJS",
|
|
8648
9352
|
"logoUrl": "/brand/mark/olon-mark-dark.svg"
|
|
8649
9353
|
},
|
|
8650
|
-
|
|
8651
9354
|
"header": {
|
|
8652
9355
|
"id": "global-header",
|
|
8653
9356
|
"type": "header",
|
|
@@ -8686,13 +9389,11 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/site.json"
|
|
|
8686
9389
|
"type": "footer",
|
|
8687
9390
|
"data": {
|
|
8688
9391
|
"brandText": "OlonJS",
|
|
9392
|
+
"description": "AI-native content infrastructure for deterministic, sovereign, git-backed sites.",
|
|
8689
9393
|
"copyright": "© 2026 OlonJS · v1.5 · Guido Serio",
|
|
8690
|
-
"links":
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
"href": "https://github.com/olonjs/core"
|
|
8694
|
-
}
|
|
8695
|
-
],
|
|
9394
|
+
"links": {
|
|
9395
|
+
"$ref": "../config/menu.json#/footer"
|
|
9396
|
+
},
|
|
8696
9397
|
"designSystemHref": ""
|
|
8697
9398
|
},
|
|
8698
9399
|
"settings": {
|
|
@@ -8896,6 +9597,65 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
|
8896
9597
|
"global-header": false
|
|
8897
9598
|
}
|
|
8898
9599
|
END_OF_FILE_CONTENT
|
|
9600
|
+
mkdir -p "src/data/pages/libri"
|
|
9601
|
+
echo "Creating src/data/pages/libri.json..."
|
|
9602
|
+
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/libri.json"
|
|
9603
|
+
{
|
|
9604
|
+
"id": "libri-page",
|
|
9605
|
+
"slug": "libri",
|
|
9606
|
+
"meta": {
|
|
9607
|
+
"title": "Libri",
|
|
9608
|
+
"description": "Catalogo libri dimostrativo alimentato da COP collections."
|
|
9609
|
+
},
|
|
9610
|
+
"sections": [
|
|
9611
|
+
{
|
|
9612
|
+
"id": "books-list-1",
|
|
9613
|
+
"type": "books-list",
|
|
9614
|
+
"data": {
|
|
9615
|
+
"anchorId": "catalogo-libri",
|
|
9616
|
+
"eyebrow": "Collection demo",
|
|
9617
|
+
"title": "Libri",
|
|
9618
|
+
"description": "Una pagina collection con 30 titoli, paginazione lato componente e link alle schede dinamiche.",
|
|
9619
|
+
"items": {
|
|
9620
|
+
"$ref": "../collections/libri/libri.json"
|
|
9621
|
+
},
|
|
9622
|
+
"pageSize": 10
|
|
9623
|
+
}
|
|
9624
|
+
}
|
|
9625
|
+
],
|
|
9626
|
+
"global-header": false
|
|
9627
|
+
}
|
|
9628
|
+
|
|
9629
|
+
END_OF_FILE_CONTENT
|
|
9630
|
+
echo "Creating src/data/pages/libri/[slug].json..."
|
|
9631
|
+
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/libri/[slug].json"
|
|
9632
|
+
{
|
|
9633
|
+
"id": "libro-detail-page",
|
|
9634
|
+
"slug": "libri/[slug]",
|
|
9635
|
+
"meta": {
|
|
9636
|
+
"title": "Dettaglio libro",
|
|
9637
|
+
"description": "Pagina dinamica dettaglio libro alimentata dalla collection libri."
|
|
9638
|
+
},
|
|
9639
|
+
"sections": [
|
|
9640
|
+
{
|
|
9641
|
+
"id": "book-detail-1",
|
|
9642
|
+
"type": "book-detail",
|
|
9643
|
+
"data": {
|
|
9644
|
+
"anchorId": "dettaglio-libro",
|
|
9645
|
+
"item": {
|
|
9646
|
+
"$ref": "collection:current"
|
|
9647
|
+
},
|
|
9648
|
+
"backLabel": "← Torna ai libri"
|
|
9649
|
+
}
|
|
9650
|
+
}
|
|
9651
|
+
],
|
|
9652
|
+
"collection": {
|
|
9653
|
+
"source": "libri",
|
|
9654
|
+
"paramKey": "slug"
|
|
9655
|
+
},
|
|
9656
|
+
"global-header": false
|
|
9657
|
+
}
|
|
9658
|
+
END_OF_FILE_CONTENT
|
|
8899
9659
|
mkdir -p "src/emails"
|
|
8900
9660
|
echo "Creating src/emails/LeadNotificationEmail.tsx..."
|
|
8901
9661
|
cat << 'END_OF_FILE_CONTENT' > "src/emails/LeadNotificationEmail.tsx"
|
|
@@ -9213,12 +9973,19 @@ echo "Creating src/entry-ssg.tsx..."
|
|
|
9213
9973
|
cat << 'END_OF_FILE_CONTENT' > "src/entry-ssg.tsx"
|
|
9214
9974
|
import { renderToString } from 'react-dom/server';
|
|
9215
9975
|
import { StaticRouter } from 'react-router-dom/server';
|
|
9216
|
-
import {
|
|
9976
|
+
import {
|
|
9977
|
+
ConfigProvider,
|
|
9978
|
+
PageRenderer,
|
|
9979
|
+
StudioProvider,
|
|
9980
|
+
contract,
|
|
9981
|
+
resolvePageMatchFromRegistry,
|
|
9982
|
+
resolveRuntimeConfig,
|
|
9983
|
+
} from '@olonjs/core';
|
|
9217
9984
|
import type { JsonPagesConfig, PageConfig, SiteConfig, ThemeConfig } from '@/types';
|
|
9218
9985
|
import { ThemeProvider } from '@/components/ThemeProvider';
|
|
9219
9986
|
import { ComponentRegistry } from '@/lib/ComponentRegistry';
|
|
9220
9987
|
import { SECTION_SCHEMAS } from '@/lib/schemas';
|
|
9221
|
-
import { menuConfig, pages, refDocuments, siteConfig, themeConfig } from '@/runtime';
|
|
9988
|
+
import { collectionSchemas, collections, menuConfig, pages, refDocuments, siteConfig, themeConfig } from '@/runtime';
|
|
9222
9989
|
import tenantCss from '@/index.css?inline';
|
|
9223
9990
|
|
|
9224
9991
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -9233,10 +10000,16 @@ function getSortedSlugs(): string[] {
|
|
|
9233
10000
|
return Object.keys(pages).sort((a, b) => a.localeCompare(b));
|
|
9234
10001
|
}
|
|
9235
10002
|
|
|
9236
|
-
function resolvePage(slug: string): { slug: string; page: PageConfig } {
|
|
10003
|
+
function resolvePage(slug: string): { slug: string; registrySlug: string; page: PageConfig; params: Record<string, string> } {
|
|
9237
10004
|
const normalized = normalizeSlug(slug);
|
|
9238
|
-
|
|
9239
|
-
|
|
10005
|
+
const pageMatch = resolvePageMatchFromRegistry(pages, normalized);
|
|
10006
|
+
if (pageMatch) {
|
|
10007
|
+
return {
|
|
10008
|
+
slug: normalized || pageMatch.registrySlug,
|
|
10009
|
+
registrySlug: pageMatch.registrySlug,
|
|
10010
|
+
page: pageMatch.page,
|
|
10011
|
+
params: pageMatch.params,
|
|
10012
|
+
};
|
|
9240
10013
|
}
|
|
9241
10014
|
|
|
9242
10015
|
const slugs = getSortedSlugs();
|
|
@@ -9246,7 +10019,7 @@ function resolvePage(slug: string): { slug: string; page: PageConfig } {
|
|
|
9246
10019
|
|
|
9247
10020
|
const home = slugs.find((item) => item === 'home');
|
|
9248
10021
|
const fallbackSlug = home ?? slugs[0];
|
|
9249
|
-
return { slug: fallbackSlug, page: pages[fallbackSlug] };
|
|
10022
|
+
return { slug: fallbackSlug, registrySlug: fallbackSlug, page: pages[fallbackSlug], params: {} };
|
|
9250
10023
|
}
|
|
9251
10024
|
|
|
9252
10025
|
function flattenThemeTokens(
|
|
@@ -9334,14 +10107,18 @@ function resolveTenantId(): string {
|
|
|
9334
10107
|
export function render(slug: string): string {
|
|
9335
10108
|
const resolved = resolvePage(slug);
|
|
9336
10109
|
const location = resolved.slug === 'home' ? '/' : `/${resolved.slug}`;
|
|
10110
|
+
const collectionContext = contract.resolveCollectionContext(resolved.page, resolved.params, collections);
|
|
9337
10111
|
const resolvedRuntime = resolveRuntimeConfig({
|
|
9338
|
-
pages,
|
|
10112
|
+
pages: { [resolved.registrySlug]: resolved.page },
|
|
9339
10113
|
siteConfig,
|
|
9340
10114
|
themeConfig,
|
|
9341
10115
|
menuConfig,
|
|
10116
|
+
collections,
|
|
10117
|
+
collectionSchemas,
|
|
10118
|
+
collectionContext,
|
|
9342
10119
|
refDocuments,
|
|
9343
10120
|
});
|
|
9344
|
-
const resolvedPage = resolvedRuntime.pages[resolved.
|
|
10121
|
+
const resolvedPage = resolvedRuntime.pages[resolved.registrySlug] ?? resolved.page;
|
|
9345
10122
|
|
|
9346
10123
|
return renderToString(
|
|
9347
10124
|
<StaticRouter location={location}>
|
|
@@ -9391,12 +10168,22 @@ export function getPageMeta(slug: string): { title: string; description: string
|
|
|
9391
10168
|
export function getWebMcpBuildState(): {
|
|
9392
10169
|
pages: Record<string, PageConfig>;
|
|
9393
10170
|
schemas: JsonPagesConfig['schemas'];
|
|
10171
|
+
collections: JsonPagesConfig['collections'];
|
|
10172
|
+
collectionSchemas: JsonPagesConfig['collectionSchemas'];
|
|
9394
10173
|
siteConfig: SiteConfig;
|
|
10174
|
+
themeConfig: ThemeConfig;
|
|
10175
|
+
menuConfig: JsonPagesConfig['menuConfig'];
|
|
10176
|
+
refDocuments: JsonPagesConfig['refDocuments'];
|
|
9395
10177
|
} {
|
|
9396
10178
|
return {
|
|
9397
10179
|
pages,
|
|
9398
10180
|
schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
|
|
10181
|
+
collections,
|
|
10182
|
+
collectionSchemas,
|
|
9399
10183
|
siteConfig,
|
|
10184
|
+
themeConfig,
|
|
10185
|
+
menuConfig,
|
|
10186
|
+
refDocuments,
|
|
9400
10187
|
};
|
|
9401
10188
|
}
|
|
9402
10189
|
|
|
@@ -10344,17 +11131,34 @@ cat << 'END_OF_FILE_CONTENT' > "src/index.css"
|
|
|
10344
11131
|
|
|
10345
11132
|
END_OF_FILE_CONTENT
|
|
10346
11133
|
mkdir -p "src/lib"
|
|
11134
|
+
echo "Creating src/lib/CollectionRegistry.ts..."
|
|
11135
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/CollectionRegistry.ts"
|
|
11136
|
+
import { LibriCollectionSchema } from '@/collections/libri';
|
|
11137
|
+
|
|
11138
|
+
export const CollectionRegistry = {
|
|
11139
|
+
libri: LibriCollectionSchema,
|
|
11140
|
+
} as const;
|
|
11141
|
+
|
|
11142
|
+
export type CollectionType = keyof typeof CollectionRegistry;
|
|
11143
|
+
|
|
11144
|
+
END_OF_FILE_CONTENT
|
|
10347
11145
|
echo "Creating src/lib/ComponentRegistry.tsx..."
|
|
10348
11146
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/ComponentRegistry.tsx"
|
|
10349
11147
|
import type { SectionType } from '@/types';
|
|
10350
11148
|
import type { SectionComponentPropsMap } from '@/types';
|
|
11149
|
+
import { BookDetailView } from '@/components/book-detail';
|
|
11150
|
+
import { BooksListView } from '@/components/books-list';
|
|
10351
11151
|
import { EmptyTenantView } from '@/components/empty-tenant';
|
|
11152
|
+
import { FooterView } from '@/components/footer';
|
|
10352
11153
|
import { FormDemoView } from '@/components/form-demo';
|
|
10353
11154
|
|
|
10354
11155
|
export const ComponentRegistry: {
|
|
10355
11156
|
[K in SectionType]: React.FC<SectionComponentPropsMap[K]>;
|
|
10356
11157
|
} = {
|
|
11158
|
+
'book-detail': BookDetailView as React.FC<SectionComponentPropsMap['book-detail']>,
|
|
11159
|
+
'books-list': BooksListView as React.FC<SectionComponentPropsMap['books-list']>,
|
|
10357
11160
|
'empty-tenant': EmptyTenantView as React.FC<SectionComponentPropsMap['empty-tenant']>,
|
|
11161
|
+
footer: FooterView as React.FC<SectionComponentPropsMap['footer']>,
|
|
10358
11162
|
'form-demo': FormDemoView as React.FC<SectionComponentPropsMap['form-demo']>,
|
|
10359
11163
|
};
|
|
10360
11164
|
|
|
@@ -10423,20 +11227,44 @@ echo "Creating src/lib/addSectionConfig.ts..."
|
|
|
10423
11227
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/addSectionConfig.ts"
|
|
10424
11228
|
import type { AddSectionConfig } from '@olonjs/core';
|
|
10425
11229
|
|
|
10426
|
-
const addableSectionTypes = ['empty-tenant', 'form-demo'] as const;
|
|
11230
|
+
const addableSectionTypes = ['book-detail', 'books-list', 'empty-tenant', 'footer', 'form-demo'] as const;
|
|
10427
11231
|
|
|
10428
11232
|
const sectionTypeLabels: Record<string, string> = {
|
|
11233
|
+
'book-detail': 'Book Detail',
|
|
11234
|
+
'books-list': 'Books List',
|
|
10429
11235
|
'empty-tenant': 'Empty Tenant',
|
|
11236
|
+
footer: 'Footer',
|
|
10430
11237
|
'form-demo': 'Form Demo',
|
|
10431
11238
|
};
|
|
10432
11239
|
|
|
10433
11240
|
function getDefaultSectionData(type: string): Record<string, unknown> {
|
|
10434
11241
|
switch (type) {
|
|
11242
|
+
case 'book-detail':
|
|
11243
|
+
return {
|
|
11244
|
+
item: { $ref: 'collection:current' },
|
|
11245
|
+
backLabel: 'Torna ai libri',
|
|
11246
|
+
};
|
|
11247
|
+
case 'books-list':
|
|
11248
|
+
return {
|
|
11249
|
+
eyebrow: 'Collection demo',
|
|
11250
|
+
title: 'Libri',
|
|
11251
|
+
description: 'Catalogo dimostrativo alimentato dalla collection libri.',
|
|
11252
|
+
items: { $ref: '../collections/libri/libri.json' },
|
|
11253
|
+
pageSize: 10,
|
|
11254
|
+
};
|
|
10435
11255
|
case 'empty-tenant':
|
|
10436
11256
|
return {
|
|
10437
11257
|
title: 'Your tenant is empty.',
|
|
10438
11258
|
description: 'Create your first page to start building your site.',
|
|
10439
11259
|
};
|
|
11260
|
+
case 'footer':
|
|
11261
|
+
return {
|
|
11262
|
+
brandText: 'OlonJS',
|
|
11263
|
+
description: 'AI-native content infrastructure for deterministic, git-backed sites.',
|
|
11264
|
+
copyright: '© 2026 OlonJS',
|
|
11265
|
+
links: [],
|
|
11266
|
+
designSystemHref: '',
|
|
11267
|
+
};
|
|
10440
11268
|
case 'form-demo':
|
|
10441
11269
|
return {
|
|
10442
11270
|
title: 'Contattaci',
|
|
@@ -10535,11 +11363,17 @@ export function getFilePages(): Record<string, PageConfig> {
|
|
|
10535
11363
|
END_OF_FILE_CONTENT
|
|
10536
11364
|
echo "Creating src/lib/schemas.ts..."
|
|
10537
11365
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/schemas.ts"
|
|
11366
|
+
import { BookDetailSchema } from '@/components/book-detail';
|
|
11367
|
+
import { BooksListSchema } from '@/components/books-list';
|
|
10538
11368
|
import { EmptyTenantSchema } from '@/components/empty-tenant';
|
|
11369
|
+
import { FooterSchema } from '@/components/footer';
|
|
10539
11370
|
import { FormDemoSchema, FormDemoSubmissionSchema } from '@/components/form-demo';
|
|
10540
11371
|
|
|
10541
11372
|
export const SECTION_SCHEMAS = {
|
|
11373
|
+
'book-detail': BookDetailSchema,
|
|
11374
|
+
'books-list': BooksListSchema,
|
|
10542
11375
|
'empty-tenant': EmptyTenantSchema,
|
|
11376
|
+
footer: FooterSchema,
|
|
10543
11377
|
'form-demo': FormDemoSchema,
|
|
10544
11378
|
} as const;
|
|
10545
11379
|
|
|
@@ -10559,6 +11393,7 @@ export type SectionType = keyof typeof SECTION_SCHEMAS;
|
|
|
10559
11393
|
export {
|
|
10560
11394
|
BaseSectionData,
|
|
10561
11395
|
BaseArrayItem,
|
|
11396
|
+
BaseCollectionItem,
|
|
10562
11397
|
BaseSectionSettingsSchema,
|
|
10563
11398
|
CtaSchema,
|
|
10564
11399
|
ImageSelectionSchema,
|
|
@@ -10817,16 +11652,22 @@ END_OF_FILE_CONTENT
|
|
|
10817
11652
|
echo "Creating src/runtime.ts..."
|
|
10818
11653
|
cat << 'END_OF_FILE_CONTENT' > "src/runtime.ts"
|
|
10819
11654
|
import type { JsonPagesConfig, MenuConfig, PageConfig, SiteConfig, ThemeConfig } from '@/types';
|
|
11655
|
+
import { CollectionRegistry } from '@/lib/CollectionRegistry';
|
|
10820
11656
|
import { SECTION_SCHEMAS } from '@/lib/schemas';
|
|
10821
11657
|
import { getFilePages } from '@/lib/getFilePages';
|
|
10822
11658
|
import siteData from '@/data/config/site.json';
|
|
10823
11659
|
import menuData from '@/data/config/menu.json';
|
|
10824
11660
|
import themeData from '@/data/config/theme.json';
|
|
11661
|
+
import libriData from '@/data/collections/libri/libri.json';
|
|
10825
11662
|
|
|
10826
11663
|
export const siteConfig = siteData as unknown as SiteConfig;
|
|
10827
11664
|
export const themeConfig = themeData as unknown as ThemeConfig;
|
|
10828
11665
|
export const menuConfig = menuData as unknown as MenuConfig;
|
|
10829
11666
|
export const pages = getFilePages();
|
|
11667
|
+
export const collections = {
|
|
11668
|
+
libri: libriData as unknown as Record<string, unknown>,
|
|
11669
|
+
} satisfies NonNullable<JsonPagesConfig['collections']>;
|
|
11670
|
+
export const collectionSchemas = CollectionRegistry as unknown as JsonPagesConfig['collectionSchemas'];
|
|
10830
11671
|
export const refDocuments = {
|
|
10831
11672
|
'menu.json': menuConfig,
|
|
10832
11673
|
'config/menu.json': menuConfig,
|
|
@@ -10836,12 +11677,22 @@ export const refDocuments = {
|
|
|
10836
11677
|
export function getWebMcpBuildState(): {
|
|
10837
11678
|
pages: Record<string, PageConfig>;
|
|
10838
11679
|
schemas: JsonPagesConfig['schemas'];
|
|
11680
|
+
collectionSchemas: JsonPagesConfig['collectionSchemas'];
|
|
11681
|
+
collections: JsonPagesConfig['collections'];
|
|
10839
11682
|
siteConfig: SiteConfig;
|
|
11683
|
+
themeConfig: ThemeConfig;
|
|
11684
|
+
menuConfig: MenuConfig;
|
|
11685
|
+
refDocuments: JsonPagesConfig['refDocuments'];
|
|
10840
11686
|
} {
|
|
10841
11687
|
return {
|
|
10842
11688
|
pages,
|
|
10843
11689
|
schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
|
|
11690
|
+
collectionSchemas,
|
|
11691
|
+
collections,
|
|
10844
11692
|
siteConfig,
|
|
11693
|
+
themeConfig,
|
|
11694
|
+
menuConfig,
|
|
11695
|
+
refDocuments,
|
|
10845
11696
|
};
|
|
10846
11697
|
}
|
|
10847
11698
|
|
|
@@ -10849,23 +11700,39 @@ END_OF_FILE_CONTENT
|
|
|
10849
11700
|
# SKIP: src/runtime.ts:Zone.Identifier is binary and cannot be embedded as text.
|
|
10850
11701
|
echo "Creating src/types.ts..."
|
|
10851
11702
|
cat << 'END_OF_FILE_CONTENT' > "src/types.ts"
|
|
11703
|
+
import type { BookDetailData, BookDetailSettings } from '@/components/book-detail';
|
|
11704
|
+
import type { BooksListData, BooksListSettings } from '@/components/books-list';
|
|
10852
11705
|
import type { EmptyTenantData, EmptyTenantSettings } from '@/components/empty-tenant';
|
|
11706
|
+
import type { FooterData, FooterSettings } from '@/components/footer';
|
|
10853
11707
|
import type { FormDemoData, FormDemoSettings } from '@/components/form-demo';
|
|
11708
|
+
import type { Libro } from '@/collections/libri';
|
|
10854
11709
|
|
|
10855
11710
|
export type SectionComponentPropsMap = {
|
|
11711
|
+
'book-detail': { data: BookDetailData; settings?: BookDetailSettings };
|
|
11712
|
+
'books-list': { data: BooksListData; settings?: BooksListSettings };
|
|
10856
11713
|
'empty-tenant': { data: EmptyTenantData; settings?: EmptyTenantSettings };
|
|
11714
|
+
footer: { data: FooterData; settings?: FooterSettings };
|
|
10857
11715
|
'form-demo': { data: FormDemoData; settings?: FormDemoSettings };
|
|
10858
11716
|
};
|
|
10859
11717
|
|
|
10860
11718
|
declare module '@olonjs/core' {
|
|
10861
11719
|
export interface SectionDataRegistry {
|
|
11720
|
+
'book-detail': BookDetailData;
|
|
11721
|
+
'books-list': BooksListData;
|
|
10862
11722
|
'empty-tenant': EmptyTenantData;
|
|
11723
|
+
footer: FooterData;
|
|
10863
11724
|
'form-demo': FormDemoData;
|
|
10864
11725
|
}
|
|
10865
11726
|
export interface SectionSettingsRegistry {
|
|
11727
|
+
'book-detail': BookDetailSettings;
|
|
11728
|
+
'books-list': BooksListSettings;
|
|
10866
11729
|
'empty-tenant': EmptyTenantSettings;
|
|
11730
|
+
footer: FooterSettings;
|
|
10867
11731
|
'form-demo': FormDemoSettings;
|
|
10868
11732
|
}
|
|
11733
|
+
export interface CollectionItemRegistry {
|
|
11734
|
+
libri: Libro;
|
|
11735
|
+
}
|
|
10869
11736
|
}
|
|
10870
11737
|
|
|
10871
11738
|
export * from '@olonjs/core';
|
|
@@ -11027,6 +11894,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
11027
11894
|
const ASSETS_IMAGES_DIR = path.resolve(__dirname, 'public', 'assets', 'images');
|
|
11028
11895
|
const DATA_CONFIG_DIR = path.resolve(__dirname, 'src', 'data', 'config');
|
|
11029
11896
|
const DATA_PAGES_DIR = path.resolve(__dirname, 'src', 'data', 'pages');
|
|
11897
|
+
const DATA_COLLECTIONS_DIR = path.resolve(__dirname, 'src', 'data', 'collections');
|
|
11030
11898
|
const IMAGE_EXT = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.avif']);
|
|
11031
11899
|
const IMAGE_MIMES = new Set([
|
|
11032
11900
|
'image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/svg+xml', 'image/avif',
|
|
@@ -11063,6 +11931,20 @@ function sendJsonFile(res, filePath) {
|
|
|
11063
11931
|
}
|
|
11064
11932
|
}
|
|
11065
11933
|
|
|
11934
|
+
function safeDataSlugPath(rootDir, rawSlug, fallback) {
|
|
11935
|
+
const slug = String(rawSlug || fallback)
|
|
11936
|
+
.replace(/\\/g, '/')
|
|
11937
|
+
.replace(/^\/+|\/+$/g, '');
|
|
11938
|
+
const segments = slug
|
|
11939
|
+
.split('/')
|
|
11940
|
+
.map((segment) => segment.replace(/[^a-zA-Z0-9_[\]-]/g, '_'))
|
|
11941
|
+
.filter(Boolean);
|
|
11942
|
+
const candidate = path.resolve(rootDir, `${segments.join(path.sep) || fallback}.json`);
|
|
11943
|
+
const isInsideRoot = candidate.startsWith(`${rootDir}${path.sep}`) || candidate === rootDir;
|
|
11944
|
+
if (!isInsideRoot) throw new Error('Invalid data path');
|
|
11945
|
+
return candidate;
|
|
11946
|
+
}
|
|
11947
|
+
|
|
11066
11948
|
function isTenantPageJsonRequest(req, pathname) {
|
|
11067
11949
|
if (req.method !== 'GET' || !pathname.endsWith('.json')) return false;
|
|
11068
11950
|
const viteOrStaticPrefixes = ['/api/', '/assets/', '/src/', '/node_modules/', '/public/', '/@'];
|
|
@@ -11120,15 +12002,23 @@ export default defineConfig({
|
|
|
11120
12002
|
const pageManifestMatch = pathname.match(/^\/mcp-manifests\/(.+)\.json$/i);
|
|
11121
12003
|
if (pageManifestMatch && req.method === 'GET') {
|
|
11122
12004
|
const slug = normalizeManifestSlug(pageManifestMatch[1]);
|
|
11123
|
-
const
|
|
11124
|
-
|
|
12005
|
+
const resolved = core.resolvePublicPageDocument({
|
|
12006
|
+
slug,
|
|
12007
|
+
pages: buildState.pages,
|
|
12008
|
+
siteConfig: buildState.siteConfig,
|
|
12009
|
+
themeConfig: buildState.themeConfig,
|
|
12010
|
+
menuConfig: buildState.menuConfig,
|
|
12011
|
+
collections: buildState.collections,
|
|
12012
|
+
refDocuments: buildState.refDocuments,
|
|
12013
|
+
});
|
|
12014
|
+
if (!resolved) {
|
|
11125
12015
|
sendJson(res, 404, { error: 'Page manifest not found' });
|
|
11126
12016
|
return true;
|
|
11127
12017
|
}
|
|
11128
12018
|
|
|
11129
12019
|
sendJson(res, 200, buildPageManifest({
|
|
11130
12020
|
slug,
|
|
11131
|
-
pageConfig,
|
|
12021
|
+
pageConfig: resolved.page,
|
|
11132
12022
|
schemas: buildState.schemas,
|
|
11133
12023
|
submissionSchemas: buildState.submissionSchemas,
|
|
11134
12024
|
siteConfig: buildState.siteConfig,
|
|
@@ -11139,15 +12029,23 @@ export default defineConfig({
|
|
|
11139
12029
|
const schemaMatch = pathname.match(/^\/schemas\/(.+)\.schema\.json$/i);
|
|
11140
12030
|
if (schemaMatch && req.method === 'GET') {
|
|
11141
12031
|
const slug = normalizeManifestSlug(schemaMatch[1]);
|
|
11142
|
-
const
|
|
11143
|
-
|
|
12032
|
+
const resolved = core.resolvePublicPageDocument({
|
|
12033
|
+
slug,
|
|
12034
|
+
pages: buildState.pages,
|
|
12035
|
+
siteConfig: buildState.siteConfig,
|
|
12036
|
+
themeConfig: buildState.themeConfig,
|
|
12037
|
+
menuConfig: buildState.menuConfig,
|
|
12038
|
+
collections: buildState.collections,
|
|
12039
|
+
refDocuments: buildState.refDocuments,
|
|
12040
|
+
});
|
|
12041
|
+
if (!resolved) {
|
|
11144
12042
|
sendJson(res, 404, { error: 'Schema contract not found' });
|
|
11145
12043
|
return true;
|
|
11146
12044
|
}
|
|
11147
12045
|
|
|
11148
12046
|
sendJson(res, 200, buildPageContract({
|
|
11149
12047
|
slug,
|
|
11150
|
-
pageConfig,
|
|
12048
|
+
pageConfig: resolved.page,
|
|
11151
12049
|
schemas: buildState.schemas,
|
|
11152
12050
|
submissionSchemas: buildState.submissionSchemas,
|
|
11153
12051
|
siteConfig: buildState.siteConfig,
|
|
@@ -11188,13 +12086,31 @@ export default defineConfig({
|
|
|
11188
12086
|
.replace(/^pages\//i, '')
|
|
11189
12087
|
.replace(/\.json$/i, '')
|
|
11190
12088
|
.replace(/^\/+|\/+$/g, '');
|
|
11191
|
-
|
|
11192
|
-
const isInsidePagesDir = candidate.startsWith(`${DATA_PAGES_DIR}${path.sep}`) || candidate === DATA_PAGES_DIR;
|
|
11193
|
-
if (!slug || !isInsidePagesDir || !fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) {
|
|
12089
|
+
if (!slug) {
|
|
11194
12090
|
sendJson(res, 404, { error: 'Page JSON not found' });
|
|
11195
12091
|
return;
|
|
11196
12092
|
}
|
|
11197
|
-
|
|
12093
|
+
void (async () => {
|
|
12094
|
+
const core = await loadWebMcpBuilders();
|
|
12095
|
+
const runtime = await server.ssrLoadModule('/src/runtime.ts');
|
|
12096
|
+
const buildState = runtime.getWebMcpBuildState();
|
|
12097
|
+
const resolved = core.resolvePublicPageDocument({
|
|
12098
|
+
slug,
|
|
12099
|
+
pages: buildState.pages,
|
|
12100
|
+
siteConfig: buildState.siteConfig,
|
|
12101
|
+
themeConfig: buildState.themeConfig,
|
|
12102
|
+
menuConfig: buildState.menuConfig,
|
|
12103
|
+
collections: buildState.collections,
|
|
12104
|
+
refDocuments: buildState.refDocuments,
|
|
12105
|
+
});
|
|
12106
|
+
if (!resolved) {
|
|
12107
|
+
sendJson(res, 404, { error: 'Page JSON not found' });
|
|
12108
|
+
return;
|
|
12109
|
+
}
|
|
12110
|
+
sendJson(res, 200, resolved.page);
|
|
12111
|
+
})().catch((error) => {
|
|
12112
|
+
sendJson(res, 500, { error: error?.message || 'Page JSON resolution failed' });
|
|
12113
|
+
});
|
|
11198
12114
|
return;
|
|
11199
12115
|
}
|
|
11200
12116
|
if (req.method === 'GET' && req.url === '/api/list-assets') {
|
|
@@ -11213,12 +12129,23 @@ export default defineConfig({
|
|
|
11213
12129
|
if (!projectState || typeof slug !== 'string') { sendJson(res, 400, { error: 'Missing projectState or slug' }); return; }
|
|
11214
12130
|
if (!fs.existsSync(DATA_CONFIG_DIR)) fs.mkdirSync(DATA_CONFIG_DIR, { recursive: true });
|
|
11215
12131
|
if (!fs.existsSync(DATA_PAGES_DIR)) fs.mkdirSync(DATA_PAGES_DIR, { recursive: true });
|
|
12132
|
+
if (!fs.existsSync(DATA_COLLECTIONS_DIR)) fs.mkdirSync(DATA_COLLECTIONS_DIR, { recursive: true });
|
|
11216
12133
|
if (projectState.site != null) fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'site.json'), JSON.stringify(projectState.site, null, 2), 'utf8');
|
|
11217
12134
|
if (projectState.menu != null) fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'menu.json'), JSON.stringify(projectState.menu, null, 2), 'utf8');
|
|
11218
12135
|
if (projectState.theme != null) fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'theme.json'), JSON.stringify(projectState.theme, null, 2), 'utf8');
|
|
11219
12136
|
if (projectState.page != null) {
|
|
11220
|
-
const
|
|
11221
|
-
fs.
|
|
12137
|
+
const pagePath = safeDataSlugPath(DATA_PAGES_DIR, slug, 'page');
|
|
12138
|
+
fs.mkdirSync(path.dirname(pagePath), { recursive: true });
|
|
12139
|
+
fs.writeFileSync(pagePath, JSON.stringify(projectState.page, null, 2), 'utf8');
|
|
12140
|
+
}
|
|
12141
|
+
if (projectState.collections && typeof projectState.collections === 'object') {
|
|
12142
|
+
for (const [source, collection] of Object.entries(projectState.collections)) {
|
|
12143
|
+
const sourceSlug = String(source).replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
12144
|
+
if (!sourceSlug) continue;
|
|
12145
|
+
const collectionDir = path.join(DATA_COLLECTIONS_DIR, sourceSlug);
|
|
12146
|
+
fs.mkdirSync(collectionDir, { recursive: true });
|
|
12147
|
+
fs.writeFileSync(path.join(collectionDir, `${sourceSlug}.json`), JSON.stringify(collection, null, 2), 'utf8');
|
|
12148
|
+
}
|
|
11222
12149
|
}
|
|
11223
12150
|
sendJson(res, 200, { ok: true });
|
|
11224
12151
|
} catch (e) { sendJson(res, 500, { error: e?.message || 'Save to file failed' }); }
|