@thorprovider/storefront 0.3.1 → 0.4.0-mvp.0

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.
Files changed (179) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +248 -99
  3. package/assets/templates/app/app/[[...slug]]/page.tsx +13 -2
  4. package/assets/templates/app/app/layout.tsx +18 -4
  5. package/assets/templates/app/app/robots.ts +13 -0
  6. package/assets/templates/app/app/sitemap.ts +12 -0
  7. package/assets/templates/app/lib/__STOREFRONT__/actions.ts +542 -0
  8. package/assets/templates/app/lib/__STOREFRONT__/cart-state.ts +66 -0
  9. package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +40 -57
  10. package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +608 -0
  11. package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +165 -0
  12. package/assets/templates/app/lib/__STOREFRONT__/handlers.ts +5 -36
  13. package/assets/templates/app/lib/__STOREFRONT__/icon-map.ts +27 -0
  14. package/assets/templates/app/lib/__STOREFRONT__/mappers.ts +13 -11
  15. package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +232 -51
  16. package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +3 -3
  17. package/assets/templates/app/lib/__STOREFRONT__/schema.ts +121 -0
  18. package/assets/templates/app/lib/__STOREFRONT__/session.core.ts +470 -0
  19. package/assets/templates/app/lib/__STOREFRONT__/session.ts +169 -0
  20. package/assets/templates/app/lib/__STOREFRONT__/shell/content.ts +96 -0
  21. package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +34 -0
  22. package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +100 -11
  23. package/assets/templates/app/lib/__STOREFRONT__/spec/page-root.ts +5 -1
  24. package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +26 -0
  25. package/assets/templates/app/lib/__STOREFRONT__/state.core.ts +257 -0
  26. package/assets/templates/app/lib/__STOREFRONT__/state.ts +7 -31
  27. package/assets/templates/app/lib/__STOREFRONT__/store-context.ts +142 -0
  28. package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.client.tsx +25 -0
  29. package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.tsx +16 -0
  30. package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.client.tsx +24 -0
  31. package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.tsx +5 -18
  32. package/assets/templates/app/lib/__STOREFRONT__/views/sections/index.ts +17 -0
  33. package/assets/templates/app/lib/__STOREFRONT__/views/sections/logoCloud.tsx +26 -14
  34. package/assets/templates/app/lib/__STOREFRONT__/views/sections/pricing.tsx +36 -18
  35. package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.cta.ts +19 -0
  36. package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.faq.ts +18 -0
  37. package/assets/templates/app/lib/__STOREFRONT__/views/sections/statistics.tsx +35 -14
  38. package/assets/templates/app/lib/__STOREFRONT__/views/sections/testimonials.tsx +31 -13
  39. package/assets/templates/app/lib/__STOREFRONT__/views/types.ts +5 -3
  40. package/assets/templates/features/checkout/handlers.ts +153 -0
  41. package/assets/templates/features/checkout/state.ts +61 -0
  42. package/assets/templates/features/customer-auth/handlers.ts +59 -0
  43. package/assets/templates/features/customer-auth/state.ts +36 -0
  44. package/assets/templates/features/recommendations/state.ts +11 -0
  45. package/assets/templates/views/account/content.ts +35 -0
  46. package/assets/templates/views/account/index.tsx +114 -0
  47. package/assets/templates/views/account/registry.tsx +378 -0
  48. package/assets/templates/views/account/schema.ts +52 -0
  49. package/assets/templates/views/category/assembly/assembly.json +28 -0
  50. package/assets/templates/views/category/assembly/index.skeleton.tsx +193 -0
  51. package/assets/templates/views/category/assembly/registry.skeleton.tsx +33 -0
  52. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/catalog.part +5 -0
  53. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/element.part +34 -0
  54. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/registry.part +71 -0
  55. package/assets/templates/views/category/content.ts +30 -0
  56. package/assets/templates/views/category/data.ts +12 -0
  57. package/assets/templates/views/category/index.tsx +230 -0
  58. package/assets/templates/views/category/props.ts +16 -0
  59. package/assets/templates/views/category/registry.tsx +103 -0
  60. package/assets/templates/views/category/schema.ts +78 -0
  61. package/assets/templates/views/checkout/content.ts +105 -0
  62. package/assets/templates/views/checkout/data.ts +23 -0
  63. package/assets/templates/views/checkout/index.tsx +111 -0
  64. package/assets/templates/views/checkout/props.ts +49 -0
  65. package/assets/templates/views/checkout/registry.tsx +217 -0
  66. package/assets/templates/views/checkout/schema.ts +229 -0
  67. package/assets/templates/views/home/assembly/assembly.json +24 -0
  68. package/assets/templates/views/home/assembly/index.skeleton.tsx +72 -0
  69. package/assets/templates/views/home/assembly/registry.skeleton.tsx +25 -0
  70. package/assets/templates/views/home/assembly/slots/benefits/Feature1/binding.json +5 -0
  71. package/assets/templates/views/home/assembly/slots/benefits/Feature1/catalog.part +4 -0
  72. package/assets/templates/views/home/assembly/slots/benefits/Feature1/element.part +4 -0
  73. package/assets/templates/views/home/assembly/slots/benefits/Feature1/registry.part +11 -0
  74. package/assets/templates/views/home/assembly/slots/benefits/Feature2/binding.json +5 -0
  75. package/assets/templates/views/home/assembly/slots/benefits/Feature2/catalog.part +4 -0
  76. package/assets/templates/views/home/assembly/slots/benefits/Feature2/element.part +4 -0
  77. package/assets/templates/views/home/assembly/slots/benefits/Feature2/registry.part +10 -0
  78. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/binding.json +5 -0
  79. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/catalog.part +4 -0
  80. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/element.part +5 -0
  81. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/registry.part +16 -0
  82. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/binding.json +5 -0
  83. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/catalog.part +4 -0
  84. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/element.part +5 -0
  85. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/registry.part +26 -0
  86. package/assets/templates/views/home/content.ts +70 -0
  87. package/assets/templates/views/home/data.ts +15 -0
  88. package/assets/templates/views/home/index.tsx +38 -84
  89. package/assets/templates/views/home/props.ts +22 -0
  90. package/assets/templates/views/home/registry.tsx +38 -22
  91. package/assets/templates/views/home/schema.ts +93 -0
  92. package/assets/templates/views/product/content.ts +25 -0
  93. package/assets/templates/views/product/data.ts +48 -0
  94. package/assets/templates/views/product/index.tsx +104 -0
  95. package/assets/templates/views/product/props.ts +20 -0
  96. package/assets/templates/views/product/registry.tsx +78 -0
  97. package/assets/templates/views/product/schema.ts +108 -0
  98. package/assets/templates/views/static/content.ts +99 -0
  99. package/assets/templates/views/static/index.tsx +60 -0
  100. package/assets/templates/views/static/registry.tsx +66 -0
  101. package/assets/templates/views/static/schema.ts +45 -0
  102. package/{skills/sf-view-gen/assets → assets}/templates/views/user-panel/index.tsx +8 -70
  103. package/{skills/sf-view-gen/assets → assets}/templates/views/user-panel/registry.tsx +23 -18
  104. package/assets/templates/views/user-panel/schema.ts +79 -0
  105. package/bin/storefront.js +1278 -79
  106. package/commands/sf-add-view.md +44 -13
  107. package/commands/sf-feature.md +28 -0
  108. package/commands/sf-theme.md +14 -8
  109. package/commands/sf-view.md +54 -12
  110. package/package.json +6 -4
  111. package/recipes/archetypes.json +28 -1
  112. package/recipes/catalog.json +361 -0
  113. package/recipes/catalog.mjs +128 -0
  114. package/recipes/catalog.schema.json +64 -0
  115. package/recipes/feature.schema.json +89 -0
  116. package/recipes/features.json +139 -0
  117. package/recipes/manifest.schema.json +69 -0
  118. package/recipes/operations.schema.json +43 -0
  119. package/recipes/recipe.schema.json +11 -2
  120. package/recipes/recipes.json +60 -5
  121. package/recipes/sections.json +24 -5
  122. package/recipes/validate.mjs +784 -32
  123. package/scripts/candidate.mjs +236 -0
  124. package/scripts/composers.mjs +140 -0
  125. package/scripts/file-classes.mjs +116 -0
  126. package/scripts/logical-controls.mjs +61 -0
  127. package/scripts/manifest.mjs +298 -0
  128. package/scripts/operation-registry.mjs +256 -0
  129. package/scripts/operations.mjs +720 -0
  130. package/scripts/ownership-conflict.mjs +356 -0
  131. package/scripts/pinning.mjs +504 -0
  132. package/scripts/plan-apply.mjs +843 -0
  133. package/scripts/preflight.mjs +51 -0
  134. package/scripts/reconcile.mjs +511 -0
  135. package/scripts/render-templates.mjs +245 -28
  136. package/scripts/schema-validate.mjs +132 -0
  137. package/scripts/section-compat.mjs +300 -0
  138. package/scripts/section-operations.mjs +508 -0
  139. package/scripts/sync-assets.mjs +22 -15
  140. package/scripts/view-assembly.mjs +291 -0
  141. package/scripts/view-selection.mjs +211 -0
  142. package/skills/sf-feature/SKILL.md +67 -0
  143. package/skills/sf-theme-gen/SKILL.md +12 -5
  144. package/skills/sf-view-gen/SKILL.md +123 -93
  145. package/skills/sf-view-gen/assets/recipes/archetypes.json +28 -1
  146. package/skills/sf-view-gen/assets/recipes/catalog.json +361 -0
  147. package/skills/sf-view-gen/assets/recipes/catalog.schema.json +64 -0
  148. package/skills/sf-view-gen/assets/recipes/features.json +139 -0
  149. package/skills/sf-view-gen/assets/recipes/recipes.json +60 -5
  150. package/skills/sf-view-gen/assets/recipes/sections.json +24 -5
  151. package/skills/sf-view-gen/assets/templates/views/sections/cta.client.tsx +25 -0
  152. package/skills/sf-view-gen/assets/templates/views/sections/cta.tsx +16 -0
  153. package/skills/sf-view-gen/assets/templates/views/sections/faq.client.tsx +13 -9
  154. package/skills/sf-view-gen/assets/templates/views/sections/faq.tsx +2 -7
  155. package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.client.tsx +12 -8
  156. package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.tsx +2 -7
  157. package/skills/sf-view-gen/assets/templates/views/sections/pricing.client.tsx +14 -10
  158. package/skills/sf-view-gen/assets/templates/views/sections/pricing.tsx +2 -9
  159. package/skills/sf-view-gen/assets/templates/views/sections/schema.cta.ts +19 -0
  160. package/skills/sf-view-gen/assets/templates/views/sections/schema.faq.ts +18 -0
  161. package/skills/sf-view-gen/assets/templates/views/sections/schema.logoCloud.ts +19 -0
  162. package/skills/sf-view-gen/assets/templates/views/sections/schema.pricing.ts +26 -0
  163. package/skills/sf-view-gen/assets/templates/views/sections/schema.statistics.ts +25 -0
  164. package/skills/sf-view-gen/assets/templates/views/sections/schema.testimonials.ts +23 -0
  165. package/skills/sf-view-gen/assets/templates/views/sections/statistics.client.tsx +16 -8
  166. package/skills/sf-view-gen/assets/templates/views/sections/statistics.tsx +2 -7
  167. package/skills/sf-view-gen/assets/templates/views/sections/testimonials.client.tsx +13 -9
  168. package/skills/sf-view-gen/assets/templates/views/sections/testimonials.tsx +2 -5
  169. package/skills/sf-view-gen/references/views.md +44 -10
  170. package/skills/sf-view-gen/scripts/catalog.mjs +128 -0
  171. package/skills/sf-view-gen/scripts/schema-validate.mjs +132 -0
  172. package/skills/sf-view-gen/scripts/validate.mjs +784 -32
  173. package/skills/storefront-workflow/SKILL.md +116 -0
  174. package/skills/sf-view-gen/assets/templates/views/category/index.tsx +0 -86
  175. package/skills/sf-view-gen/assets/templates/views/category/registry.tsx +0 -48
  176. package/skills/sf-view-gen/assets/templates/views/checkout/index.tsx +0 -192
  177. package/skills/sf-view-gen/assets/templates/views/checkout/registry.tsx +0 -45
  178. package/skills/sf-view-gen/assets/templates/views/product/index.tsx +0 -122
  179. package/skills/sf-view-gen/assets/templates/views/product/registry.tsx +0 -49
@@ -0,0 +1,108 @@
1
+ // Thor Commerce view schema fragment: product
2
+ // Zod mirrors of the block contracts for this view — the single source for the
3
+ // catalog entries and the client-side registry parsing (json-render does not
4
+ // validate props against the catalog itself, so the registry parses here).
5
+ // Kept free of React and server imports so the Server Component module and the
6
+ // 'use client' registry can share one contract. Block zod contracts are
7
+ // mirrored, not imported: @thorprovider/blocks ships zod v3 while the scaffold
8
+ // runs zod v4 and cross-major schema assignment breaks the catalog contract.
9
+
10
+ import { z } from 'zod'
11
+
12
+ const imageSchema = z.object({
13
+ id: z.string().optional(),
14
+ url: z.string(),
15
+ altText: z.string(),
16
+ width: z.number().optional(),
17
+ height: z.number().optional(),
18
+ })
19
+
20
+ // Canonical Money: a decimal string in major units ("69.99"), never cents.
21
+ const moneySchema = z.object({
22
+ amount: z.string(),
23
+ currencyCode: z.string(),
24
+ })
25
+
26
+ const selectedOptionSchema = z.object({
27
+ name: z.string(),
28
+ value: z.string(),
29
+ })
30
+
31
+ // Mirrors ProductDetailVariant: `price` is absent/null when the backend has no
32
+ // resolved price (the block disables purchase); a valid zero stays "0".
33
+ const productDetailVariantSchema = z.object({
34
+ id: z.string(),
35
+ title: z.string(),
36
+ sku: z.string().optional(),
37
+ availableForSale: z.boolean(),
38
+ selectedOptions: z.array(selectedOptionSchema),
39
+ price: moneySchema.nullish(),
40
+ compareAtPrice: moneySchema.optional(),
41
+ image: imageSchema.optional(),
42
+ })
43
+
44
+ // Mirrors ProductDetailProduct (the ProductOverview1 block contract).
45
+ export const productDetailProductSchema = z.object({
46
+ id: z.string(),
47
+ handle: z.string().optional(),
48
+ title: z.string(),
49
+ description: z.string().optional(),
50
+ vendor: z.string().optional(),
51
+ options: z.array(z.object({ id: z.string(), name: z.string(), values: z.array(z.string()) })),
52
+ variants: z.array(productDetailVariantSchema),
53
+ images: z.array(imageSchema),
54
+ featuredImage: imageSchema.optional(),
55
+ })
56
+
57
+ export const productOverviewPropsSchema = z.object({
58
+ product: productDetailProductSchema,
59
+ selectedVariantId: z.string().nullable().optional(),
60
+ quantity: z.number().optional(),
61
+ sizesLabel: z.string().optional(),
62
+ variantLabel: z.string().optional(),
63
+ reviewsLabel: z.string().optional(),
64
+ addToCartLabel: z.string().optional(),
65
+ outOfStockLabel: z.string().optional(),
66
+ unavailablePriceLabel: z.string().optional(),
67
+ selectVariantLabel: z.string().optional(),
68
+ quantityLabel: z.string().optional(),
69
+ })
70
+
71
+ // Mirrors ReviewRating1Review (real reviews only; never seeded).
72
+ export const reviewSchema = z.object({
73
+ id: z.number(),
74
+ userName: z.string(),
75
+ userAvatar: z.string().optional(),
76
+ date: z.string(),
77
+ rating: z.number(),
78
+ content: z.string(),
79
+ likes: z.number().optional(),
80
+ comments: z.number().optional(),
81
+ })
82
+
83
+ export const reviewFormSchema = z.object({
84
+ title: z.string(),
85
+ description: z.string(),
86
+ nameLabel: z.string(),
87
+ emailLabel: z.string(),
88
+ reviewLabel: z.string(),
89
+ namePlaceholder: z.string(),
90
+ emailPlaceholder: z.string(),
91
+ reviewPlaceholder: z.string(),
92
+ submitButtonText: z.string(),
93
+ submittingButtonText: z.string(),
94
+ })
95
+
96
+ export const reviewsPropsSchema = z.object({
97
+ pageTitle: z.string(),
98
+ pageDescription: z.string().optional(),
99
+ reviews: z.array(reviewSchema),
100
+ form: reviewFormSchema.optional(),
101
+ reviewsPerPage: z.number().optional(),
102
+ })
103
+
104
+ export type ProductDetailProductSpec = z.infer<typeof productDetailProductSchema>
105
+ export type ProductOverviewProps = z.infer<typeof productOverviewPropsSchema>
106
+ export type Review = z.infer<typeof reviewSchema>
107
+ export type ReviewForm = z.infer<typeof reviewFormSchema>
108
+ export type ReviewsProps = z.infer<typeof reviewsPropsSchema>
@@ -0,0 +1,99 @@
1
+ // Thor Commerce view content: static
2
+ // User-owned content — reconcile seeds this file once and never rewrites it, so
3
+ // your edits survive `storefront sync`. Declare the pages this store serves and
4
+ // the ordered sections each one renders. The derived view builds one fixed route
5
+ // per declared page, so an undeclared path falls through to the not-found
6
+ // response.
7
+ //
8
+ // Neutral, truthful copy only: these pages make no delivery, shipping, or price
9
+ // promise and invent no company facts. Replace the copy with your own before
10
+ // publishing.
11
+
12
+ import type { StaticPageContent } from './schema'
13
+
14
+ export const staticPagesContent: StaticPageContent[] = [
15
+ {
16
+ path: '/contacto',
17
+ title: 'Contacto',
18
+ description: 'Cómo comunicarte con la tienda.',
19
+ sections: [
20
+ { kind: 'heading', text: 'Contacto' },
21
+ {
22
+ kind: 'paragraph',
23
+ text: 'Esta tienda atiende en línea. Usa los canales de atención que la tienda publique para resolver tus dudas sobre productos, pedidos y pagos.',
24
+ },
25
+ {
26
+ kind: 'list',
27
+ items: [
28
+ 'Consulta el estado de tu pedido desde tu cuenta.',
29
+ 'Escríbenos por el canal de atención que la tienda publique.',
30
+ 'Revisa las preguntas frecuentes antes de escribirnos.',
31
+ ],
32
+ },
33
+ ],
34
+ },
35
+ {
36
+ path: '/terminos',
37
+ title: 'Términos y condiciones',
38
+ description: 'Condiciones de uso de la tienda en línea.',
39
+ sections: [
40
+ { kind: 'heading', text: 'Términos y condiciones' },
41
+ {
42
+ kind: 'paragraph',
43
+ text: 'Al usar esta tienda en línea aceptas estas condiciones de uso. Léelas con atención antes de realizar una compra.',
44
+ },
45
+ {
46
+ kind: 'paragraph',
47
+ text: 'La tienda puede actualizar estos términos para reflejar cambios en el servicio. La versión vigente es la publicada en esta página.',
48
+ },
49
+ {
50
+ kind: 'list',
51
+ items: [
52
+ 'Usa la tienda de forma lícita y con información veraz.',
53
+ 'El catálogo, los precios y la disponibilidad se muestran en la tienda y pueden cambiar.',
54
+ 'El proceso de compra se completa siguiendo los pasos indicados en el sitio.',
55
+ ],
56
+ },
57
+ ],
58
+ },
59
+ {
60
+ path: '/privacidad',
61
+ title: 'Política de privacidad',
62
+ description: 'Cómo se tratan los datos en la tienda en línea.',
63
+ sections: [
64
+ { kind: 'heading', text: 'Política de privacidad' },
65
+ {
66
+ kind: 'paragraph',
67
+ text: 'Esta política describe cómo la tienda trata la información que proporcionas al navegar y al comprar.',
68
+ },
69
+ {
70
+ kind: 'paragraph',
71
+ text: 'Usamos tus datos para gestionar tu cuenta, procesar tus pedidos y darte soporte. No los usamos para fines distintos sin tu consentimiento.',
72
+ },
73
+ {
74
+ kind: 'list',
75
+ items: [
76
+ 'Proporciona solo la información necesaria para completar tu compra.',
77
+ 'Puedes solicitar la actualización o eliminación de tus datos por los canales de atención.',
78
+ 'Conserva tus credenciales de acceso de forma segura.',
79
+ ],
80
+ },
81
+ ],
82
+ },
83
+ {
84
+ path: '/sobre-nosotros',
85
+ title: 'Sobre nosotros',
86
+ description: 'Información sobre la tienda.',
87
+ sections: [
88
+ { kind: 'heading', text: 'Sobre nosotros' },
89
+ {
90
+ kind: 'paragraph',
91
+ text: 'Somos una tienda en línea que ofrece un catálogo de productos para tu hogar y tu negocio.',
92
+ },
93
+ {
94
+ kind: 'paragraph',
95
+ text: 'Trabajamos para que encuentres lo que buscas y para acompañarte durante todo el proceso de compra a través de esta tienda.',
96
+ },
97
+ ],
98
+ },
99
+ ]
@@ -0,0 +1,60 @@
1
+ // Thor Commerce view module: static
2
+ // Server Component module: elements + catalog entries + routes + loader only.
3
+ // The client-side component mapping lives in ./registry.tsx ('use client'); this
4
+ // module imports it as data so the RSC graph never evaluates @json-render/react
5
+ // on the server. Registered by importing it in ../index.ts. Recipe: recipes.json -> "static".
6
+ //
7
+ // The pages are declared in the user-owned ./content.ts. This module builds one
8
+ // fixed route per declared page (each with its own metadata); an undeclared path
9
+ // is never declared here, so it falls through to the storefront not-found
10
+ // response.
11
+
12
+ import { getDataSource } from '../../store-context'
13
+ import { pageRoot } from '../../spec/page-root'
14
+ import type { StorefrontElement } from '../../spec/types'
15
+ import type { ViewModule, ViewRoute } from '../types'
16
+ import { registry } from './registry'
17
+ import { staticPagesContent } from './content'
18
+ import { staticContentPropsSchema } from './schema'
19
+
20
+ // A single catalog/registry declaration for the local StaticContent element. The
21
+ // per-route props (the page's sections) are supplied in each route's entry below.
22
+ const elements: Record<string, StorefrontElement> = {
23
+ 'static.content': {
24
+ type: 'StaticContent',
25
+ props: {},
26
+ },
27
+ }
28
+
29
+ const catalog = {
30
+ StaticContent: {
31
+ props: staticContentPropsSchema,
32
+ description: 'Renders declared static page sections.',
33
+ },
34
+ }
35
+
36
+ // A stable, unique root id per page: `/contacto` -> `static-contacto`,
37
+ // `/` -> `static-index`.
38
+ const pageRootId = (path: string): string =>
39
+ `static-${path.replace(/^\//, '').replace(/\//g, '-') || 'index'}`
40
+
41
+ const route: ViewRoute[] = staticPagesContent.map((page) => ({
42
+ path: page.path,
43
+ loaderName: 'static',
44
+ layout: 'default',
45
+ metadata: { title: page.title, description: page.description },
46
+ entry: pageRoot(pageRootId(page.path), ['static.content'], {
47
+ 'static.content': { type: 'StaticContent', props: { sections: page.sections } },
48
+ }),
49
+ }))
50
+
51
+ export const staticView: ViewModule = {
52
+ name: 'static',
53
+ elements,
54
+ catalog,
55
+ registry,
56
+ route,
57
+ loader: {
58
+ static: async () => ({ dataSource: getDataSource(), dataError: null }),
59
+ },
60
+ }
@@ -0,0 +1,66 @@
1
+ 'use client'
2
+
3
+ // Thor Commerce view registry fragment: static
4
+ // Client boundary for the static view. The local StaticContent component renders
5
+ // the declared sections with semantic HTML and storefront tokens; it reads no
6
+ // backend data, so it is a pure, Server-Component-safe client component.
7
+
8
+ import type { ViewRegistry } from '../types'
9
+ import { staticContentPropsSchema, type StaticSection } from './schema'
10
+
11
+ function Section({ section }: { section: StaticSection }) {
12
+ switch (section.kind) {
13
+ case 'heading':
14
+ return <h2 className="text-2xl font-semibold tracking-tight">{section.text}</h2>
15
+ case 'paragraph':
16
+ return <p className="text-muted-foreground leading-relaxed">{section.text}</p>
17
+ case 'list':
18
+ return (
19
+ <ul className="list-disc space-y-1 pl-6">
20
+ {section.items.map((item, index) => (
21
+ <li key={index}>{item}</li>
22
+ ))}
23
+ </ul>
24
+ )
25
+ case 'links':
26
+ return (
27
+ <ul className="space-y-1">
28
+ {section.items.map((item, index) => (
29
+ <li key={index}>
30
+ <a className="text-primary underline-offset-4 hover:underline" href={item.href}>
31
+ {item.label}
32
+ </a>
33
+ </li>
34
+ ))}
35
+ </ul>
36
+ )
37
+ case 'contact':
38
+ return (
39
+ <dl className="space-y-2">
40
+ {section.entries.map((entry, index) => (
41
+ <div key={index} className="flex flex-col">
42
+ <dt className="text-muted-foreground text-sm">{entry.label}</dt>
43
+ <dd>{entry.value}</dd>
44
+ </div>
45
+ ))}
46
+ </dl>
47
+ )
48
+ }
49
+ }
50
+
51
+ function StaticContent({ sections }: { sections: StaticSection[] }) {
52
+ return (
53
+ <div className="mx-auto w-full max-w-3xl px-4 py-12 sm:px-6 lg:px-8 space-y-6">
54
+ {sections.map((section, index) => (
55
+ <Section key={index} section={section} />
56
+ ))}
57
+ </div>
58
+ )
59
+ }
60
+
61
+ export const registry: ViewRegistry = {
62
+ StaticContent: ({ props }) => {
63
+ const parsed = staticContentPropsSchema.parse(props)
64
+ return <StaticContent sections={parsed.sections} />
65
+ },
66
+ }
@@ -0,0 +1,45 @@
1
+ // Thor Commerce view schema fragment: static
2
+ // Zod contract for content-driven static pages. The user-owned content declares
3
+ // each page (path, title, optional description, ordered sections); the derived
4
+ // index builds one fixed route per page and the client registry renders the
5
+ // sections. Kept free of React and server imports so the Server Component module
6
+ // and the 'use client' registry share one contract.
7
+
8
+ import { z } from 'zod'
9
+
10
+ // One rendered block inside a static page. A discriminated union keeps each
11
+ // section's props explicit and lets the registry switch on `kind`.
12
+ export const staticSectionSchema = z.discriminatedUnion('kind', [
13
+ z.object({ kind: z.literal('heading'), text: z.string() }),
14
+ z.object({ kind: z.literal('paragraph'), text: z.string() }),
15
+ z.object({ kind: z.literal('list'), items: z.array(z.string()) }),
16
+ z.object({
17
+ kind: z.literal('links'),
18
+ items: z.array(z.object({ label: z.string(), href: z.string() })),
19
+ }),
20
+ z.object({
21
+ kind: z.literal('contact'),
22
+ entries: z.array(z.object({ label: z.string(), value: z.string() })),
23
+ }),
24
+ ])
25
+
26
+ // A declared page: an absolute route path, its document metadata, and the
27
+ // ordered sections to render.
28
+ export const staticPageSchema = z.object({
29
+ path: z.string().startsWith('/'),
30
+ title: z.string(),
31
+ description: z.string().optional(),
32
+ sections: z.array(staticSectionSchema),
33
+ })
34
+
35
+ // The user-owned content is a list of pages.
36
+ export const staticPagesContentSchema = z.array(staticPageSchema)
37
+
38
+ // The props a single `StaticContent` element carries on a route.
39
+ export const staticContentPropsSchema = z.object({
40
+ sections: z.array(staticSectionSchema),
41
+ })
42
+
43
+ export type StaticSection = z.infer<typeof staticSectionSchema>
44
+ export type StaticPageContent = z.infer<typeof staticPageSchema>
45
+ export type StaticContentProps = z.infer<typeof staticContentPropsSchema>
@@ -5,11 +5,11 @@
5
5
  // on the server. Registered by importing it in ../index.ts. Recipe: recipes.json -> "user-panel".
6
6
 
7
7
  import { getCatalog } from '@thorprovider/runtime/commerce'
8
- import { z } from 'zod'
9
8
  import { pageRoot } from '../../spec/page-root'
10
9
  import type { StorefrontElement } from '../../spec/types'
11
10
  import type { ViewModule } from '../types'
12
11
  import { registry } from './registry'
12
+ import { appShellPropsSchema, orderHistoryPropsSchema } from './schema'
13
13
 
14
14
  const elements: Record<string, StorefrontElement> = {
15
15
  'userPanel.shell': {
@@ -55,73 +55,14 @@ const elements: Record<string, StorefrontElement> = {
55
55
  },
56
56
  }
57
57
 
58
- const ctaSchema = z.object({ label: z.string(), href: z.string().optional() })
59
-
60
- const appShellNavItemSchema = z.object({
61
- label: z.string(),
62
- href: z.string(),
63
- icon: z.enum([
64
- 'layout-dashboard',
65
- 'package',
66
- 'user',
67
- 'settings',
68
- 'home',
69
- 'shopping-cart',
70
- 'bar-chart-3',
71
- 'users',
72
- 'folder-kanban',
73
- ]),
74
- active: z.boolean().optional(),
75
- })
76
-
77
- const appShellUserSchema = z.object({
78
- name: z.string(),
79
- email: z.string(),
80
- initials: z.string(),
81
- })
82
-
83
- const breadcrumbItemSchema = z.object({
84
- label: z.string(),
85
- href: z.string().optional(),
86
- })
87
-
88
- const statSchema = z.object({
89
- label: z.string(),
90
- value: z.string(),
91
- })
92
-
93
- const orderLineSchema = z.object({
94
- name: z.string(),
95
- image: z.string(),
96
- color: z.string(),
97
- size: z.string(),
98
- quantity: z.number(),
99
- price: z.number(),
100
- orderDate: z.string(),
101
- })
102
-
103
58
  const catalog = {
104
59
  AppShell: {
105
- props: z.object({
106
- brandName: z.string().optional(),
107
- navGroupLabel: z.string().optional(),
108
- navItems: z.array(appShellNavItemSchema).optional(),
109
- user: appShellUserSchema.optional(),
110
- breadcrumb: z.array(breadcrumbItemSchema).optional(),
111
- pageTitle: z.string().optional(),
112
- pageDescription: z.string().optional(),
113
- action: ctaSchema.optional(),
114
- stats: z.array(statSchema).optional(),
115
- }),
60
+ props: appShellPropsSchema,
116
61
  slots: ['default'],
117
62
  description: 'Panel de cuenta con navegación lateral, cabecera y área de contenido.',
118
63
  },
119
64
  OrderHistory: {
120
- props: z.object({
121
- orders: z.array(orderLineSchema),
122
- title: z.string().optional(),
123
- description: z.string().optional(),
124
- }),
65
+ props: orderHistoryPropsSchema,
125
66
  description: 'Historial de pedidos del cliente.',
126
67
  },
127
68
  }
@@ -155,14 +96,11 @@ export const userPanel: ViewModule = {
155
96
  price: product.price,
156
97
  orderDate,
157
98
  }))
158
- const lastOrder = {
159
- number: `TC-${placedAt.getFullYear()}0001`,
160
- placedAt: placedAt.toISOString(),
161
- email: 'cliente@ejemplo.com',
162
- status: 'Confirmado',
163
- total: lastOrderLines.reduce((sum, line) => sum + line.price * line.quantity, 0),
164
- }
165
- return { lastOrder, lastOrderLines, orderPlaced: lastOrderLines.length > 0 }
99
+ // Only lastOrderLines is bound by this view's elements. lastOrder and
100
+ // orderPlaced belong to the checkout contract (StorefrontOrder shape and
101
+ // an ISO timestamp); writing demo values in a foreign shape here would
102
+ // leak into /checkout state and crash CheckoutConfirmation.
103
+ return { lastOrderLines }
166
104
  },
167
105
  },
168
106
  }
@@ -11,31 +11,36 @@ import {
11
11
  OrderHistory1 as OrderHistoryBlock,
12
12
  } from '@thorprovider/blocks'
13
13
  import { useStateStore } from '@json-render/react'
14
+ import { appShellPropsSchema, orderHistoryPropsSchema } from './schema'
14
15
  import type { ViewRegistry } from '../types'
15
16
 
16
17
  export const registry: ViewRegistry = {
17
- AppShell: ({ props, children }) => (
18
- <AppShellBlock
19
- brandName={props.brandName as string}
20
- navGroupLabel={props.navGroupLabel as string}
21
- navItems={props.navItems as never}
22
- user={props.user as never}
23
- breadcrumb={props.breadcrumb as never}
24
- pageTitle={props.pageTitle as string}
25
- pageDescription={props.pageDescription as string}
26
- action={props.action as never}
27
- stats={props.stats as never}
28
- >
29
- {children}
30
- </AppShellBlock>
31
- ),
18
+ AppShell: ({ props, children }) => {
19
+ const parsed = appShellPropsSchema.parse(props)
20
+ return (
21
+ <AppShellBlock
22
+ brandName={parsed.brandName}
23
+ navGroupLabel={parsed.navGroupLabel}
24
+ navItems={parsed.navItems}
25
+ user={parsed.user}
26
+ breadcrumb={parsed.breadcrumb}
27
+ pageTitle={parsed.pageTitle}
28
+ pageDescription={parsed.pageDescription}
29
+ action={parsed.action}
30
+ stats={parsed.stats}
31
+ >
32
+ {children}
33
+ </AppShellBlock>
34
+ )
35
+ },
32
36
  OrderHistory: ({ props, emit }) => {
37
+ const parsed = orderHistoryPropsSchema.parse(props)
33
38
  const { set } = useStateStore()
34
39
  return (
35
40
  <OrderHistoryBlock
36
- orders={props.orders as never}
37
- title={props.title as string}
38
- description={props.description as string}
41
+ orders={parsed.orders}
42
+ title={parsed.title}
43
+ description={parsed.description}
39
44
  onViewDetails={() => {
40
45
  set('/orderDetailsOpen', true)
41
46
  emit('viewOrder')
@@ -0,0 +1,79 @@
1
+ // Thor Commerce view schema fragment: user-panel
2
+ // Zod mirrors of the block contracts for this view — the single source for the
3
+ // catalog entries and the client-side registry parsing (json-render does not
4
+ // validate props against the catalog itself, so the registry parses here).
5
+ // Kept free of React and server imports so the Server Component module and the
6
+ // 'use client' registry can share one contract.
7
+
8
+ import { z } from 'zod'
9
+
10
+ export const ctaSchema = z.object({ label: z.string(), href: z.string().optional() })
11
+
12
+ // Mirrors the AppShell1NavItem block contract: `icon` is a NAME of the
13
+ // AppShell1NavIcon set — the block resolves it to a component internally.
14
+ export const appShellNavItemSchema = z.object({
15
+ label: z.string(),
16
+ href: z.string(),
17
+ icon: z.enum([
18
+ 'layout-dashboard',
19
+ 'package',
20
+ 'user',
21
+ 'settings',
22
+ 'home',
23
+ 'shopping-cart',
24
+ 'bar-chart-3',
25
+ 'users',
26
+ 'folder-kanban',
27
+ ]),
28
+ active: z.boolean().optional(),
29
+ })
30
+
31
+ export const appShellUserSchema = z.object({
32
+ name: z.string(),
33
+ email: z.string(),
34
+ initials: z.string(),
35
+ })
36
+
37
+ export const breadcrumbItemSchema = z.object({
38
+ label: z.string(),
39
+ href: z.string().optional(),
40
+ })
41
+
42
+ export const statSchema = z.object({
43
+ label: z.string(),
44
+ value: z.string(),
45
+ })
46
+
47
+ // Mirrors the OrderHistory1Order block contract.
48
+ export const orderLineSchema = z.object({
49
+ name: z.string(),
50
+ image: z.string(),
51
+ color: z.string(),
52
+ size: z.string(),
53
+ quantity: z.number(),
54
+ price: z.number(),
55
+ orderDate: z.string(),
56
+ })
57
+
58
+ export const appShellPropsSchema = z.object({
59
+ brandName: z.string().optional(),
60
+ navGroupLabel: z.string().optional(),
61
+ navItems: z.array(appShellNavItemSchema).optional(),
62
+ user: appShellUserSchema.optional(),
63
+ breadcrumb: z.array(breadcrumbItemSchema).optional(),
64
+ pageTitle: z.string().optional(),
65
+ pageDescription: z.string().optional(),
66
+ action: ctaSchema.optional(),
67
+ stats: z.array(statSchema).optional(),
68
+ })
69
+
70
+ export const orderHistoryPropsSchema = z.object({
71
+ orders: z.array(orderLineSchema),
72
+ title: z.string().optional(),
73
+ description: z.string().optional(),
74
+ })
75
+
76
+ export type AppShellNavItem = z.infer<typeof appShellNavItemSchema>
77
+ export type AppShellProps = z.infer<typeof appShellPropsSchema>
78
+ export type OrderLine = z.infer<typeof orderLineSchema>
79
+ export type OrderHistoryProps = z.infer<typeof orderHistoryPropsSchema>