@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.
Files changed (263) hide show
  1. package/README.md +321 -152
  2. package/VENDOR.md +69 -0
  3. package/assets/openspec/config.yaml +38 -0
  4. package/assets/templates/AGENTS.md +76 -0
  5. package/assets/templates/app/app/[[...slug]]/page.tsx +54 -2
  6. package/assets/templates/app/app/[[...slug]]/renderer.tsx +13 -2
  7. package/assets/templates/app/app/globals.css +5 -8
  8. package/assets/templates/app/app/layout.tsx +17 -7
  9. package/assets/templates/app/app/robots.ts +19 -0
  10. package/assets/templates/app/app/sitemap.ts +71 -0
  11. package/assets/templates/app/lib/__STOREFRONT__/actions.ts +612 -0
  12. package/assets/templates/app/lib/__STOREFRONT__/cart-state.ts +66 -0
  13. package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +50 -57
  14. package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +679 -0
  15. package/assets/templates/app/lib/__STOREFRONT__/composition.ts +26 -0
  16. package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +197 -0
  17. package/assets/templates/app/lib/__STOREFRONT__/handlers.ts +5 -36
  18. package/assets/templates/app/lib/__STOREFRONT__/icon-map.ts +27 -0
  19. package/assets/templates/app/lib/__STOREFRONT__/mappers.ts +13 -11
  20. package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +264 -51
  21. package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +6 -3
  22. package/assets/templates/app/lib/__STOREFRONT__/schema.ts +136 -0
  23. package/assets/templates/app/lib/__STOREFRONT__/section-observation.ts +182 -0
  24. package/assets/templates/app/lib/__STOREFRONT__/section-sources.ts +130 -0
  25. package/assets/templates/app/lib/__STOREFRONT__/session.core.ts +470 -0
  26. package/assets/templates/app/lib/__STOREFRONT__/session.ts +169 -0
  27. package/assets/templates/app/lib/__STOREFRONT__/shell/content.ts +120 -0
  28. package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +38 -0
  29. package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +129 -10
  30. package/assets/templates/app/lib/__STOREFRONT__/spec/page-root.ts +5 -1
  31. package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +191 -0
  32. package/assets/templates/app/lib/__STOREFRONT__/state.core.ts +257 -0
  33. package/assets/templates/app/lib/__STOREFRONT__/state.ts +7 -31
  34. package/assets/templates/app/lib/__STOREFRONT__/store-context.ts +161 -0
  35. package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.client.tsx +25 -0
  36. package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.tsx +16 -0
  37. package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.client.tsx +24 -0
  38. package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.tsx +5 -18
  39. package/assets/templates/app/lib/__STOREFRONT__/views/sections/index.ts +18 -0
  40. package/assets/templates/app/lib/__STOREFRONT__/views/sections/logoCloud.tsx +26 -14
  41. package/assets/templates/app/lib/__STOREFRONT__/views/sections/pricing.tsx +36 -18
  42. package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.client.tsx +100 -0
  43. package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.tsx +19 -0
  44. package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.cta.ts +19 -0
  45. package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.faq.ts +18 -0
  46. package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.products.ts +39 -0
  47. package/assets/templates/app/lib/__STOREFRONT__/views/sections/statistics.tsx +35 -14
  48. package/assets/templates/app/lib/__STOREFRONT__/views/sections/testimonials.tsx +31 -13
  49. package/assets/templates/app/lib/__STOREFRONT__/views/types.ts +5 -3
  50. package/assets/templates/features/checkout/handlers.ts +159 -0
  51. package/assets/templates/features/checkout/state.ts +61 -0
  52. package/assets/templates/features/customer-auth/handlers.ts +59 -0
  53. package/assets/templates/features/customer-auth/state.ts +45 -0
  54. package/assets/templates/features/recommendations/state.ts +11 -0
  55. package/assets/templates/views/account/content.ts +35 -0
  56. package/assets/templates/views/account/index.tsx +114 -0
  57. package/assets/templates/views/account/registry.tsx +439 -0
  58. package/assets/templates/views/account/schema.ts +53 -0
  59. package/assets/templates/views/cart/content.ts +11 -0
  60. package/assets/templates/views/cart/data.ts +23 -0
  61. package/assets/templates/views/cart/index.tsx +53 -0
  62. package/assets/templates/views/cart/props.ts +12 -0
  63. package/assets/templates/views/cart/registry.tsx +81 -0
  64. package/assets/templates/views/cart/schema.ts +52 -0
  65. package/assets/templates/views/category/assembly/assembly.json +29 -0
  66. package/assets/templates/views/category/assembly/index.skeleton.tsx +251 -0
  67. package/assets/templates/views/category/assembly/registry.skeleton.tsx +34 -0
  68. package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/binding.json +5 -0
  69. package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/catalog.part +4 -0
  70. package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/element.part +7 -0
  71. package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/registry.part +18 -0
  72. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/catalog.part +5 -0
  73. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/element.part +34 -0
  74. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/registry.part +71 -0
  75. package/assets/templates/views/category/content.ts +30 -0
  76. package/assets/templates/views/category/data.ts +12 -0
  77. package/assets/templates/views/category/index.tsx +299 -0
  78. package/assets/templates/views/category/props.ts +23 -0
  79. package/assets/templates/views/category/registry.tsx +122 -0
  80. package/assets/templates/views/category/schema.ts +91 -0
  81. package/assets/templates/views/checkout/content.ts +105 -0
  82. package/assets/templates/views/checkout/data.ts +23 -0
  83. package/assets/templates/views/checkout/index.tsx +111 -0
  84. package/assets/templates/views/checkout/props.ts +49 -0
  85. package/assets/templates/views/checkout/registry.tsx +217 -0
  86. package/assets/templates/views/checkout/schema.ts +229 -0
  87. package/assets/templates/views/home/assembly/assembly.json +33 -0
  88. package/assets/templates/views/home/assembly/index.skeleton.tsx +90 -0
  89. package/assets/templates/views/home/assembly/registry.skeleton.tsx +25 -0
  90. package/assets/templates/views/home/assembly/slots/benefits/Feature1/binding.json +5 -0
  91. package/assets/templates/views/home/assembly/slots/benefits/Feature1/catalog.part +4 -0
  92. package/assets/templates/views/home/assembly/slots/benefits/Feature1/element.part +4 -0
  93. package/assets/templates/views/home/assembly/slots/benefits/Feature1/registry.part +11 -0
  94. package/assets/templates/views/home/assembly/slots/benefits/Feature2/binding.json +5 -0
  95. package/assets/templates/views/home/assembly/slots/benefits/Feature2/catalog.part +4 -0
  96. package/assets/templates/views/home/assembly/slots/benefits/Feature2/element.part +4 -0
  97. package/assets/templates/views/home/assembly/slots/benefits/Feature2/registry.part +10 -0
  98. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/binding.json +5 -0
  99. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/catalog.part +4 -0
  100. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/element.part +5 -0
  101. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/registry.part +16 -0
  102. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/binding.json +5 -0
  103. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/catalog.part +4 -0
  104. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/element.part +5 -0
  105. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/registry.part +26 -0
  106. package/assets/templates/views/home/content.ts +70 -0
  107. package/assets/templates/views/home/data.ts +24 -0
  108. package/assets/templates/views/home/index.tsx +57 -85
  109. package/assets/templates/views/home/props.ts +22 -0
  110. package/assets/templates/views/home/registry.tsx +38 -22
  111. package/assets/templates/views/home/schema.ts +93 -0
  112. package/assets/templates/views/product/content.ts +25 -0
  113. package/assets/templates/views/product/data.ts +50 -0
  114. package/assets/templates/views/product/index.tsx +148 -0
  115. package/assets/templates/views/product/props.ts +27 -0
  116. package/assets/templates/views/product/registry.tsx +103 -0
  117. package/assets/templates/views/product/schema.ts +121 -0
  118. package/assets/templates/views/static/content.ts +99 -0
  119. package/assets/templates/views/static/index.tsx +60 -0
  120. package/assets/templates/views/static/registry.tsx +66 -0
  121. package/assets/templates/views/static/schema.ts +45 -0
  122. package/bin/storefront.js +2424 -148
  123. package/examples/merchandising/category-led.json +37 -0
  124. package/examples/merchandising/collection-led.json +37 -0
  125. package/package.json +15 -9
  126. package/recipes/archetypes.json +28 -14
  127. package/recipes/catalog.json +400 -0
  128. package/recipes/catalog.mjs +128 -0
  129. package/recipes/catalog.schema.json +64 -0
  130. package/recipes/feature.schema.json +89 -0
  131. package/recipes/features.json +162 -0
  132. package/recipes/manifest.schema.json +69 -0
  133. package/recipes/operations.schema.json +43 -0
  134. package/recipes/recipe.schema.json +11 -2
  135. package/recipes/recipes.json +76 -19
  136. package/recipes/sections.json +34 -5
  137. package/recipes/validate.mjs +838 -32
  138. package/scripts/candidate.mjs +236 -0
  139. package/scripts/composers.mjs +160 -0
  140. package/scripts/env-file.mjs +86 -0
  141. package/scripts/file-classes.mjs +121 -0
  142. package/scripts/logical-controls.mjs +61 -0
  143. package/scripts/manifest.mjs +298 -0
  144. package/scripts/migrate-english-routes.mjs +276 -0
  145. package/scripts/operation-registry.mjs +256 -0
  146. package/scripts/operations.mjs +720 -0
  147. package/scripts/ownership-conflict.mjs +356 -0
  148. package/scripts/pinning.mjs +573 -0
  149. package/scripts/plan-apply.mjs +987 -0
  150. package/scripts/preflight.mjs +51 -0
  151. package/scripts/reconcile.mjs +549 -0
  152. package/scripts/render-templates.mjs +338 -68
  153. package/scripts/schema-validate.mjs +132 -0
  154. package/scripts/section-compat.mjs +300 -0
  155. package/scripts/section-config.mjs +253 -0
  156. package/scripts/section-operations.mjs +508 -0
  157. package/scripts/starter.mjs +17 -0
  158. package/scripts/sync-assets.mjs +22 -15
  159. package/scripts/theme/color.mjs +185 -0
  160. package/scripts/theme/css-theme.mjs +233 -0
  161. package/scripts/theme/fonts.mjs +123 -0
  162. package/scripts/theme/import-export.mjs +125 -0
  163. package/scripts/theme/preset.mjs +93 -0
  164. package/scripts/theme/theme-plan.mjs +292 -0
  165. package/scripts/theme/tokens.mjs +137 -0
  166. package/scripts/view-assembly.mjs +337 -0
  167. package/scripts/view-selection.mjs +211 -0
  168. package/skills/sf-feature/SKILL.md +80 -0
  169. package/skills/sf-setup/SKILL.md +79 -0
  170. package/skills/sf-theme-gen/SKILL.md +100 -30
  171. package/skills/sf-theme-gen/references/shadcn-theming.md +87 -0
  172. package/skills/{shadcn-theming/references/templates → sf-theme-gen/references}/theme-setup.md +15 -19
  173. package/skills/{shadcn-theming → sf-theme-gen}/references/theming-guide.md +6 -11
  174. package/skills/sf-view-gen/SKILL.md +146 -93
  175. package/skills/sf-view-gen/assets/recipes/archetypes.json +28 -14
  176. package/skills/sf-view-gen/assets/recipes/catalog.json +400 -0
  177. package/skills/sf-view-gen/assets/recipes/catalog.schema.json +64 -0
  178. package/skills/sf-view-gen/assets/recipes/features.json +162 -0
  179. package/skills/sf-view-gen/assets/recipes/recipes.json +76 -19
  180. package/skills/sf-view-gen/assets/recipes/sections.json +34 -5
  181. package/skills/sf-view-gen/assets/templates/views/sections/cta.client.tsx +25 -0
  182. package/skills/sf-view-gen/assets/templates/views/sections/cta.tsx +16 -0
  183. package/skills/sf-view-gen/assets/templates/views/sections/faq.client.tsx +13 -9
  184. package/skills/sf-view-gen/assets/templates/views/sections/faq.tsx +2 -7
  185. package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.client.tsx +12 -8
  186. package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.tsx +2 -7
  187. package/skills/sf-view-gen/assets/templates/views/sections/pricing.client.tsx +14 -10
  188. package/skills/sf-view-gen/assets/templates/views/sections/pricing.tsx +2 -9
  189. package/skills/sf-view-gen/assets/templates/views/sections/products.client.tsx +100 -0
  190. package/skills/sf-view-gen/assets/templates/views/sections/products.tsx +19 -0
  191. package/skills/sf-view-gen/assets/templates/views/sections/schema.cta.ts +19 -0
  192. package/skills/sf-view-gen/assets/templates/views/sections/schema.faq.ts +18 -0
  193. package/skills/sf-view-gen/assets/templates/views/sections/schema.logoCloud.ts +19 -0
  194. package/skills/sf-view-gen/assets/templates/views/sections/schema.pricing.ts +26 -0
  195. package/skills/sf-view-gen/assets/templates/views/sections/schema.products.ts +39 -0
  196. package/skills/sf-view-gen/assets/templates/views/sections/schema.statistics.ts +25 -0
  197. package/skills/sf-view-gen/assets/templates/views/sections/schema.testimonials.ts +23 -0
  198. package/skills/sf-view-gen/assets/templates/views/sections/statistics.client.tsx +16 -8
  199. package/skills/sf-view-gen/assets/templates/views/sections/statistics.tsx +2 -7
  200. package/skills/sf-view-gen/assets/templates/views/sections/testimonials.client.tsx +13 -9
  201. package/skills/sf-view-gen/assets/templates/views/sections/testimonials.tsx +2 -5
  202. package/skills/{json-render-core/SKILL.md → sf-view-gen/references/json-render-core.md} +4 -27
  203. package/skills/{json-render-next/SKILL.md → sf-view-gen/references/json-render-next.md} +5 -8
  204. package/skills/{json-render-react/SKILL.md → sf-view-gen/references/json-render-react.md} +3 -5
  205. package/skills/{json-render-shadcn/SKILL.md → sf-view-gen/references/json-render-shadcn.md} +3 -5
  206. package/skills/sf-view-gen/references/views.md +58 -33
  207. package/skills/sf-view-gen/scripts/catalog.mjs +128 -0
  208. package/skills/sf-view-gen/scripts/schema-validate.mjs +132 -0
  209. package/skills/sf-view-gen/scripts/validate.mjs +838 -32
  210. package/skills/storefront-workflow/SKILL.md +179 -0
  211. package/skills/{storefront-best-practices/reference/design.md → storefront-workflow/references/design-consistency.md} +13 -29
  212. package/CHANGELOG.md +0 -61
  213. package/commands/sf-add-view.md +0 -26
  214. package/commands/sf-theme.md +0 -20
  215. package/commands/sf-view.md +0 -25
  216. package/skills/building-storefronts/SKILL.md +0 -178
  217. package/skills/building-storefronts/references/frontend-integration.md +0 -229
  218. package/skills/json-render-remotion/SKILL.md +0 -111
  219. package/skills/json-render-solid/SKILL.md +0 -204
  220. package/skills/nextjs-shadcn/SKILL.md +0 -367
  221. package/skills/nextjs-shadcn/references/architecture.md +0 -499
  222. package/skills/nextjs-shadcn/references/project-setup.md +0 -162
  223. package/skills/nextjs-shadcn/references/shadcn-platform.md +0 -261
  224. package/skills/nextjs-shadcn/references/sidebar.md +0 -274
  225. package/skills/nextjs-shadcn/references/styling.md +0 -555
  226. package/skills/sf-view-gen/assets/templates/views/category/index.tsx +0 -86
  227. package/skills/sf-view-gen/assets/templates/views/category/registry.tsx +0 -48
  228. package/skills/sf-view-gen/assets/templates/views/checkout/index.tsx +0 -192
  229. package/skills/sf-view-gen/assets/templates/views/checkout/registry.tsx +0 -45
  230. package/skills/sf-view-gen/assets/templates/views/product/index.tsx +0 -122
  231. package/skills/sf-view-gen/assets/templates/views/product/registry.tsx +0 -49
  232. package/skills/sf-view-gen/assets/templates/views/user-panel/index.tsx +0 -168
  233. package/skills/sf-view-gen/assets/templates/views/user-panel/registry.tsx +0 -49
  234. package/skills/shadcn-component-discovery/SKILL.md +0 -273
  235. package/skills/shadcn-component-discovery/references/registries.md +0 -226
  236. package/skills/shadcn-theming/SKILL.md +0 -104
  237. package/skills/storefront-best-practices/SKILL.md +0 -421
  238. package/skills/storefront-best-practices/reference/components/breadcrumbs.md +0 -123
  239. package/skills/storefront-best-practices/reference/components/cart-popup.md +0 -189
  240. package/skills/storefront-best-practices/reference/components/country-selector.md +0 -298
  241. package/skills/storefront-best-practices/reference/components/footer.md +0 -112
  242. package/skills/storefront-best-practices/reference/components/hero.md +0 -241
  243. package/skills/storefront-best-practices/reference/components/megamenu.md +0 -239
  244. package/skills/storefront-best-practices/reference/components/navbar.md +0 -397
  245. package/skills/storefront-best-practices/reference/components/popups.md +0 -221
  246. package/skills/storefront-best-practices/reference/components/product-card.md +0 -125
  247. package/skills/storefront-best-practices/reference/components/product-reviews.md +0 -217
  248. package/skills/storefront-best-practices/reference/components/product-slider.md +0 -174
  249. package/skills/storefront-best-practices/reference/components/search.md +0 -101
  250. package/skills/storefront-best-practices/reference/connecting-to-backend.md +0 -391
  251. package/skills/storefront-best-practices/reference/features/promotions.md +0 -307
  252. package/skills/storefront-best-practices/reference/features/wishlist.md +0 -230
  253. package/skills/storefront-best-practices/reference/layouts/account.md +0 -380
  254. package/skills/storefront-best-practices/reference/layouts/cart.md +0 -316
  255. package/skills/storefront-best-practices/reference/layouts/checkout.md +0 -486
  256. package/skills/storefront-best-practices/reference/layouts/home-page.md +0 -264
  257. package/skills/storefront-best-practices/reference/layouts/order-confirmation.md +0 -231
  258. package/skills/storefront-best-practices/reference/layouts/product-details.md +0 -527
  259. package/skills/storefront-best-practices/reference/layouts/product-listing.md +0 -520
  260. package/skills/storefront-best-practices/reference/layouts/static-pages.md +0 -356
  261. package/skills/storefront-best-practices/reference/medusa.md +0 -307
  262. /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/mobile-responsiveness.md +0 -0
  263. /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/seo.md +0 -0
@@ -0,0 +1,120 @@
1
+ // Thor Commerce shell content: brand accents, surface classes, and footer copy.
2
+ //
3
+ // User-owned shell content — reconcile seeds this file once and never rewrites
4
+ // it, so your edits survive `storefront sync`. Plain data only: no React and no
5
+ // server imports.
6
+ //
7
+ // Neutral defaults only: no delivery, shipping, or promo promise is made here,
8
+ // and footer links point only at routes the generated storefront actually
9
+ // serves (`/`, `/categories`, `/checkout`, and the static pages `/contact`,
10
+ // `/about`, `/terms`, `/privacy`). Brand colors and copy belong
11
+ // in your own content, never in the package.
12
+ //
13
+ // The scaffold ships no newsletter submit callback, so the footer newsletter is
14
+ // omitted by default. A content-only `newsletter` is swallowed by the block
15
+ // (no form renders); wiring a real host callback to enable it is an external
16
+ // integration, out of scope for the scaffold.
17
+
18
+ export interface ShellAccent {
19
+ /** Trailing segment of the site-name wordmark to render with the accent. */
20
+ text: string
21
+ /** Consumer-supplied classes for the accent span (e.g. a brand color token). */
22
+ className?: string
23
+ }
24
+
25
+ export interface ShellLink {
26
+ label: string
27
+ /** A route the storefront actually serves. Omit for a non-link label. */
28
+ href?: string
29
+ }
30
+
31
+ export interface ShellLinkColumn {
32
+ title: string
33
+ links: ShellLink[]
34
+ }
35
+
36
+ export interface ShellNewsletter {
37
+ placeholder?: string
38
+ buttonLabel: string
39
+ }
40
+
41
+ /**
42
+ * Optional trailing accent for the navbar wordmark, e.g. the "Cuba" in
43
+ * "ElectroCuba". Leave `undefined` to render the wordmark in one colour. The
44
+ * accent only applies when `text` is a trailing segment of the site name.
45
+ */
46
+ export const logoAccent: ShellAccent | undefined = undefined
47
+
48
+ /**
49
+ * Optional trailing accent for the footer brand name. Same trailing-segment
50
+ * rule as `logoAccent`.
51
+ */
52
+ export const brandAccent: ShellAccent | undefined = undefined
53
+
54
+ /**
55
+ * Optional classes merged onto the navbar's outer surface (e.g.
56
+ * `bg-brand-nav text-brand-nav-foreground`). Leave `undefined` for the default.
57
+ */
58
+ export const navbarClassName: string | undefined = undefined
59
+
60
+ /**
61
+ * Optional classes merged onto the footer's outer surface. Leave `undefined`
62
+ * for the default.
63
+ */
64
+ export const footerClassName: string | undefined = undefined
65
+
66
+ /** Neutral footer brand description. No delivery/shipping promise. */
67
+ export const brandDescription: string =
68
+ 'Catálogo en línea de productos para tu hogar y tu negocio.'
69
+
70
+ /**
71
+ * Optional navbar utility-strip note (e.g. `'Nuevo horario de atención de 9 a
72
+ * 18 h'`). Empty by default: while it is empty and there are no utility links,
73
+ * the strip is not rendered, so a fresh storefront shows no note. Set a real
74
+ * note here to display it.
75
+ */
76
+ export const shippingNote: string = ''
77
+
78
+ /** Neutral footer links — only routes the storefront serves; no dead `#` links. */
79
+ export const linkColumns: ShellLinkColumn[] = [
80
+ {
81
+ title: 'Comprar',
82
+ links: [
83
+ { label: 'Inicio', href: '/' },
84
+ { label: 'Todas las categorías', href: '/categories' },
85
+ { label: 'Finalizar compra', href: '/checkout' },
86
+ ],
87
+ },
88
+ {
89
+ title: 'Información',
90
+ links: [
91
+ { label: 'Contacto', href: '/contact' },
92
+ { label: 'Sobre nosotros', href: '/about' },
93
+ { label: 'Términos y condiciones', href: '/terms' },
94
+ { label: 'Política de privacidad', href: '/privacy' },
95
+ ],
96
+ },
97
+ ]
98
+
99
+ /**
100
+ * Footer newsletter content. The scaffold has no working submit handler, so it
101
+ * is omitted by default (`false`). Setting it without a host-provided callback
102
+ * renders no form — never a fake one.
103
+ */
104
+ export const newsletter: false | ShellNewsletter = false
105
+
106
+ // Primary destinations for the mobile bottom navigation. Only routes the
107
+ // storefront actually serves; the icons come from the shared category icon set.
108
+ export interface ShellBottomNavItem {
109
+ id?: string
110
+ label: string
111
+ href: string
112
+ icon?: string
113
+ }
114
+
115
+ export const mobileBottomNavItems: ShellBottomNavItem[] = [
116
+ { id: 'home', label: 'Inicio', href: '/', icon: 'home' },
117
+ { id: 'categories', label: 'Categorías', href: '/categories', icon: 'store' },
118
+ { id: 'cart', label: 'Carrito', href: '/cart', icon: 'boxes' },
119
+ { id: 'account', label: 'Cuenta', href: '/account', icon: 'shield' },
120
+ ]
@@ -0,0 +1,38 @@
1
+ // Thor Commerce shell props: the shell chrome element props composed from the
2
+ // user-owned shell content. Reconcile seeds this file once and never rewrites
3
+ // it; the derived spec (../spec/index.ts) imports these objects and binds them
4
+ // into the shell element tree, so edits here survive `storefront sync`.
5
+ // Plain data only: no React and no server imports.
6
+
7
+ import {
8
+ brandAccent,
9
+ brandDescription,
10
+ footerClassName,
11
+ linkColumns,
12
+ logoAccent,
13
+ navbarClassName,
14
+ newsletter,
15
+ shippingNote,
16
+ } from './content'
17
+
18
+ export const navbarShellProps = {
19
+ logoAccent,
20
+ className: navbarClassName,
21
+ // Optional utility-strip note. Empty by default, so the strip renders only
22
+ // once a real note (or a utility link) is configured.
23
+ shippingNote,
24
+ // Global navigation search is held until a verified backend search contract
25
+ // exists (E2 task 5.4), so the generated storefront presents NO search
26
+ // control. A merchant may set this to `true` to opt in once the contract is
27
+ // verified; the `Navbar4` block still defaults to visible for legacy
28
+ // consumers that do not set this prop.
29
+ showSearch: false,
30
+ }
31
+
32
+ export const footerShellProps = {
33
+ brandAccent,
34
+ brandDescription,
35
+ linkColumns,
36
+ className: footerClassName,
37
+ ...(newsletter ? { newsletter } : {}),
38
+ }
@@ -3,23 +3,133 @@
3
3
  // Routes are derived from the view barrel; adding a view never edits this file.
4
4
 
5
5
  import type { NextAppSpec } from '@json-render/next'
6
+ import { storefrontInitialState } from '../state'
6
7
  import { views } from '../views'
8
+ import { footerShellProps, navbarShellProps } from '../shell/props'
9
+ import { mobileBottomNavItems } from '../shell/content'
10
+ import { mountedFeatureConfigs, mountedFeatureNames } from '../composition'
7
11
  import type { StorefrontElement } from './types'
8
12
 
9
13
  export type { StorefrontElement }
10
14
 
15
+ // The spec's state is a plain record; spreading the typed initial state keeps
16
+ // the single source of truth in ../state while satisfying the index signature.
17
+ const initialState: Record<string, unknown> = { ...storefrontInitialState }
18
+
11
19
  const routes = Object.fromEntries(
12
- views.map((view) => [
13
- view.route.path,
14
- {
15
- layout: view.route.layout,
16
- loader: view.route.loaderName,
17
- metadata: view.route.metadata,
18
- ...view.route.entry,
19
- },
20
- ]),
20
+ views.flatMap((view) => {
21
+ // A view declares one route or several; normalise to an array and flatten so
22
+ // a multi-route view (content-driven static pages) contributes one page per
23
+ // declared route while a single-route view is unchanged.
24
+ const viewRoutes = Array.isArray(view.route) ? view.route : [view.route]
25
+ return viewRoutes.map((route) => [
26
+ route.path,
27
+ {
28
+ layout: route.layout,
29
+ loader: route.loaderName,
30
+ metadata: route.metadata,
31
+ ...route.entry,
32
+ },
33
+ ])
34
+ }),
21
35
  )
22
36
 
37
+ // The default layout is the storefront chrome shared by every page: the
38
+ // system notice region sits in the notices slot (always first, above the
39
+ // header), the announcement banner and navbar live in the header slot, and the
40
+ // footer in the footer slot. The cart is a lateral panel owned by the navbar
41
+ // (opened by the navbar icon or a successful add), never an inline element of
42
+ // the shell, so page content is never followed by a duplicated cart. This
43
+ // mirrors the reference app so a fresh scaffold ships a real storefront, not a
44
+ // bare page.
45
+ // The navigability feature contributes its chrome only when it is installed.
46
+ // `mountedFeatureNames` is the general presence seam (unfiltered by the catalog
47
+ // boundary), so a removed feature leaves no dangling skip link, back-to-top, or
48
+ // bottom navigation behind.
49
+ const hasNavigability = mountedFeatureNames.includes('navigability')
50
+ const navigabilityConfig = (mountedFeatureConfigs['navigability'] ?? {}) as {
51
+ showBackToTop?: boolean
52
+ }
53
+ const showBackToTop = navigabilityConfig.showBackToTop !== false
54
+
55
+ const shellElements: Record<string, StorefrontElement> = {
56
+ // Navigability chrome, present only when the feature is installed. The skip
57
+ // link is the first focusable element (rendered before the notices); the
58
+ // back-to-top and mobile bottom navigation are fixed-position aids.
59
+ ...(hasNavigability
60
+ ? {
61
+ skipToContent: {
62
+ type: 'SkipToContent',
63
+ props: { targetId: 'main-content' },
64
+ },
65
+ ...(showBackToTop
66
+ ? {
67
+ backToTop: {
68
+ type: 'BackToTop',
69
+ props: { label: 'Volver arriba' },
70
+ },
71
+ }
72
+ : {}),
73
+ mobileBottomNav: {
74
+ type: 'MobileBottomNav',
75
+ props: {
76
+ items: mobileBottomNavItems,
77
+ ariaLabel: 'Navegación principal',
78
+ },
79
+ },
80
+ }
81
+ : {}),
82
+ // Reserved top region for system/status messages. Only the shell layout
83
+ // references it, so it renders once and always as the first element.
84
+ notice: {
85
+ type: 'SystemNotice',
86
+ props: {},
87
+ },
88
+ banner: {
89
+ type: 'Banner',
90
+ props: {
91
+ badge: 'Novedades',
92
+ message: 'Descubre el catálogo de __SITE_NAME__.',
93
+ href: '/categories',
94
+ linkLabel: 'Ver catálogo',
95
+ },
96
+ visible: { $state: '/bannerDismissed', not: true },
97
+ on: { dismissBanner: { action: 'dismissBanner' } },
98
+ },
99
+ navbar: {
100
+ type: 'StorefrontNavbar',
101
+ props: {
102
+ storeCategories: { $state: '/navCategories' },
103
+ // No shipping/delivery/COD note is shipped by default; the note comes
104
+ // from the user-owned shell seam (`...navbarShellProps` below) and is
105
+ // empty unless the merchant sets one, so no placeholder strip renders.
106
+ utilityLinks: [],
107
+ logoText: '__SITE_NAME__',
108
+ logoHref: '/',
109
+ // Optional wordmark accent and surface classes, configured in the
110
+ // user-owned shell seam. Spread last so merchant edits win.
111
+ ...navbarShellProps,
112
+ },
113
+ on: {
114
+ openCart: { action: 'openCart' },
115
+ goToCart: { action: 'goToCart' },
116
+ goToCheckout: { action: 'goToCheckout' },
117
+ applyQuantityDelta: { action: 'applyQuantityDelta' },
118
+ removeFromCart: { action: 'removeFromCart' },
119
+ },
120
+ },
121
+ footer: {
122
+ type: 'Footer',
123
+ props: {
124
+ brandName: '__SITE_NAME__',
125
+ // Footer brand description, links, accent, and surface classes all come
126
+ // from the user-owned shell seam. Spread last so merchant edits win; no
127
+ // delivery/shipping promise and no dead `#` link is shipped by default.
128
+ ...footerShellProps,
129
+ },
130
+ },
131
+ }
132
+
23
133
  export const __STOREFRONT_CAMEL__Spec: NextAppSpec = {
24
134
  metadata: {
25
135
  title: {
@@ -28,7 +138,7 @@ export const __STOREFRONT_CAMEL__Spec: NextAppSpec = {
28
138
  },
29
139
  description: 'Tienda online de __SITE_NAME__.',
30
140
  },
31
- state: {},
141
+ state: initialState,
32
142
  layouts: {
33
143
  default: {
34
144
  root: 'layout',
@@ -36,11 +146,20 @@ export const __STOREFRONT_CAMEL__Spec: NextAppSpec = {
36
146
  layout: {
37
147
  type: 'Layout',
38
148
  props: {},
149
+ slots: {
150
+ ...(hasNavigability
151
+ ? { chrome: ['skipToContent', 'backToTop', 'mobileBottomNav'] }
152
+ : {}),
153
+ notices: ['notice'],
154
+ header: ['banner', 'navbar'],
155
+ footer: ['footer'],
156
+ },
39
157
  children: ['slot'],
40
158
  },
41
159
  // The page content is injected at the Slot element by @json-render/next.
42
160
  // Without it the rendered layout drops the page and the store looks empty.
43
161
  slot: { type: 'Slot', props: {}, children: [] },
162
+ ...shellElements,
44
163
  },
45
164
  },
46
165
  },
@@ -19,7 +19,11 @@ export const pageRoot = (
19
19
  page: {
20
20
  root: id,
21
21
  elements: {
22
- [id]: { type: 'Layout', props: {}, children },
22
+ // `Page` is a neutral container, not the shell `Layout`: the shell is
23
+ // already rendered by the route's layoutSpec, so nesting another Layout
24
+ // here would duplicate chrome (header/footer/main) and any shell-only
25
+ // notices.
26
+ [id]: { type: 'Page', props: {}, children },
23
27
  ...elements,
24
28
  },
25
29
  },
@@ -0,0 +1,191 @@
1
+ // Generated by Thor Commerce storefront (template: assets/templates/app/lib/__STOREFRONT__/spec/seo.ts)
2
+ // Pure SEO helpers shared by the seeded-once `app/sitemap.ts` and `app/robots.ts`
3
+ // host files. Kept free of Next.js, React, and env imports so the logic is
4
+ // testable at the source and the host files stay thin.
5
+
6
+ import type { EntitySeo } from '@thorprovider/runtime/contracts'
7
+ import {
8
+ mapCategoryEntityToSeo,
9
+ mapProductEntityToSeo,
10
+ } from '@thorprovider/runtime/contracts'
11
+
12
+ /** The routes the storefront can serve as fixed pages (no dynamic `[` segment). */
13
+ export function fixedRoutePaths(routes: Record<string, unknown>): string[] {
14
+ return Object.keys(routes).filter((path) => !path.includes('['))
15
+ }
16
+
17
+ /**
18
+ * Absolute sitemap URLs for the fixed routes. Emits nothing when no site URL is
19
+ * configured, so no canonical URL is invented.
20
+ */
21
+ export function sitemapUrls(
22
+ routes: Record<string, unknown>,
23
+ siteUrl: string | undefined,
24
+ ): string[] {
25
+ if (!siteUrl) return []
26
+ return fixedRoutePaths(routes).map((path) => new URL(path, siteUrl).toString())
27
+ }
28
+
29
+ /** Absolute URL of the sitemap for `robots.txt`, or undefined without a site URL. */
30
+ export function robotsSitemapUrl(siteUrl: string | undefined): string | undefined {
31
+ return siteUrl ? new URL('/sitemap.xml', siteUrl).toString() : undefined
32
+ }
33
+
34
+ /**
35
+ * Surfaces that are never public: they require a shopper session or hold
36
+ * checkout state and must not be indexed or listed in the sitemap.
37
+ */
38
+ export const PRIVATE_ROUTE_PREFIXES = ['/account', '/checkout'] as const
39
+
40
+ /** True when a path is (or is nested under) a private surface. */
41
+ export function isPrivatePath(path: string): boolean {
42
+ return PRIVATE_ROUTE_PREFIXES.some(
43
+ (prefix) => path === prefix || path.startsWith(`${prefix}/`),
44
+ )
45
+ }
46
+
47
+ /**
48
+ * The paths `robots.txt` disallows: the fixed private surfaces plus any private
49
+ * path the known route set declares. Public routes are never returned.
50
+ */
51
+ export function robotsDisallowPaths(routes: Record<string, unknown>): string[] {
52
+ const paths = new Set<string>(PRIVATE_ROUTE_PREFIXES)
53
+ for (const path of Object.keys(routes)) {
54
+ if (isPrivatePath(path)) paths.add(path)
55
+ }
56
+ return [...paths]
57
+ }
58
+
59
+ /**
60
+ * Per-route entity-SEO extractors keyed by the route's loader name. Each runs
61
+ * after the same request-scoped loader as the page and maps the real loaded
62
+ * entity through the pure `@thorprovider/runtime/contracts` mappers. The route
63
+ * prefix is supplied here because the runtime stays route-agnostic.
64
+ */
65
+ export const entitySeoExtractors: Record<
66
+ string,
67
+ (data: Record<string, unknown>) => EntitySeo | undefined
68
+ > = {
69
+ product: (data) =>
70
+ mapProductEntityToSeo(data.selectedProduct, { pathPrefix: '/products' }),
71
+ category: (data) =>
72
+ mapCategoryEntityToSeo(data.categoryEntity, { pathPrefix: '/categories' }),
73
+ }
74
+
75
+ /** A public catalog entity referenced by its stable id and public path. */
76
+ export interface PublicEntityRef {
77
+ id: string
78
+ path: string
79
+ }
80
+
81
+ /** One page of public entities plus the cursor for the next page, if any. */
82
+ export interface PublicEntityPage {
83
+ items: PublicEntityRef[]
84
+ nextCursor?: string
85
+ }
86
+
87
+ /** A page-fetching callback for the bounded entity enumeration. */
88
+ export type PublicEntityPageFetcher = (
89
+ cursor: string | undefined,
90
+ ) => Promise<PublicEntityPage>
91
+
92
+ /** Explicit reason an enumeration did not complete. */
93
+ export type SitemapEnumerationFailure =
94
+ | { kind: 'enumeration'; message: string }
95
+ | { kind: 'bound'; maxPages: number }
96
+ | { kind: 'cycle'; id: string }
97
+
98
+ export type SitemapEnumerationResult =
99
+ | { ok: true; entities: PublicEntityRef[] }
100
+ | { ok: false; failure: SitemapEnumerationFailure }
101
+
102
+ /** The traversal bound: an enumeration that exceeds it fails, never truncates. */
103
+ export const DEFAULT_SITEMAP_MAX_PAGES = 100
104
+
105
+ /**
106
+ * Enumerate public entities across pagination, de-duplicated by id, with a
107
+ * visited-id cycle guard and a maximum-page bound. A fetch failure, a bound hit,
108
+ * or a repeated id/cursor returns an explicit typed failure instead of an
109
+ * apparently-complete or truncated list.
110
+ */
111
+ export async function enumeratePublicEntities(
112
+ fetchPage: PublicEntityPageFetcher,
113
+ options: { maxPages?: number } = {},
114
+ ): Promise<SitemapEnumerationResult> {
115
+ const maxPages = options.maxPages ?? DEFAULT_SITEMAP_MAX_PAGES
116
+ const seenIds = new Set<string>()
117
+ const seenCursors = new Set<string>()
118
+ const entities: PublicEntityRef[] = []
119
+ let cursor: string | undefined
120
+
121
+ for (let pages = 0; ; pages += 1) {
122
+ if (pages >= maxPages) {
123
+ return { ok: false, failure: { kind: 'bound', maxPages } }
124
+ }
125
+
126
+ let page: PublicEntityPage
127
+ try {
128
+ page = await fetchPage(cursor)
129
+ } catch (error) {
130
+ return {
131
+ ok: false,
132
+ failure: {
133
+ kind: 'enumeration',
134
+ message: error instanceof Error ? error.message : String(error),
135
+ },
136
+ }
137
+ }
138
+
139
+ for (const item of page.items) {
140
+ if (seenIds.has(item.id)) {
141
+ return { ok: false, failure: { kind: 'cycle', id: item.id } }
142
+ }
143
+ seenIds.add(item.id)
144
+ entities.push(item)
145
+ }
146
+
147
+ const next = page.nextCursor
148
+ if (next === undefined || next === '') {
149
+ return { ok: true, entities }
150
+ }
151
+ if (seenCursors.has(next)) {
152
+ return { ok: false, failure: { kind: 'cycle', id: next } }
153
+ }
154
+ seenCursors.add(next)
155
+ cursor = next
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Raised by `app/sitemap.ts` when enumeration fails, so the route responds with
161
+ * a non-success instead of an apparently-complete sitemap.
162
+ */
163
+ export class SitemapEnumerationError extends Error {
164
+ readonly failure: SitemapEnumerationFailure
165
+ constructor(failure: SitemapEnumerationFailure) {
166
+ super(`Sitemap enumeration failed (${failure.kind})`)
167
+ this.name = 'SitemapEnumerationError'
168
+ this.failure = failure
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Combine the fixed public routes with the enumerated public entities into
174
+ * de-duplicated absolute sitemap entries. Private surfaces are never listed.
175
+ */
176
+ export function buildSitemapEntries(input: {
177
+ routes: Record<string, unknown>
178
+ siteUrl: string
179
+ entities: readonly PublicEntityRef[]
180
+ }): { url: string }[] {
181
+ const urls = new Set<string>()
182
+ for (const path of fixedRoutePaths(input.routes)) {
183
+ if (!isPrivatePath(path)) urls.add(new URL(path, input.siteUrl).toString())
184
+ }
185
+ for (const entity of input.entities) {
186
+ if (!isPrivatePath(entity.path)) {
187
+ urls.add(new URL(entity.path, input.siteUrl).toString())
188
+ }
189
+ }
190
+ return [...urls].map((url) => ({ url }))
191
+ }