@thorprovider/storefront 0.3.1 → 0.4.0-mvp.2
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/README.md +321 -152
- package/VENDOR.md +69 -0
- package/assets/openspec/config.yaml +38 -0
- package/assets/templates/AGENTS.md +76 -0
- package/assets/templates/app/app/[[...slug]]/page.tsx +54 -2
- package/assets/templates/app/app/[[...slug]]/renderer.tsx +13 -2
- package/assets/templates/app/app/globals.css +5 -8
- package/assets/templates/app/app/layout.tsx +17 -7
- package/assets/templates/app/app/robots.ts +19 -0
- package/assets/templates/app/app/sitemap.ts +71 -0
- package/assets/templates/app/lib/__STOREFRONT__/actions.ts +612 -0
- package/assets/templates/app/lib/__STOREFRONT__/cart-state.ts +66 -0
- package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +50 -57
- package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +679 -0
- package/assets/templates/app/lib/__STOREFRONT__/composition.ts +26 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +197 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.ts +5 -36
- package/assets/templates/app/lib/__STOREFRONT__/icon-map.ts +27 -0
- package/assets/templates/app/lib/__STOREFRONT__/mappers.ts +13 -11
- package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +264 -51
- package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +6 -3
- package/assets/templates/app/lib/__STOREFRONT__/schema.ts +136 -0
- package/assets/templates/app/lib/__STOREFRONT__/section-observation.ts +182 -0
- package/assets/templates/app/lib/__STOREFRONT__/section-sources.ts +130 -0
- package/assets/templates/app/lib/__STOREFRONT__/session.core.ts +470 -0
- package/assets/templates/app/lib/__STOREFRONT__/session.ts +169 -0
- package/assets/templates/app/lib/__STOREFRONT__/shell/content.ts +120 -0
- package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +38 -0
- package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +129 -10
- package/assets/templates/app/lib/__STOREFRONT__/spec/page-root.ts +5 -1
- package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +191 -0
- package/assets/templates/app/lib/__STOREFRONT__/state.core.ts +257 -0
- package/assets/templates/app/lib/__STOREFRONT__/state.ts +7 -31
- package/assets/templates/app/lib/__STOREFRONT__/store-context.ts +161 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.client.tsx +25 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.tsx +16 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.client.tsx +24 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.tsx +5 -18
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/index.ts +18 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/logoCloud.tsx +26 -14
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/pricing.tsx +36 -18
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.client.tsx +100 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.tsx +19 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.cta.ts +19 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.faq.ts +18 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.products.ts +39 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/statistics.tsx +35 -14
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/testimonials.tsx +31 -13
- package/assets/templates/app/lib/__STOREFRONT__/views/types.ts +5 -3
- package/assets/templates/features/checkout/handlers.ts +159 -0
- package/assets/templates/features/checkout/state.ts +61 -0
- package/assets/templates/features/customer-auth/handlers.ts +59 -0
- package/assets/templates/features/customer-auth/state.ts +45 -0
- package/assets/templates/features/recommendations/state.ts +11 -0
- package/assets/templates/views/account/content.ts +35 -0
- package/assets/templates/views/account/index.tsx +114 -0
- package/assets/templates/views/account/registry.tsx +439 -0
- package/assets/templates/views/account/schema.ts +53 -0
- package/assets/templates/views/cart/content.ts +11 -0
- package/assets/templates/views/cart/data.ts +23 -0
- package/assets/templates/views/cart/index.tsx +53 -0
- package/assets/templates/views/cart/props.ts +12 -0
- package/assets/templates/views/cart/registry.tsx +81 -0
- package/assets/templates/views/cart/schema.ts +52 -0
- package/assets/templates/views/category/assembly/assembly.json +29 -0
- package/assets/templates/views/category/assembly/index.skeleton.tsx +251 -0
- package/assets/templates/views/category/assembly/registry.skeleton.tsx +34 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/binding.json +5 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/catalog.part +4 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/element.part +7 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/registry.part +18 -0
- package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/catalog.part +5 -0
- package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/element.part +34 -0
- package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/registry.part +71 -0
- package/assets/templates/views/category/content.ts +30 -0
- package/assets/templates/views/category/data.ts +12 -0
- package/assets/templates/views/category/index.tsx +299 -0
- package/assets/templates/views/category/props.ts +23 -0
- package/assets/templates/views/category/registry.tsx +122 -0
- package/assets/templates/views/category/schema.ts +91 -0
- package/assets/templates/views/checkout/content.ts +105 -0
- package/assets/templates/views/checkout/data.ts +23 -0
- package/assets/templates/views/checkout/index.tsx +111 -0
- package/assets/templates/views/checkout/props.ts +49 -0
- package/assets/templates/views/checkout/registry.tsx +217 -0
- package/assets/templates/views/checkout/schema.ts +229 -0
- package/assets/templates/views/home/assembly/assembly.json +33 -0
- package/assets/templates/views/home/assembly/index.skeleton.tsx +90 -0
- package/assets/templates/views/home/assembly/registry.skeleton.tsx +25 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/element.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/registry.part +11 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/element.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/registry.part +10 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/element.part +5 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/registry.part +16 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/element.part +5 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/registry.part +26 -0
- package/assets/templates/views/home/content.ts +70 -0
- package/assets/templates/views/home/data.ts +24 -0
- package/assets/templates/views/home/index.tsx +57 -85
- package/assets/templates/views/home/props.ts +22 -0
- package/assets/templates/views/home/registry.tsx +38 -22
- package/assets/templates/views/home/schema.ts +93 -0
- package/assets/templates/views/product/content.ts +25 -0
- package/assets/templates/views/product/data.ts +50 -0
- package/assets/templates/views/product/index.tsx +148 -0
- package/assets/templates/views/product/props.ts +27 -0
- package/assets/templates/views/product/registry.tsx +103 -0
- package/assets/templates/views/product/schema.ts +121 -0
- package/assets/templates/views/static/content.ts +99 -0
- package/assets/templates/views/static/index.tsx +60 -0
- package/assets/templates/views/static/registry.tsx +66 -0
- package/assets/templates/views/static/schema.ts +45 -0
- package/bin/storefront.js +2424 -148
- package/examples/merchandising/category-led.json +37 -0
- package/examples/merchandising/collection-led.json +37 -0
- package/package.json +15 -9
- package/recipes/archetypes.json +28 -14
- package/recipes/catalog.json +400 -0
- package/recipes/catalog.mjs +128 -0
- package/recipes/catalog.schema.json +64 -0
- package/recipes/feature.schema.json +89 -0
- package/recipes/features.json +162 -0
- package/recipes/manifest.schema.json +69 -0
- package/recipes/operations.schema.json +43 -0
- package/recipes/recipe.schema.json +11 -2
- package/recipes/recipes.json +76 -19
- package/recipes/sections.json +34 -5
- package/recipes/validate.mjs +838 -32
- package/scripts/candidate.mjs +236 -0
- package/scripts/composers.mjs +160 -0
- package/scripts/env-file.mjs +86 -0
- package/scripts/file-classes.mjs +121 -0
- package/scripts/logical-controls.mjs +61 -0
- package/scripts/manifest.mjs +298 -0
- package/scripts/migrate-english-routes.mjs +276 -0
- package/scripts/operation-registry.mjs +256 -0
- package/scripts/operations.mjs +720 -0
- package/scripts/ownership-conflict.mjs +356 -0
- package/scripts/pinning.mjs +573 -0
- package/scripts/plan-apply.mjs +987 -0
- package/scripts/preflight.mjs +51 -0
- package/scripts/reconcile.mjs +549 -0
- package/scripts/render-templates.mjs +338 -68
- package/scripts/schema-validate.mjs +132 -0
- package/scripts/section-compat.mjs +300 -0
- package/scripts/section-config.mjs +253 -0
- package/scripts/section-operations.mjs +508 -0
- package/scripts/starter.mjs +17 -0
- package/scripts/sync-assets.mjs +22 -15
- package/scripts/theme/color.mjs +185 -0
- package/scripts/theme/css-theme.mjs +233 -0
- package/scripts/theme/fonts.mjs +123 -0
- package/scripts/theme/import-export.mjs +125 -0
- package/scripts/theme/preset.mjs +93 -0
- package/scripts/theme/theme-plan.mjs +292 -0
- package/scripts/theme/tokens.mjs +137 -0
- package/scripts/view-assembly.mjs +337 -0
- package/scripts/view-selection.mjs +211 -0
- package/skills/sf-feature/SKILL.md +80 -0
- package/skills/sf-setup/SKILL.md +79 -0
- package/skills/sf-theme-gen/SKILL.md +100 -30
- package/skills/sf-theme-gen/references/shadcn-theming.md +87 -0
- package/skills/{shadcn-theming/references/templates → sf-theme-gen/references}/theme-setup.md +15 -19
- package/skills/{shadcn-theming → sf-theme-gen}/references/theming-guide.md +6 -11
- package/skills/sf-view-gen/SKILL.md +146 -93
- package/skills/sf-view-gen/assets/recipes/archetypes.json +28 -14
- package/skills/sf-view-gen/assets/recipes/catalog.json +400 -0
- package/skills/sf-view-gen/assets/recipes/catalog.schema.json +64 -0
- package/skills/sf-view-gen/assets/recipes/features.json +162 -0
- package/skills/sf-view-gen/assets/recipes/recipes.json +76 -19
- package/skills/sf-view-gen/assets/recipes/sections.json +34 -5
- package/skills/sf-view-gen/assets/templates/views/sections/cta.client.tsx +25 -0
- package/skills/sf-view-gen/assets/templates/views/sections/cta.tsx +16 -0
- package/skills/sf-view-gen/assets/templates/views/sections/faq.client.tsx +13 -9
- package/skills/sf-view-gen/assets/templates/views/sections/faq.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.client.tsx +12 -8
- package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/pricing.client.tsx +14 -10
- package/skills/sf-view-gen/assets/templates/views/sections/pricing.tsx +2 -9
- package/skills/sf-view-gen/assets/templates/views/sections/products.client.tsx +100 -0
- package/skills/sf-view-gen/assets/templates/views/sections/products.tsx +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.cta.ts +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.faq.ts +18 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.logoCloud.ts +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.pricing.ts +26 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.products.ts +39 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.statistics.ts +25 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.testimonials.ts +23 -0
- package/skills/sf-view-gen/assets/templates/views/sections/statistics.client.tsx +16 -8
- package/skills/sf-view-gen/assets/templates/views/sections/statistics.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/testimonials.client.tsx +13 -9
- package/skills/sf-view-gen/assets/templates/views/sections/testimonials.tsx +2 -5
- package/skills/{json-render-core/SKILL.md → sf-view-gen/references/json-render-core.md} +4 -27
- package/skills/{json-render-next/SKILL.md → sf-view-gen/references/json-render-next.md} +5 -8
- package/skills/{json-render-react/SKILL.md → sf-view-gen/references/json-render-react.md} +3 -5
- package/skills/{json-render-shadcn/SKILL.md → sf-view-gen/references/json-render-shadcn.md} +3 -5
- package/skills/sf-view-gen/references/views.md +58 -33
- package/skills/sf-view-gen/scripts/catalog.mjs +128 -0
- package/skills/sf-view-gen/scripts/schema-validate.mjs +132 -0
- package/skills/sf-view-gen/scripts/validate.mjs +838 -32
- package/skills/storefront-workflow/SKILL.md +179 -0
- package/skills/{storefront-best-practices/reference/design.md → storefront-workflow/references/design-consistency.md} +13 -29
- package/CHANGELOG.md +0 -61
- package/commands/sf-add-view.md +0 -26
- package/commands/sf-theme.md +0 -20
- package/commands/sf-view.md +0 -25
- package/skills/building-storefronts/SKILL.md +0 -178
- package/skills/building-storefronts/references/frontend-integration.md +0 -229
- package/skills/json-render-remotion/SKILL.md +0 -111
- package/skills/json-render-solid/SKILL.md +0 -204
- package/skills/nextjs-shadcn/SKILL.md +0 -367
- package/skills/nextjs-shadcn/references/architecture.md +0 -499
- package/skills/nextjs-shadcn/references/project-setup.md +0 -162
- package/skills/nextjs-shadcn/references/shadcn-platform.md +0 -261
- package/skills/nextjs-shadcn/references/sidebar.md +0 -274
- package/skills/nextjs-shadcn/references/styling.md +0 -555
- package/skills/sf-view-gen/assets/templates/views/category/index.tsx +0 -86
- package/skills/sf-view-gen/assets/templates/views/category/registry.tsx +0 -48
- package/skills/sf-view-gen/assets/templates/views/checkout/index.tsx +0 -192
- package/skills/sf-view-gen/assets/templates/views/checkout/registry.tsx +0 -45
- package/skills/sf-view-gen/assets/templates/views/product/index.tsx +0 -122
- package/skills/sf-view-gen/assets/templates/views/product/registry.tsx +0 -49
- package/skills/sf-view-gen/assets/templates/views/user-panel/index.tsx +0 -168
- package/skills/sf-view-gen/assets/templates/views/user-panel/registry.tsx +0 -49
- package/skills/shadcn-component-discovery/SKILL.md +0 -273
- package/skills/shadcn-component-discovery/references/registries.md +0 -226
- package/skills/shadcn-theming/SKILL.md +0 -104
- package/skills/storefront-best-practices/SKILL.md +0 -421
- package/skills/storefront-best-practices/reference/components/breadcrumbs.md +0 -123
- package/skills/storefront-best-practices/reference/components/cart-popup.md +0 -189
- package/skills/storefront-best-practices/reference/components/country-selector.md +0 -298
- package/skills/storefront-best-practices/reference/components/footer.md +0 -112
- package/skills/storefront-best-practices/reference/components/hero.md +0 -241
- package/skills/storefront-best-practices/reference/components/megamenu.md +0 -239
- package/skills/storefront-best-practices/reference/components/navbar.md +0 -397
- package/skills/storefront-best-practices/reference/components/popups.md +0 -221
- package/skills/storefront-best-practices/reference/components/product-card.md +0 -125
- package/skills/storefront-best-practices/reference/components/product-reviews.md +0 -217
- package/skills/storefront-best-practices/reference/components/product-slider.md +0 -174
- package/skills/storefront-best-practices/reference/components/search.md +0 -101
- package/skills/storefront-best-practices/reference/connecting-to-backend.md +0 -391
- package/skills/storefront-best-practices/reference/features/promotions.md +0 -307
- package/skills/storefront-best-practices/reference/features/wishlist.md +0 -230
- package/skills/storefront-best-practices/reference/layouts/account.md +0 -380
- package/skills/storefront-best-practices/reference/layouts/cart.md +0 -316
- package/skills/storefront-best-practices/reference/layouts/checkout.md +0 -486
- package/skills/storefront-best-practices/reference/layouts/home-page.md +0 -264
- package/skills/storefront-best-practices/reference/layouts/order-confirmation.md +0 -231
- package/skills/storefront-best-practices/reference/layouts/product-details.md +0 -527
- package/skills/storefront-best-practices/reference/layouts/product-listing.md +0 -520
- package/skills/storefront-best-practices/reference/layouts/static-pages.md +0 -356
- package/skills/storefront-best-practices/reference/medusa.md +0 -307
- /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/mobile-responsiveness.md +0 -0
- /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/seo.md +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Optional section fragment: products (mirrors recipes/sections.json) — shared contract.
|
|
2
|
+
// Zod mirror of the resolved Products section props, shared by the server catalog
|
|
3
|
+
// (products.tsx) and the client registry (products.client.tsx). The product list
|
|
4
|
+
// reuses the canonical presentation contract owned by @thorprovider/blocks so the
|
|
5
|
+
// section renders exactly what the host mapped from the catalog result.
|
|
6
|
+
|
|
7
|
+
import { z } from 'zod'
|
|
8
|
+
import { productPresentationSchema } from '@thorprovider/blocks'
|
|
9
|
+
|
|
10
|
+
// The per-instance resolution status the host loader computes for a data-bound
|
|
11
|
+
// section. `ready`/`empty` carry resolved products; the remaining states carry no
|
|
12
|
+
// products and render an actionable, truthful message instead.
|
|
13
|
+
export const productsStatusSchema = z.enum([
|
|
14
|
+
'unconfigured',
|
|
15
|
+
'ready',
|
|
16
|
+
'empty',
|
|
17
|
+
'invalid-query',
|
|
18
|
+
'unsupported-query',
|
|
19
|
+
'unmounted-capability',
|
|
20
|
+
'data-error',
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
export const productsPropsSchema = z.object({
|
|
24
|
+
sectionId: z.string().min(1).optional(),
|
|
25
|
+
sectionType: z.string().min(1).optional(),
|
|
26
|
+
variant: z.string().optional(),
|
|
27
|
+
// Defensive defaults: a missing resolved state (e.g. the view loader could not
|
|
28
|
+
// run) degrades to a truthful error instead of crashing the render.
|
|
29
|
+
status: productsStatusSchema.default('data-error'),
|
|
30
|
+
products: z.array(productPresentationSchema).default([]),
|
|
31
|
+
heading: z.string().optional(),
|
|
32
|
+
description: z.string().optional(),
|
|
33
|
+
objective: z.string().optional(),
|
|
34
|
+
ctaLabel: z.string().optional(),
|
|
35
|
+
ctaHref: z.string().optional(),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
export type ProductsProps = z.infer<typeof productsPropsSchema>
|
|
39
|
+
export type ProductsStatus = z.infer<typeof productsStatusSchema>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Optional section fragment: statistics (mirrors recipes/sections.json) — shared contract.
|
|
2
|
+
// Zod mirror of the StatisticItem/Statistic1Props block contracts, shared by
|
|
3
|
+
// the server catalog (statistics.tsx) and the client registry
|
|
4
|
+
// (statistics.client.tsx). The block expects `icon` as a Lucide component;
|
|
5
|
+
// specs ship the icon NAME, so it is a string here and the client registry
|
|
6
|
+
// resolves it via resolveIcon.
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod'
|
|
9
|
+
|
|
10
|
+
export const statisticItemSchema = z.object({
|
|
11
|
+
id: z.string(),
|
|
12
|
+
title: z.string(),
|
|
13
|
+
value: z.string(),
|
|
14
|
+
description: z.string(),
|
|
15
|
+
progress: z.number(),
|
|
16
|
+
progressLabel: z.string(),
|
|
17
|
+
icon: z.string().optional(),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export const statisticsPropsSchema = z.object({
|
|
21
|
+
title: z.string().optional(),
|
|
22
|
+
badge: z.string().optional(),
|
|
23
|
+
description: z.string().optional(),
|
|
24
|
+
items: z.array(statisticItemSchema),
|
|
25
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Optional section fragment: testimonials (mirrors recipes/sections.json) — shared contract.
|
|
2
|
+
// Zod mirror of the Testimonial1Item/Testimonial1Props block contracts, shared
|
|
3
|
+
// by the server catalog (testimonials.tsx) and the client registry
|
|
4
|
+
// (testimonials.client.tsx).
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
|
|
8
|
+
export const testimonialItemSchema = z.object({
|
|
9
|
+
id: z.string(),
|
|
10
|
+
name: z.string(),
|
|
11
|
+
role: z.string(),
|
|
12
|
+
avatar: z.string(),
|
|
13
|
+
content: z.string(),
|
|
14
|
+
rating: z.number(),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export const testimonialsPropsSchema = z.object({
|
|
18
|
+
title: z.string().optional(),
|
|
19
|
+
prevLabel: z.string().optional(),
|
|
20
|
+
nextLabel: z.string().optional(),
|
|
21
|
+
slideAriaLabel: z.string().optional(),
|
|
22
|
+
items: z.array(testimonialItemSchema),
|
|
23
|
+
})
|
|
@@ -4,15 +4,23 @@
|
|
|
4
4
|
// The matching server-side catalog entry lives in statistics.tsx.
|
|
5
5
|
|
|
6
6
|
import { Statistic1 as StatisticBlock } from '@thorprovider/blocks'
|
|
7
|
+
import { statisticsPropsSchema } from './schema.statistics'
|
|
8
|
+
import { resolveIcon } from '../../icon-map'
|
|
7
9
|
import type { ViewRegistry } from '../types'
|
|
8
10
|
|
|
9
11
|
export const registry: ViewRegistry = {
|
|
10
|
-
Statistics: ({ props }) =>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
Statistics: ({ props }) => {
|
|
13
|
+
const parsed = statisticsPropsSchema.parse(props)
|
|
14
|
+
return (
|
|
15
|
+
<StatisticBlock
|
|
16
|
+
badge={parsed.badge}
|
|
17
|
+
title={parsed.title ?? 'Números que nos respaldan'}
|
|
18
|
+
description={parsed.description}
|
|
19
|
+
items={parsed.items.map((item) => ({
|
|
20
|
+
...item,
|
|
21
|
+
icon: resolveIcon(item.icon),
|
|
22
|
+
}))}
|
|
23
|
+
/>
|
|
24
|
+
)
|
|
25
|
+
},
|
|
18
26
|
}
|
|
@@ -2,18 +2,13 @@
|
|
|
2
2
|
// Server-side part: element type + zod catalog entry. The component mappings
|
|
3
3
|
// live in statistics.client.tsx ('use client').
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { statisticsPropsSchema } from './schema.statistics'
|
|
6
6
|
|
|
7
7
|
export const statisticsSection = {
|
|
8
8
|
elementType: 'Statistics',
|
|
9
9
|
catalog: {
|
|
10
10
|
Statistics: {
|
|
11
|
-
props:
|
|
12
|
-
title: z.string().optional(),
|
|
13
|
-
badge: z.string().optional(),
|
|
14
|
-
description: z.string().optional(),
|
|
15
|
-
items: z.array(z.record(z.string(), z.unknown())),
|
|
16
|
-
}),
|
|
11
|
+
props: statisticsPropsSchema,
|
|
17
12
|
description: 'Key metrics or trust statistics.',
|
|
18
13
|
},
|
|
19
14
|
},
|
|
@@ -4,16 +4,20 @@
|
|
|
4
4
|
// The matching server-side catalog entry lives in testimonials.tsx.
|
|
5
5
|
|
|
6
6
|
import { Testimonial1 as TestimonialBlock } from '@thorprovider/blocks'
|
|
7
|
+
import { testimonialsPropsSchema } from './schema.testimonials'
|
|
7
8
|
import type { ViewRegistry } from '../types'
|
|
8
9
|
|
|
9
10
|
export const registry: ViewRegistry = {
|
|
10
|
-
Testimonials: ({ props }) =>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
Testimonials: ({ props }) => {
|
|
12
|
+
const parsed = testimonialsPropsSchema.parse(props)
|
|
13
|
+
return (
|
|
14
|
+
<TestimonialBlock
|
|
15
|
+
title={parsed.title ?? 'Lo que dicen nuestros clientes'}
|
|
16
|
+
items={parsed.items}
|
|
17
|
+
prevLabel={parsed.prevLabel ?? 'Anterior'}
|
|
18
|
+
nextLabel={parsed.nextLabel ?? 'Siguiente'}
|
|
19
|
+
slideAriaLabel={parsed.slideAriaLabel ?? 'Testimonio'}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
},
|
|
19
23
|
}
|
|
@@ -2,16 +2,13 @@
|
|
|
2
2
|
// Server-side part: element type + zod catalog entry. The component mappings
|
|
3
3
|
// live in testimonials.client.tsx ('use client').
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { testimonialsPropsSchema } from './schema.testimonials'
|
|
6
6
|
|
|
7
7
|
export const testimonialsSection = {
|
|
8
8
|
elementType: 'Testimonials',
|
|
9
9
|
catalog: {
|
|
10
10
|
Testimonials: {
|
|
11
|
-
props:
|
|
12
|
-
title: z.string().optional(),
|
|
13
|
-
items: z.array(z.record(z.string(), z.unknown())),
|
|
14
|
-
}),
|
|
11
|
+
props: testimonialsPropsSchema,
|
|
15
12
|
description: 'Customer testimonials or social proof.',
|
|
16
13
|
},
|
|
17
14
|
},
|
|
@@ -1,38 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
name: core
|
|
3
|
-
description: Core package for defining schemas, catalogs, and AI prompt generation for json-render. Use when working with @json-render/core, defining schemas, creating catalogs, or building JSON specs for UI/video generation.
|
|
4
|
-
---
|
|
1
|
+
# @json-render/core reference
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Core package for schema definition, catalog creation, and spec streaming.
|
|
3
|
+
Reference material for the `sf-view-gen` skill: schema definition, catalog
|
|
4
|
+
creation, and spec shape for `@json-render/core`.
|
|
9
5
|
|
|
10
6
|
## Key Concepts
|
|
11
7
|
|
|
12
8
|
- **Schema**: Defines the structure of specs and catalogs (use `defineSchema`)
|
|
13
9
|
- **Catalog**: Maps component/action names to their definitions (use `defineCatalog`)
|
|
14
|
-
- **Spec**: JSON output
|
|
10
|
+
- **Spec**: JSON output that conforms to the schema
|
|
15
11
|
- **SpecStream**: JSONL streaming format for progressive spec building
|
|
16
12
|
|
|
17
|
-
## Experimental Decision-Model Composition
|
|
18
|
-
|
|
19
|
-
For decision-model composition, import `experimental_composeSpec` and `experimental_createEvaluator` from `@json-render/core`. These APIs are unreleased; use a source build until published, then pin exact versions. Experimental exports and `Experimental_` types can change in any release.
|
|
20
|
-
|
|
21
|
-
- Run the Gateway evaluator server-side with `{ model: "typesafe-ai/jev", apiKey: process.env.AI_GATEWAY_API_KEY! }`. A plain model identifier is required; Jev is the current example; do not import a provider constructor.
|
|
22
|
-
- Call `experimental_composeSpec({ catalog, candidates, prompt, evaluate, initialState, signal })`. It is an async generator; stream `step.spec` snapshots to your existing renderer and inspect `complete.stopReason` (`finish`, `limit`, `unavailable`). Errors and cancellation throw; retain the last snapshot as partial UI.
|
|
23
|
-
- New trees default to `strategy: "batch"`: one evaluation selects root/membership, then a second arranges the selected elements when needed. The first snapshot contains selected content in catalog order under the root's default/first slot. Resource variants share one exclusive question; repeated counts include the root. Root selection takes precedence over conflicting speculative membership for that recipe/resource. Equal sibling positions retain catalog order. Combined layouts are validated before publication; cycles or excessive depth throw. `maxElements` caps batched creation (default 32). Limit-truncated selections or a missing required layout call return `limit`. Use `strategy: "sequential"` for legacy `next`/`parent` adapters or sequential creation. Edits stay sequential.
|
|
24
|
-
- Batched trace steps use `choice: "select" | "layout"` and an `answers` record. Count each trace as one evaluation, including its tokens and latency once. Custom evaluators must answer every offered question; names/choices are opaque and include `root`/`select_*`, then `parent_*`/`order_*` for batches.
|
|
25
|
-
- For follow-up edits, pass the selected version as `initialSpec`. It is cloned and validated; the evaluator may add, replace, remove non-root subtrees, or move/reorder them. Unchanged IDs, bindings, and state are preserved. Optional `elementDescriptions` shares identifying descriptions without exposing raw props/state. `initialState` overrides the seed state. Seeds must be valid trees within the catalog, expression subset, and depth limit. Matching recipes consume usage/resource limits; removals/replacements release them. Replacements/moves use two evaluations (select target, then recipe/destination), each counted against the budget. Treat operation and position keys as opaque.
|
|
26
|
-
- Supply atomic candidates with `{ id, description, element: { type, props, on?, visible? }, root?, maxUses?, resource? }`. Catalog alone is insufficient: the app must supply values and binding recipes. Jev chooses elements and parent slots, never free-form text or code. It never executes actions.
|
|
27
|
-
- Candidates are configured component instances, not page templates. Build them from current app records/operations or bind props to `initialState`; offer explicit alternatives for chart types, field configurations, and layout variants. The model chooses grouping and order within those options. Name required sections in prompts; structural validity does not imply semantic completeness.
|
|
28
|
-
- V1 supports flat Spec catalogs, named slots, literals, `$state`, `$bindState`, and state visibility. No prebuilt children, repeat/watch, computed/template/conditional props, or custom directives. Success/error callbacks must reference allowed actions. Events must be declared in the component catalog.
|
|
29
|
-
- Props and action params are validated against initial state without applying schema transforms/defaults. Supply valid initial values and validate/authorize action calls at runtime. Built-ins without parameter schemas get name validation only.
|
|
30
|
-
- `root` defaults true, `maxUses` defaults one, shared `resource` values make alternatives mutually exclusive. Defaults: 32 evaluations (terminal calls included; no extra finish call for batches), depth eight, 10-second Gateway timeout per call. Supply an overall abort signal.
|
|
31
|
-
- Candidate descriptions, prompt, instructions, topology, and explicit `context` are sent to the evaluator. Initial state and raw props/binding values are not sent automatically.
|
|
32
|
-
- For custom providers implement `Experimental_CompositionEvaluator`: accept `{ state, questions, signal }`, return `{ answers: { [question]: { choice, confidence? } }, usage?: { inputTokens? } }`. Only return offered criteria keys.
|
|
33
|
-
|
|
34
|
-
See `packages/core/README.md` and `/docs/jev` for app integration and source-build instructions. The web playground is an example consumer, not a dependency of the API.
|
|
35
|
-
|
|
36
13
|
## Defining a Schema
|
|
37
14
|
|
|
38
15
|
```typescript
|
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
name: next
|
|
3
|
-
description: Next.js renderer for json-render that turns JSON specs into full Next.js applications with routes, layouts, SSR, and metadata. Use when working with @json-render/next, building Next.js apps from JSON specs, or creating AI-generated multi-page applications.
|
|
4
|
-
---
|
|
1
|
+
# @json-render/next reference
|
|
5
2
|
|
|
6
|
-
|
|
3
|
+
Reference material for the `sf-view-gen` skill: the Next.js rendering shape that
|
|
4
|
+
converts the json-render spec into routes, layouts, SSR, and metadata.
|
|
7
5
|
|
|
8
6
|
Next.js renderer that converts JSON specs into full Next.js applications with routes, pages, layouts, metadata, and SSR support.
|
|
9
7
|
|
|
10
8
|
## Quick Start
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
```
|
|
10
|
+
`@json-render/core`, `@json-render/react`, and `@json-render/next` are already
|
|
11
|
+
dependencies of the generated storefront; no install step is needed.
|
|
15
12
|
|
|
16
13
|
### 1. Define Your Spec
|
|
17
14
|
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
name: react
|
|
3
|
-
description: React renderer for json-render that turns JSON specs into React components. Use when working with @json-render/react, building React UIs from JSON, creating component catalogs, or rendering AI-generated specs.
|
|
4
|
-
---
|
|
1
|
+
# @json-render/react reference
|
|
5
2
|
|
|
6
|
-
|
|
3
|
+
Reference material for the `sf-view-gen` skill: registry mapping, providers,
|
|
4
|
+
bindings, and action wiring for the React renderer.
|
|
7
5
|
|
|
8
6
|
React renderer that converts JSON specs into React component trees.
|
|
9
7
|
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
name: shadcn
|
|
3
|
-
description: Pre-built shadcn/ui components for json-render. Use when working with @json-render/shadcn, adding standard UI components to a catalog, or building web UIs with Radix UI + Tailwind CSS components.
|
|
4
|
-
---
|
|
1
|
+
# @json-render/shadcn reference
|
|
5
2
|
|
|
6
|
-
|
|
3
|
+
Reference material for the `sf-view-gen` skill: the standard shadcn/ui component
|
|
4
|
+
definitions and implementations available to a catalog.
|
|
7
5
|
|
|
8
6
|
Pre-built shadcn/ui component definitions and implementations for json-render. Provides 36 components built on Radix UI + Tailwind CSS.
|
|
9
7
|
|
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
# View
|
|
1
|
+
# View modules reference
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Reference material for the `sf-view-gen` skill. Each view is a self-contained
|
|
4
|
+
module the CLI generates into `lib/<name>/views/<view>/`. The CLI — not the
|
|
5
|
+
agent — writes the barrel and every derived file; `npx @thorprovider/storefront
|
|
6
|
+
view add <view>` generates one view's derived files and reconciles the barrel.
|
|
7
|
+
|
|
8
|
+
The available views are whatever `npx @thorprovider/storefront views --json`
|
|
9
|
+
reports; this file names none of them as the authoritative set.
|
|
5
10
|
|
|
6
11
|
## Module shape
|
|
7
12
|
|
|
8
|
-
Each view is the **
|
|
13
|
+
Each view is the **three-file split** (RSC):
|
|
9
14
|
|
|
10
15
|
- `views/<view>/index.tsx` — pure Server Component module: name, elements,
|
|
11
16
|
zod catalog, route, loader. Never imports values from `@json-render/react`
|
|
12
|
-
or `@thorprovider/blocks`.
|
|
17
|
+
or `@thorprovider/blocks`. **Derived — the CLI writes it; never hand-edit it.**
|
|
18
|
+
- `views/<view>/schema.ts` — pure Zod schemas mirroring the block contracts
|
|
19
|
+
the view renders (props only). No React, no `'use client'`, no Next imports.
|
|
13
20
|
- `views/<view>/registry.tsx` — `'use client'`: exports
|
|
14
21
|
`registry: ViewRegistry`, the component mappings
|
|
15
22
|
(`Record<string, (ctx: BaseComponentProps<Record<string, unknown>>) => ReactNode>`).
|
|
@@ -39,31 +46,49 @@ view never edits them.
|
|
|
39
46
|
|
|
40
47
|
## Registering a view
|
|
41
48
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
49
|
+
The barrel `lib/<name>/views/index.ts` (server) and `lib/<name>/views/registry.ts`
|
|
50
|
+
(client) are **generated from the manifest by the CLI** on every reconcile. Do
|
|
51
|
+
not hand-edit them; add or remove a view with
|
|
52
|
+
`npx @thorprovider/storefront view add|remove <name>`. The CLI emits one import
|
|
53
|
+
and one entry per installed view, so a view removed from the manifest cannot
|
|
54
|
+
reappear through folder scanning.
|
|
55
|
+
|
|
56
|
+
## Variants
|
|
57
|
+
|
|
58
|
+
A slot's variant is a presentation choice recorded in the manifest and applied by
|
|
59
|
+
the reconciler. Change it through the CLI — `view add`/`remove` with
|
|
60
|
+
`--variant slot=Name` (a supported variant the registry reports), or a
|
|
61
|
+
`section.add` / `section.reorder` operation for editorial sections. Do not
|
|
62
|
+
hand-edit a derived `index.tsx` or registry to swap an imported identifier; the
|
|
63
|
+
next `sync` would overwrite it and the manifest would disagree with the files.
|
|
64
|
+
|
|
65
|
+
## Data contracts and icons
|
|
66
|
+
|
|
67
|
+
Two rules that keep generated views from crashing at render time:
|
|
68
|
+
|
|
69
|
+
1. **Mappers are applied at loaders, never in registries.** Adapter canon does
|
|
70
|
+
not match block props 1:1 (e.g. a category may carry `productCount?: number`
|
|
71
|
+
or no count at all, and some count props are optional). The view's loader maps
|
|
72
|
+
adapter data through `lib/<name>/mappers.ts` before it reaches state.
|
|
73
|
+
Registries receive already-mapped, block-shaped props. When a value is unknown
|
|
74
|
+
(e.g. no category count), **omit** it — never fabricate a `0` default, which
|
|
75
|
+
would render a false "0 items". A genuine `0` from a richer source is
|
|
76
|
+
preserved.
|
|
77
|
+
|
|
78
|
+
2. **Icons cross registries as components, via `icon-map`.** Several blocks type
|
|
79
|
+
icon props as `LucideIcon` (a component). A string like `'truck'` bound
|
|
80
|
+
directly renders a literal `<truck>` tag in the DOM. Registries resolve icon
|
|
81
|
+
names through `lib/<name>/icon-map.ts` (`resolveIcon(name)`, kebab-case →
|
|
82
|
+
`LucideIcon`, neutral `Circle` fallback). When adding a view that uses a new
|
|
83
|
+
icon, extend `icon-map.ts` with an entry — never inline a string where a
|
|
84
|
+
component is expected.
|
|
85
|
+
|
|
86
|
+
Supporting rules for the same goal:
|
|
87
|
+
|
|
88
|
+
- View schemas live in `views/<view>/schema.ts` and **mirror the block's Zod
|
|
89
|
+
contracts locally** — never import schemas from `@thorprovider/blocks` (the
|
|
90
|
+
scaffold runs Zod v4 while blocks ships v3; cross-major schema assignment
|
|
91
|
+
breaks `defineCatalog`).
|
|
92
|
+
- Registries **parse props through the view schema** (`schema.parse(props)`)
|
|
93
|
+
instead of casting bindings (`as never` / `as any` are forbidden in scaffold
|
|
94
|
+
code). Type errors on `parse` are the contract check.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Semantic executable catalog: the single contract that states, per view slot
|
|
3
|
+
// and candidate block variant, whether the variant is `executable` or `held`,
|
|
4
|
+
// the block it renders, the canonical data shape it consumes, the events it
|
|
5
|
+
// emits, its controlled/uncontrolled state, its requirements, and an optional
|
|
6
|
+
// prop bridge from the canonical shape to the block props.
|
|
7
|
+
//
|
|
8
|
+
// The published shape lives in `recipes/catalog.schema.json`; this module loads
|
|
9
|
+
// that same artifact and adds only the invariants the supported schema subset
|
|
10
|
+
// cannot express (a held entry must carry a reason; a slot's default must be a
|
|
11
|
+
// declared variant; an unverified capability must name its owning change). It
|
|
12
|
+
// does not classify the initial set, derive `supported`, or wire any
|
|
13
|
+
// generator/introspection — those are owned by later tasks.
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
16
|
+
import { dirname, join } from 'node:path'
|
|
17
|
+
import { fileURLToPath } from 'node:url'
|
|
18
|
+
import { validateAgainstSchema, validateSchemaSupported } from '../scripts/schema-validate.mjs'
|
|
19
|
+
|
|
20
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
21
|
+
|
|
22
|
+
// Layout-agnostic catalog location: at the package root the catalog lives beside
|
|
23
|
+
// this module (`recipes/`); in an installed skill it is distributed under
|
|
24
|
+
// `skills/sf-view-gen/assets/recipes/` and addressed by relative path. Either
|
|
25
|
+
// layout holds the same synced artifact, so there is one catalog source. The
|
|
26
|
+
// `../scripts/schema-validate.mjs` import resolves to the package `scripts/` at
|
|
27
|
+
// the root and to the skill's own `scripts/` once distributed (same directory).
|
|
28
|
+
const catalogDir = existsSync(join(here, 'catalog.json'))
|
|
29
|
+
? here
|
|
30
|
+
: join(here, '..', 'assets', 'recipes')
|
|
31
|
+
|
|
32
|
+
export const CATALOG_VERSION = 1
|
|
33
|
+
export const CATALOG_SCHEMA_ID = 'https://thorcommerce.dev/storefront/catalog.schema.json'
|
|
34
|
+
|
|
35
|
+
const catalogSchema = JSON.parse(readFileSync(join(catalogDir, 'catalog.schema.json'), 'utf8'))
|
|
36
|
+
|
|
37
|
+
function isPlainObject(value) {
|
|
38
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function hasOwn(object, key) {
|
|
42
|
+
return Object.prototype.hasOwnProperty.call(object, key)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function loadCatalogSchema() {
|
|
46
|
+
return catalogSchema
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Load the published semantic catalog document (`recipes/catalog.json`) — the
|
|
51
|
+
* single source of the executable/held classification. Callers validate it with
|
|
52
|
+
* `validateCatalog`; classification is data, not logic (E1 task 1.2).
|
|
53
|
+
*/
|
|
54
|
+
export function loadCatalog() {
|
|
55
|
+
return JSON.parse(readFileSync(join(catalogDir, 'catalog.json'), 'utf8'))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The slot variant entry schema, read from the published catalog schema so the
|
|
60
|
+
* per-entry contract has exactly one source. Throws loudly if the schema is
|
|
61
|
+
* reshaped, rather than silently validating against nothing.
|
|
62
|
+
*/
|
|
63
|
+
function variantEntrySchema() {
|
|
64
|
+
const entry =
|
|
65
|
+
catalogSchema?.properties?.slots?.additionalProperties?.properties?.variants
|
|
66
|
+
?.additionalProperties
|
|
67
|
+
if (!isPlainObject(entry)) {
|
|
68
|
+
throw new Error('catalog.schema.json does not declare a slot variant entry schema.')
|
|
69
|
+
}
|
|
70
|
+
return entry
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Validate one slot variant entry against the published contract.
|
|
75
|
+
* Returns { ok, errors }.
|
|
76
|
+
*/
|
|
77
|
+
export function validateCatalogEntry(entry) {
|
|
78
|
+
const errors = validateAgainstSchema(entry, variantEntrySchema(), 'catalog entry')
|
|
79
|
+
return { ok: errors.length === 0, errors }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Validate a complete catalog document against the published contract plus the
|
|
84
|
+
* invariants the schema subset cannot express. Returns { ok, errors }.
|
|
85
|
+
*/
|
|
86
|
+
export function validateCatalog(catalog) {
|
|
87
|
+
if (!isPlainObject(catalog)) {
|
|
88
|
+
return { ok: false, errors: ['Catalog must be a JSON object.'] }
|
|
89
|
+
}
|
|
90
|
+
const errors = validateSchemaSupported(catalogSchema)
|
|
91
|
+
errors.push(...validateAgainstSchema(catalog, catalogSchema, 'catalog'))
|
|
92
|
+
|
|
93
|
+
const slots = isPlainObject(catalog.slots) ? catalog.slots : {}
|
|
94
|
+
for (const [slotId, slot] of Object.entries(slots)) {
|
|
95
|
+
if (!isPlainObject(slot)) continue
|
|
96
|
+
const variants = isPlainObject(slot.variants) ? slot.variants : {}
|
|
97
|
+
if (Object.keys(variants).length === 0) {
|
|
98
|
+
errors.push(`Slot "${slotId}" must declare at least one variant.`)
|
|
99
|
+
}
|
|
100
|
+
if (typeof slot.default === 'string' && !hasOwn(variants, slot.default)) {
|
|
101
|
+
errors.push(`Slot "${slotId}" default "${slot.default}" is not a declared variant.`)
|
|
102
|
+
}
|
|
103
|
+
for (const [variantName, entry] of Object.entries(variants)) {
|
|
104
|
+
if (!isPlainObject(entry)) continue
|
|
105
|
+
if (entry.status === 'held' && !(typeof entry.reason === 'string' && entry.reason.length > 0)) {
|
|
106
|
+
errors.push(`Held variant "${slotId}.${variantName}" must declare a non-empty reason.`)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// An unverified capability is held: it must name the change that owns its
|
|
112
|
+
// implementation, so a consumer can see that the capability is not available
|
|
113
|
+
// and who will make it available. This is the honest gate the schema subset
|
|
114
|
+
// cannot express (E1 task 4.2 / CU-04).
|
|
115
|
+
const capabilities = isPlainObject(catalog.capabilities) ? catalog.capabilities : {}
|
|
116
|
+
for (const [capabilityId, capability] of Object.entries(capabilities)) {
|
|
117
|
+
if (!isPlainObject(capability)) continue
|
|
118
|
+
if (
|
|
119
|
+
capability.verification === 'unverified' &&
|
|
120
|
+
!(typeof capability.owner === 'string' && capability.owner.length > 0)
|
|
121
|
+
) {
|
|
122
|
+
errors.push(
|
|
123
|
+
`Unverified capability "${capabilityId}" must declare the owning change in "owner".`
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { ok: errors.length === 0, errors }
|
|
128
|
+
}
|