@thorprovider/storefront 0.4.0-mvp.0 → 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 (166) hide show
  1. package/README.md +173 -153
  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 +42 -1
  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 +1 -5
  9. package/assets/templates/app/app/robots.ts +9 -3
  10. package/assets/templates/app/app/sitemap.ts +64 -5
  11. package/assets/templates/app/lib/__STOREFRONT__/actions.ts +70 -0
  12. package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +15 -5
  13. package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +71 -0
  14. package/assets/templates/app/lib/__STOREFRONT__/composition.ts +26 -0
  15. package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +36 -4
  16. package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +82 -50
  17. package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +3 -0
  18. package/assets/templates/app/lib/__STOREFRONT__/schema.ts +29 -14
  19. package/assets/templates/app/lib/__STOREFRONT__/section-observation.ts +182 -0
  20. package/assets/templates/app/lib/__STOREFRONT__/section-sources.ts +130 -0
  21. package/assets/templates/app/lib/__STOREFRONT__/shell/content.ts +31 -7
  22. package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +4 -0
  23. package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +56 -26
  24. package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +165 -0
  25. package/assets/templates/app/lib/__STOREFRONT__/store-context.ts +26 -7
  26. package/assets/templates/app/lib/__STOREFRONT__/views/sections/index.ts +2 -1
  27. package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.client.tsx +100 -0
  28. package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.tsx +19 -0
  29. package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.products.ts +39 -0
  30. package/assets/templates/features/checkout/handlers.ts +6 -0
  31. package/assets/templates/features/customer-auth/state.ts +10 -1
  32. package/assets/templates/views/account/index.tsx +1 -1
  33. package/assets/templates/views/account/registry.tsx +80 -19
  34. package/assets/templates/views/account/schema.ts +1 -0
  35. package/assets/templates/views/cart/content.ts +11 -0
  36. package/assets/templates/views/cart/data.ts +23 -0
  37. package/assets/templates/views/cart/index.tsx +53 -0
  38. package/assets/templates/views/cart/props.ts +12 -0
  39. package/assets/templates/views/cart/registry.tsx +81 -0
  40. package/assets/templates/views/cart/schema.ts +52 -0
  41. package/assets/templates/views/category/assembly/assembly.json +1 -0
  42. package/assets/templates/views/category/assembly/index.skeleton.tsx +64 -6
  43. package/assets/templates/views/category/assembly/registry.skeleton.tsx +2 -1
  44. package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/binding.json +5 -0
  45. package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/catalog.part +4 -0
  46. package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/element.part +7 -0
  47. package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/registry.part +18 -0
  48. package/assets/templates/views/category/index.tsx +76 -7
  49. package/assets/templates/views/category/props.ts +7 -0
  50. package/assets/templates/views/category/registry.tsx +19 -0
  51. package/assets/templates/views/category/schema.ts +13 -0
  52. package/assets/templates/views/home/assembly/assembly.json +9 -0
  53. package/assets/templates/views/home/assembly/index.skeleton.tsx +22 -4
  54. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/element.part +1 -1
  55. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/registry.part +2 -2
  56. package/assets/templates/views/home/data.ts +9 -0
  57. package/assets/templates/views/home/index.tsx +23 -5
  58. package/assets/templates/views/home/registry.tsx +2 -2
  59. package/assets/templates/views/product/data.ts +4 -2
  60. package/assets/templates/views/product/index.tsx +49 -5
  61. package/assets/templates/views/product/props.ts +7 -0
  62. package/assets/templates/views/product/registry.tsx +25 -0
  63. package/assets/templates/views/product/schema.ts +13 -0
  64. package/assets/templates/views/static/content.ts +4 -4
  65. package/assets/templates/views/static/index.tsx +1 -1
  66. package/bin/storefront.js +1231 -154
  67. package/examples/merchandising/category-led.json +37 -0
  68. package/examples/merchandising/collection-led.json +37 -0
  69. package/package.json +11 -7
  70. package/recipes/archetypes.json +9 -22
  71. package/recipes/catalog.json +40 -1
  72. package/recipes/features.json +25 -2
  73. package/recipes/recipe.schema.json +1 -1
  74. package/recipes/recipes.json +32 -30
  75. package/recipes/sections.json +10 -0
  76. package/recipes/validate.mjs +54 -0
  77. package/scripts/candidate.mjs +4 -4
  78. package/scripts/composers.mjs +25 -5
  79. package/scripts/env-file.mjs +86 -0
  80. package/scripts/file-classes.mjs +5 -0
  81. package/scripts/migrate-english-routes.mjs +276 -0
  82. package/scripts/pinning.mjs +74 -5
  83. package/scripts/plan-apply.mjs +149 -5
  84. package/scripts/reconcile.mjs +49 -11
  85. package/scripts/render-templates.mjs +97 -44
  86. package/scripts/section-config.mjs +253 -0
  87. package/scripts/starter.mjs +17 -0
  88. package/scripts/theme/color.mjs +185 -0
  89. package/scripts/theme/css-theme.mjs +233 -0
  90. package/scripts/theme/fonts.mjs +123 -0
  91. package/scripts/theme/import-export.mjs +125 -0
  92. package/scripts/theme/preset.mjs +93 -0
  93. package/scripts/theme/theme-plan.mjs +292 -0
  94. package/scripts/theme/tokens.mjs +137 -0
  95. package/scripts/view-assembly.mjs +47 -1
  96. package/skills/sf-feature/SKILL.md +32 -19
  97. package/skills/sf-setup/SKILL.md +79 -0
  98. package/skills/sf-theme-gen/SKILL.md +99 -36
  99. package/skills/sf-theme-gen/references/shadcn-theming.md +87 -0
  100. package/skills/{shadcn-theming/references/templates → sf-theme-gen/references}/theme-setup.md +15 -19
  101. package/skills/{shadcn-theming → sf-theme-gen}/references/theming-guide.md +6 -11
  102. package/skills/sf-view-gen/SKILL.md +63 -40
  103. package/skills/sf-view-gen/assets/recipes/archetypes.json +9 -22
  104. package/skills/sf-view-gen/assets/recipes/catalog.json +40 -1
  105. package/skills/sf-view-gen/assets/recipes/features.json +25 -2
  106. package/skills/sf-view-gen/assets/recipes/recipes.json +32 -30
  107. package/skills/sf-view-gen/assets/recipes/sections.json +10 -0
  108. package/skills/sf-view-gen/assets/templates/views/sections/products.client.tsx +100 -0
  109. package/skills/sf-view-gen/assets/templates/views/sections/products.tsx +19 -0
  110. package/skills/sf-view-gen/assets/templates/views/sections/schema.products.ts +39 -0
  111. package/skills/{json-render-core/SKILL.md → sf-view-gen/references/json-render-core.md} +4 -27
  112. package/skills/{json-render-next/SKILL.md → sf-view-gen/references/json-render-next.md} +5 -8
  113. package/skills/{json-render-react/SKILL.md → sf-view-gen/references/json-render-react.md} +3 -5
  114. package/skills/{json-render-shadcn/SKILL.md → sf-view-gen/references/json-render-shadcn.md} +3 -5
  115. package/skills/sf-view-gen/references/views.md +28 -37
  116. package/skills/sf-view-gen/scripts/validate.mjs +54 -0
  117. package/skills/storefront-workflow/SKILL.md +94 -31
  118. package/skills/{storefront-best-practices/reference/design.md → storefront-workflow/references/design-consistency.md} +13 -29
  119. package/CHANGELOG.md +0 -84
  120. package/assets/templates/views/user-panel/index.tsx +0 -106
  121. package/assets/templates/views/user-panel/registry.tsx +0 -54
  122. package/assets/templates/views/user-panel/schema.ts +0 -79
  123. package/commands/sf-add-view.md +0 -57
  124. package/commands/sf-feature.md +0 -28
  125. package/commands/sf-theme.md +0 -26
  126. package/commands/sf-view.md +0 -67
  127. package/skills/building-storefronts/SKILL.md +0 -178
  128. package/skills/building-storefronts/references/frontend-integration.md +0 -229
  129. package/skills/json-render-remotion/SKILL.md +0 -111
  130. package/skills/json-render-solid/SKILL.md +0 -204
  131. package/skills/nextjs-shadcn/SKILL.md +0 -367
  132. package/skills/nextjs-shadcn/references/architecture.md +0 -499
  133. package/skills/nextjs-shadcn/references/project-setup.md +0 -162
  134. package/skills/nextjs-shadcn/references/shadcn-platform.md +0 -261
  135. package/skills/nextjs-shadcn/references/sidebar.md +0 -274
  136. package/skills/nextjs-shadcn/references/styling.md +0 -555
  137. package/skills/shadcn-component-discovery/SKILL.md +0 -273
  138. package/skills/shadcn-component-discovery/references/registries.md +0 -226
  139. package/skills/shadcn-theming/SKILL.md +0 -104
  140. package/skills/storefront-best-practices/SKILL.md +0 -421
  141. package/skills/storefront-best-practices/reference/components/breadcrumbs.md +0 -123
  142. package/skills/storefront-best-practices/reference/components/cart-popup.md +0 -189
  143. package/skills/storefront-best-practices/reference/components/country-selector.md +0 -298
  144. package/skills/storefront-best-practices/reference/components/footer.md +0 -112
  145. package/skills/storefront-best-practices/reference/components/hero.md +0 -241
  146. package/skills/storefront-best-practices/reference/components/megamenu.md +0 -239
  147. package/skills/storefront-best-practices/reference/components/navbar.md +0 -397
  148. package/skills/storefront-best-practices/reference/components/popups.md +0 -221
  149. package/skills/storefront-best-practices/reference/components/product-card.md +0 -125
  150. package/skills/storefront-best-practices/reference/components/product-reviews.md +0 -217
  151. package/skills/storefront-best-practices/reference/components/product-slider.md +0 -174
  152. package/skills/storefront-best-practices/reference/components/search.md +0 -101
  153. package/skills/storefront-best-practices/reference/connecting-to-backend.md +0 -391
  154. package/skills/storefront-best-practices/reference/features/promotions.md +0 -307
  155. package/skills/storefront-best-practices/reference/features/wishlist.md +0 -230
  156. package/skills/storefront-best-practices/reference/layouts/account.md +0 -380
  157. package/skills/storefront-best-practices/reference/layouts/cart.md +0 -316
  158. package/skills/storefront-best-practices/reference/layouts/checkout.md +0 -486
  159. package/skills/storefront-best-practices/reference/layouts/home-page.md +0 -264
  160. package/skills/storefront-best-practices/reference/layouts/order-confirmation.md +0 -231
  161. package/skills/storefront-best-practices/reference/layouts/product-details.md +0 -527
  162. package/skills/storefront-best-practices/reference/layouts/product-listing.md +0 -520
  163. package/skills/storefront-best-practices/reference/layouts/static-pages.md +0 -356
  164. package/skills/storefront-best-practices/reference/medusa.md +0 -307
  165. /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/mobile-responsiveness.md +0 -0
  166. /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/seo.md +0 -0
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "category-led",
3
+ "title": "Category-led combined presentation plus freshness selection",
4
+ "intent": "Lead with a category-focused combined grid-and-carousel of the newest arrivals, keep category navigation visible, and cross-sell a related category below. Selection (category handle and ordering) is independent of the presentation variant.",
5
+ "sections": [
6
+ { "id": "slot:hero", "type": "hero", "variant": "StorefrontHero1" },
7
+ { "id": "slot:benefits", "type": "benefits", "variant": "Feature1" },
8
+ { "id": "slot:categories", "type": "categories", "variant": "ProductCategory2" },
9
+ {
10
+ "id": "home-products-1",
11
+ "type": "products",
12
+ "variant": "ProductShowcase1",
13
+ "objective": "Showcase the newest arrivals in a category as a browsable carousel",
14
+ "destination": "/categories/repuestos",
15
+ "source": {
16
+ "kind": "products",
17
+ "query": { "categories": ["repuestos"], "sort": "newest", "pageSize": 12 }
18
+ },
19
+ "content": {
20
+ "heading": "Novedades en repuestos",
21
+ "description": "Lo último que llega a la categoría."
22
+ }
23
+ },
24
+ {
25
+ "id": "home-products-2",
26
+ "type": "products",
27
+ "variant": "ProductCard1",
28
+ "objective": "Cross-sell a related category below the carousel",
29
+ "destination": "/categories/accesorios",
30
+ "source": {
31
+ "kind": "products",
32
+ "query": { "categories": ["accesorios"], "sort": "featured", "pageSize": 4 }
33
+ },
34
+ "content": { "heading": "Accesorios recomendados" }
35
+ }
36
+ ]
37
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "collection-led",
3
+ "title": "Collection-led grid plus campaign CTA",
4
+ "intent": "Lead the home with one curated collection as the primary merchandise grid, and reinforce it with a single campaign call to action. The selection is a collection handle, not a sales ranking.",
5
+ "sections": [
6
+ { "id": "slot:hero", "type": "hero", "variant": "StorefrontHero1" },
7
+ { "id": "slot:benefits", "type": "benefits", "variant": "Feature1" },
8
+ {
9
+ "id": "home-products-1",
10
+ "type": "products",
11
+ "variant": "ProductCard1",
12
+ "objective": "Move collection browsers into the featured collection",
13
+ "destination": "/collections/featured",
14
+ "source": {
15
+ "kind": "products",
16
+ "query": { "collections": ["featured"], "sort": "featured", "pageSize": 8 }
17
+ },
18
+ "content": {
19
+ "heading": "Destacados de la temporada",
20
+ "description": "Una selección curada de la colección destacada."
21
+ }
22
+ },
23
+ {
24
+ "id": "home-cta-1",
25
+ "type": "cta",
26
+ "objective": "Reinforce the campaign message with one clear action",
27
+ "destination": "/collections/featured",
28
+ "content": {
29
+ "badgeText": "Campaña",
30
+ "title": "Envío gratis esta semana",
31
+ "description": "En pedidos superiores al mínimo de la campaña.",
32
+ "primaryCta": { "label": "Ver la colección", "href": "/collections/featured" }
33
+ }
34
+ },
35
+ { "id": "slot:categories", "type": "categories", "variant": "ProductCategory2" }
36
+ ]
37
+ }
package/package.json CHANGED
@@ -1,17 +1,20 @@
1
1
  {
2
2
  "name": "@thorprovider/storefront",
3
- "version": "0.4.0-mvp.0",
4
- "description": "Deterministic storefront scaffolder and installable OpenCode command suite for Thor Commerce (Next.js, @thorprovider/blocks, @thorprovider/adapters, Medusa)",
3
+ "version": "0.4.0-mvp.2",
4
+ "description": "Deterministic storefront scaffolder and installable OpenCode skill suite for Thor Commerce (Next.js, @thorprovider/blocks, @thorprovider/adapters, Medusa)",
5
5
  "type": "module",
6
- "bin": "./bin/storefront.js",
6
+ "bin": {
7
+ "storefront": "bin/storefront.js"
8
+ },
7
9
  "files": [
8
10
  "assets",
9
11
  "bin",
10
- "commands",
12
+ "examples",
11
13
  "skills",
12
14
  "recipes",
13
15
  "scripts",
14
- "README.md"
16
+ "README.md",
17
+ "VENDOR.md"
15
18
  ],
16
19
  "engines": {
17
20
  "node": ">=20.9.0"
@@ -21,7 +24,7 @@
21
24
  "check-assets": "node ./scripts/sync-assets.mjs --check",
22
25
  "pretest": "node ./scripts/sync-assets.mjs --check",
23
26
  "test": "vitest run --config vitest.config.ts",
24
- "test:integration": "vitest run --config vitest.config.ts tests/scaffold-integration.test.ts"
27
+ "test:integration": "vitest run --config vitest.config.ts tests/scaffold-integration.test.ts tests/starter-integration.test.ts"
25
28
  },
26
29
  "publishConfig": {
27
30
  "access": "public",
@@ -41,10 +44,11 @@
41
44
  ],
42
45
  "dependencies": {
43
46
  "@clack/prompts": "^1.8.1",
47
+ "@fission-ai/openspec": "1.11.0",
44
48
  "semver": "^7.7.4"
45
49
  },
46
50
  "devDependencies": {
47
51
  "vitest": "^3.2.4"
48
52
  },
49
53
  "license": "MIT"
50
- }
54
+ }
@@ -88,6 +88,15 @@
88
88
  }
89
89
  }
90
90
  },
91
+ "cart.view": {
92
+ "default": "review",
93
+ "intents": {
94
+ "review": {
95
+ "variant": "ShoppingCart4",
96
+ "when": "A standalone cart review page: line editing plus backend-authoritative totals before checkout."
97
+ }
98
+ }
99
+ },
91
100
  "category.filter": {
92
101
  "default": "sidebar",
93
102
  "intents": {
@@ -148,28 +157,6 @@
148
157
  "when": "A signed-in shopper's order history with per-order totals and detail on demand."
149
158
  }
150
159
  }
151
- },
152
- "user-panel.shell": {
153
- "default": "standard",
154
- "intents": {
155
- "standard": {
156
- "variant": "AppShell1",
157
- "when": "A standard account shell with side navigation and content area."
158
- }
159
- }
160
- },
161
- "user-panel.orderHistory": {
162
- "default": "compact",
163
- "intents": {
164
- "compact": {
165
- "variant": "OrderHistory1",
166
- "when": "A concise list of past orders with status and totals."
167
- },
168
- "detailed": {
169
- "variant": "OrderHistory2",
170
- "when": "Order history with line-item detail, tracking, and reorder actions."
171
- }
172
- }
173
160
  }
174
161
  }
175
162
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "./catalog.schema.json",
3
3
  "catalogVersion": 1,
4
- "description": "Initial semantic executable catalog (E1 tasks 1.2-1.3), classified by source-shape inspection of the @thorprovider/blocks prop contracts and the view templates. Scope: every slot the curated MVP views (home, product, category, checkout, account) render by default, covering both the selectable archetype slots declared by recipes/archetypes.json and the nested default slots that have no archetype (category.productCard, checkout.confirmation) so no legacy default support is dropped. The account view (change: storefront-customer-auth) declares the login/register slots: LoginPage1/SignUpPage1 are executable while LoginPage2 is held for its divergent callback contract. A variant is `executable` only when the slot's canonical data shape can feed its block props without a prop bridge and without inventing data; every other non-default variant is `held` with the specific reason. The one executable non-default swap is home.benefits Feature1 -> Feature2 (identical item contract, no bridge). user-panel (non-MVP view) is outside this initial catalog scope. E2 task 6.1 declares the base listing slot category.filter CategoryFilter2 as the single controlled listing (dataShape `CatalogQuery & CatalogResults`, state controlled) with the search/filter controls contributed by the logical mounts category.search / category.filters (no second block). Source-shape compatibility is not end-to-end backend behavior proof: the render is executable, but the category.search.apply / category.filter.apply actions remain held under the capability catalog (owner: storefront-catalog-search-filters, E2) until a real runtime proof. The `capabilities` map declares those operational capabilities with their requirements and verification state; E1 declares the held category.filter.apply action, E2 adds the held category.search.apply action and the held category.price.range capability that gates the category-filters showPrice option; no capability is claimed verified.",
4
+ "description": "Initial semantic executable catalog (E1 tasks 1.2-1.3), classified by source-shape inspection of the @thorprovider/blocks prop contracts and the view templates. Scope: every slot the curated MVP views (home, product, category, checkout, account) render by default, covering both the selectable archetype slots declared by recipes/archetypes.json and the nested default slots that have no archetype (category.productCard, checkout.confirmation) so no legacy default support is dropped. The account view (change: storefront-customer-auth) declares the login/register slots: LoginPage1/SignUpPage1 are executable while LoginPage2 is held for its divergent callback contract. A variant is `executable` only when the slot's canonical data shape can feed its block props without a prop bridge and without inventing data; every other non-default variant is `held` with the specific reason. The one executable non-default swap is home.benefits Feature1 -> Feature2 (identical item contract, no bridge). E2 task 6.1 declares the base listing slot category.filter CategoryFilter2 as the single controlled listing (dataShape `CatalogQuery & CatalogResults`, state controlled) with the search/filter controls contributed by the logical mounts category.search / category.filters (no second block). Source-shape compatibility is not end-to-end backend behavior proof: the render is executable, but the category.search.apply / category.filter.apply actions remain held under the capability catalog (owner: storefront-catalog-search-filters, E2) until a real runtime proof. The `capabilities` map declares those operational capabilities with their requirements and verification state; E1 declares the held category.filter.apply action, E2 adds the held category.search.apply action and the held category.price.range capability that gates the category-filters showPrice option; no capability is claimed verified.",
5
5
  "slots": {
6
6
  "home.hero": {
7
7
  "default": "StorefrontHero1",
@@ -157,6 +157,32 @@
157
157
  }
158
158
  }
159
159
  },
160
+ "product.breadcrumb": {
161
+ "default": "Breadcrumb",
162
+ "variants": {
163
+ "Breadcrumb": {
164
+ "status": "executable",
165
+ "block": "Breadcrumb",
166
+ "dataShape": "BreadcrumbItemData[]",
167
+ "events": [],
168
+ "state": "uncontrolled",
169
+ "requirements": ["productBreadcrumb"]
170
+ }
171
+ }
172
+ },
173
+ "cart.view": {
174
+ "default": "ShoppingCart4",
175
+ "variants": {
176
+ "ShoppingCart4": {
177
+ "status": "executable",
178
+ "block": "ShoppingCart4",
179
+ "dataShape": "CartLineState[] & CartTotalsState",
180
+ "events": ["refreshCart", "updateQuantity", "removeFromCart", "checkout", "continueShopping"],
181
+ "state": "controlled",
182
+ "requirements": ["cart", "cartTotals"]
183
+ }
184
+ }
185
+ },
160
186
  "category.filter": {
161
187
  "default": "CategoryFilter2",
162
188
  "variants": {
@@ -339,6 +365,19 @@
339
365
  "requirements": ["accountCustomer", "accountAuthenticated"]
340
366
  }
341
367
  }
368
+ },
369
+ "category.breadcrumb": {
370
+ "default": "Breadcrumb",
371
+ "variants": {
372
+ "Breadcrumb": {
373
+ "status": "executable",
374
+ "block": "Breadcrumb",
375
+ "dataShape": "BreadcrumbItemData[]",
376
+ "events": [],
377
+ "state": "uncontrolled",
378
+ "requirements": ["categoryBreadcrumb"]
379
+ }
380
+ }
342
381
  }
343
382
  },
344
383
  "capabilities": {
@@ -30,7 +30,7 @@
30
30
  "name": "customer-auth",
31
31
  "mvp": true,
32
32
  "description": "Customer authentication: registration, login, logout, and a minimal signed-in state. Owns the account view.",
33
- "route": "/cuenta",
33
+ "route": "/account",
34
34
  "views": ["account"],
35
35
  "state": [
36
36
  "accountCustomer",
@@ -48,7 +48,7 @@
48
48
  "name": "static-pages",
49
49
  "mvp": true,
50
50
  "description": "Content-driven static pages (contact, legal, about) served with per-page metadata. Owns the static view.",
51
- "route": "/contacto",
51
+ "route": "/contact",
52
52
  "views": ["static"],
53
53
  "state": [],
54
54
  "handlers": [],
@@ -122,6 +122,29 @@
122
122
  "mounts": [{ "id": "category-filters", "host": "category", "slot": "filters" }],
123
123
  "surfaces": []
124
124
  },
125
+ {
126
+ "name": "navigability",
127
+ "mvp": true,
128
+ "description": "Cross-cutting navigability feature: entity-derived breadcrumbs with BreadcrumbList structured data on the product and category surfaces, plus skip-to-content, current-page indication, back-to-top, and a mobile bottom navigation in the shared shell.",
129
+ "views": [],
130
+ "state": [],
131
+ "handlers": [],
132
+ "adapter": { "needs": [] },
133
+ "config": {
134
+ "type": "object",
135
+ "additionalProperties": false,
136
+ "properties": {
137
+ "presentation": { "type": "string", "enum": ["default"], "default": "default" },
138
+ "showBackToTop": { "type": "boolean", "default": true }
139
+ }
140
+ },
141
+ "presentations": { "default": "Breadcrumb" },
142
+ "mounts": [
143
+ { "id": "category-breadcrumb", "host": "category", "slot": "breadcrumb" },
144
+ { "id": "product-breadcrumb", "host": "product", "slot": "breadcrumb" }
145
+ ],
146
+ "surfaces": []
147
+ },
125
148
  {
126
149
  "name": "recommendations",
127
150
  "mvp": false,
@@ -23,7 +23,7 @@
23
23
  "properties": {
24
24
  "name": {
25
25
  "type": "string",
26
- "enum": ["home", "product", "category", "checkout", "account", "static", "user-panel"]
26
+ "enum": ["home", "product", "cart", "category", "checkout", "account", "static"]
27
27
  },
28
28
  "description": { "type": "string" },
29
29
  "route": { "type": "string" },
@@ -22,47 +22,69 @@
22
22
  },
23
23
  "optionalSections": ["testimonials", "faq", "statistics", "logoCloud", "pricing"],
24
24
  "registeredSlots": ["hero", "benefits", "categories"],
25
- "allowedSections": ["faq", "cta"],
25
+ "allowedSections": ["faq", "cta", "products"],
26
26
  "maxSections": 4
27
27
  },
28
28
  {
29
29
  "name": "product",
30
30
  "mvp": true,
31
31
  "description": "Single product detail page with overview, gallery, and reviews",
32
- "route": "/productos/[id]",
32
+ "route": "/products/[id]",
33
33
  "loader": "product",
34
34
  "requiredDataKeys": ["selectedProduct"],
35
35
  "defaultVariants": {
36
36
  "overview": "ProductOverview1",
37
- "reviews": "ReviewRating1"
37
+ "reviews": "ReviewRating1",
38
+ "breadcrumb": "Breadcrumb"
38
39
  },
39
40
  "allowedVariants": {
40
41
  "overview": ["ProductOverview1", "ProductOverview2", "ProductOverview5"],
41
42
  "reviews": ["ReviewRating1", "ReviewRating2"],
43
+ "breadcrumb": ["Breadcrumb"],
42
44
  "productCard": ["ProductCard1", "ProductCard2", "ProductCard3", "ProductCard5"]
43
45
  },
44
46
  "optionalSections": ["testimonials", "faq", "statistics"],
45
- "registeredSlots": ["overview", "reviews"],
47
+ "registeredSlots": ["overview", "reviews", "breadcrumb"],
46
48
  "allowedSections": [],
47
49
  "maxSections": 4
48
50
  },
51
+ {
52
+ "name": "cart",
53
+ "mvp": true,
54
+ "description": "Standalone cart review page with authoritative totals and quantity editing",
55
+ "route": "/cart",
56
+ "loader": "cart",
57
+ "requiredDataKeys": ["cart"],
58
+ "defaultVariants": {
59
+ "view": "ShoppingCart4"
60
+ },
61
+ "allowedVariants": {
62
+ "view": ["ShoppingCart4"]
63
+ },
64
+ "optionalSections": [],
65
+ "registeredSlots": ["view"],
66
+ "allowedSections": [],
67
+ "maxSections": 0
68
+ },
49
69
  {
50
70
  "name": "category",
51
71
  "mvp": true,
52
72
  "description": "Product listing filtered by category with filters and sorting",
53
- "route": "/categorias/[[...slug]]",
73
+ "route": "/categories/[[...slug]]",
54
74
  "loader": "category",
55
75
  "requiredDataKeys": ["categoryProducts", "storeCategories"],
56
76
  "defaultVariants": {
57
77
  "filter": "CategoryFilter2",
58
- "productCard": "ProductCard1"
78
+ "productCard": "ProductCard1",
79
+ "breadcrumb": "Breadcrumb"
59
80
  },
60
81
  "allowedVariants": {
61
82
  "filter": ["CategoryFilter1", "CategoryFilter2", "CategoryFilter4"],
62
- "productCard": ["ProductCard1", "ProductCard2", "ProductCard3", "ProductCard5"]
83
+ "productCard": ["ProductCard1", "ProductCard2", "ProductCard3", "ProductCard5"],
84
+ "breadcrumb": ["Breadcrumb"]
63
85
  },
64
86
  "optionalSections": ["logoCloud", "testimonials"],
65
- "registeredSlots": ["filter", "productCard"],
87
+ "registeredSlots": ["breadcrumb", "filter", "productCard"],
66
88
  "allowedSections": ["faq", "cta"],
67
89
  "maxSections": 4
68
90
  },
@@ -90,7 +112,7 @@
90
112
  "name": "account",
91
113
  "mvp": true,
92
114
  "description": "Customer account area with sign-in, registration, and a minimal signed-in state",
93
- "route": "/cuenta",
115
+ "route": "/account",
94
116
  "loader": "account",
95
117
  "requiredDataKeys": ["accountCustomer"],
96
118
  "defaultVariants": {
@@ -111,7 +133,7 @@
111
133
  "name": "static",
112
134
  "mvp": true,
113
135
  "description": "Content-driven static pages (contact, legal, about) with per-page metadata",
114
- "route": "/contacto",
136
+ "route": "/contact",
115
137
  "loader": "static",
116
138
  "requiredDataKeys": [],
117
139
  "defaultVariants": {},
@@ -120,26 +142,6 @@
120
142
  "registeredSlots": [],
121
143
  "allowedSections": [],
122
144
  "maxSections": 0
123
- },
124
- {
125
- "name": "user-panel",
126
- "mvp": false,
127
- "description": "Customer account area with order history and account navigation",
128
- "route": "/cuenta",
129
- "loader": "userPanel",
130
- "requiredDataKeys": ["lastOrder", "lastOrderLines", "orderPlaced"],
131
- "defaultVariants": {
132
- "shell": "AppShell1",
133
- "orderHistory": "OrderHistory1"
134
- },
135
- "allowedVariants": {
136
- "shell": ["AppShell1"],
137
- "orderHistory": ["OrderHistory1", "OrderHistory2"]
138
- },
139
- "optionalSections": [],
140
- "registeredSlots": ["shell", "orderHistory"],
141
- "allowedSections": [],
142
- "maxSections": 4
143
145
  }
144
146
  ]
145
147
  }
@@ -31,6 +31,16 @@
31
31
  "propsHint": "badgeText, title, description, primaryCta/secondaryCta (label, href)",
32
32
  "status": "executable"
33
33
  },
34
+ {
35
+ "name": "products",
36
+ "description": "Data-bound product presentation (grid or combined grid-and-carousel) whose selection is configured independently per instance",
37
+ "elementType": "Products",
38
+ "variants": ["ProductCard1", "ProductShowcase1"],
39
+ "defaultVariant": "ProductCard1",
40
+ "dataBound": true,
41
+ "propsHint": "per-instance selection configured in views/<view>/data.ts sectionSources[id] (canonical category/collection/search/ordering query); heading, description, objective, ctaLabel/ctaHref are editorial content",
42
+ "status": "executable"
43
+ },
34
44
  {
35
45
  "name": "statistics",
36
46
  "description": "Key metrics or trust statistics",
@@ -455,6 +455,27 @@ export function sectionRegistry() {
455
455
  }))
456
456
  }
457
457
 
458
+ /**
459
+ * The full registry entry for a section type, or null when it is not
460
+ * registered. Unlike `sectionRegistry()` (the projected introspection view),
461
+ * this exposes every declared field — including `dataBound` and `variants` —
462
+ * so the CLI can tell which instances need user-owned source configuration.
463
+ */
464
+ export function sectionDefinitionByName(name) {
465
+ const list = Array.isArray(sections.sections) ? sections.sections : []
466
+ return list.find((entry) => isPlainObject(entry) && entry.name === name) ?? null
467
+ }
468
+
469
+ /**
470
+ * Whether a section type resolves its content from a data source configured by
471
+ * the user (for example the `products` type) rather than from static editorial
472
+ * content. Unregistered types are not data-bound.
473
+ */
474
+ export function sectionIsDataBound(name) {
475
+ const section = sectionDefinitionByName(name)
476
+ return section?.status === 'executable' && section.dataBound === true
477
+ }
478
+
458
479
  export function getRecipe(name) {
459
480
  return recipes.views.find((v) => v.name === name) ?? null
460
481
  }
@@ -485,6 +506,39 @@ export function isCuratedFeature(name) {
485
506
  return getFeature(name)?.mvp === true
486
507
  }
487
508
 
509
+ /**
510
+ * The starter composition: every curated (installable) feature plus every
511
+ * curated view no installed curated feature owns. A feature-owned view (for
512
+ * example `checkout`, owned by the `checkout` feature) is installed through its
513
+ * feature, not listed directly. The set is derived from the registry's
514
+ * `mvp: true` declaration — never a hand-maintained list — so a curated unit
515
+ * added or removed flows automatically and a held unit (`recommendations`, a
516
+ * held section) is excluded by construction.
517
+ *
518
+ * `registries` is the injection seam for tests: pass fixture `{ views }` /
519
+ * `{ features }` sources to exercise the derivation without mutating the loaded
520
+ * registry.
521
+ */
522
+ export function starterComposition(registries = {}) {
523
+ const recipeSource = registries.recipes ?? recipes
524
+ const featureSource = registries.features ?? features
525
+ const featureNames = featureSource.features
526
+ .filter((f) => f.mvp === true)
527
+ .map((f) => f.name)
528
+ const ownedViews = new Set()
529
+ for (const name of featureNames) {
530
+ const feature = featureSource.features.find((f) => f.name === name)
531
+ for (const view of Array.isArray(feature?.views) ? feature.views : []) {
532
+ ownedViews.add(view)
533
+ }
534
+ }
535
+ const viewNames = recipeSource.views
536
+ .filter((v) => v.mvp === true)
537
+ .map((v) => v.name)
538
+ .filter((name) => !ownedViews.has(name))
539
+ return { views: viewNames, features: featureNames }
540
+ }
541
+
488
542
  /**
489
543
  * The catalog entry for one slot ("<view>.<slot>"), or null when the slot is
490
544
  * outside the catalog scope (a held, non-curated view). The catalog is the
@@ -42,10 +42,10 @@ export const CANDIDATE_TOOLSET = Object.freeze({
42
42
  '@json-render/next': '0.21.0',
43
43
  }),
44
44
  local: Object.freeze({
45
- '@thorprovider/blocks': '0.1.1',
46
- '@thorprovider/adapters': '5.0.11',
47
- '@thorprovider/runtime': '0.1.2',
48
- '@thorprovider/types': '5.7.7',
45
+ '@thorprovider/blocks': '0.1.2',
46
+ '@thorprovider/adapters': '5.0.12',
47
+ '@thorprovider/runtime': '0.1.4',
48
+ '@thorprovider/types': '5.8.0',
49
49
  }),
50
50
  })
51
51
 
@@ -83,16 +83,27 @@ function isPlainObject(value) {
83
83
  * derived from the manifest — never hardcoded as always-mounted.
84
84
  *
85
85
  * `features` accepts bare names or `{ name, config }` entries (the fresh
86
- * scaffold and a later reconcile both pass manifest entries).
86
+ * scaffold and a later reconcile both pass manifest entries). `views` accepts
87
+ * bare view names or `{ name }` entries; it drives the typed route/action
88
+ * availability flags below so the generated host can suppress links to routes
89
+ * that are not installed (e.g. the cart review page or checkout).
87
90
  */
88
- export function buildCompositionComposer(features) {
91
+ export function buildCompositionComposer(features, views = []) {
89
92
  const entries = (Array.isArray(features) ? features : []).map((feature) =>
90
93
  typeof feature === 'string' ? { name: feature, config: {} } : feature,
91
94
  )
92
95
  const installed = new Set(entries.map((entry) => entry.name))
96
+ const viewNames = (Array.isArray(views) ? views : []).map((view) =>
97
+ typeof view === 'string' ? view : view.name,
98
+ )
99
+ const installedViews = new Set(viewNames)
100
+ const cartViewAvailable = installedViews.has('cart')
101
+ const checkoutAvailable = installed.has('checkout') || installedViews.has('checkout')
93
102
  const names = CATALOG_FEATURE_NAMES.filter((name) => installed.has(name))
94
103
  const list = names.map((name) => JSON.stringify(name)).join(', ')
95
- const configLines = names
104
+ const installedNames = [...installed].sort()
105
+ const featureNameList = installedNames.map((name) => JSON.stringify(name)).join(', ')
106
+ const configLines = installedNames
96
107
  .map((name) => {
97
108
  const entry = entries.find((candidate) => candidate.name === name)
98
109
  const config = entry && isPlainObject(entry.config) ? entry.config : {}
@@ -109,9 +120,18 @@ export function buildCompositionComposer(features) {
109
120
  `// visibility/config from the installed manifest only.\n\n` +
110
121
  `import type { CatalogFeatureName } from '@thorprovider/runtime/contracts'\n\n` +
111
122
  `export const mountedFeatures: readonly CatalogFeatureName[] = [${list}]\n\n` +
112
- `export const mountedFeatureConfigs: Partial<Record<CatalogFeatureName, Record<string, unknown>>> = {\n` +
123
+ `// Every installed feature name, unfiltered by the catalog URL boundary. The\n` +
124
+ `// general presence seam cross-cutting features read to gate their own\n` +
125
+ `// contributions (e.g. the navigability feature's shell chrome).\n` +
126
+ `export const mountedFeatureNames: readonly string[] = [${featureNameList}]\n\n` +
127
+ `export const mountedFeatureConfigs: Partial<Record<string, Record<string, unknown>>> = {\n` +
113
128
  `${configLines ? `${configLines}\n` : ''}` +
114
- `}\n`
129
+ `}\n\n` +
130
+ `// Typed route/action availability derived from the installed manifest. The\n` +
131
+ `// generated host uses these to hide links to routes that are not installed,\n` +
132
+ `// so a removed view never leaves an active dead-end navigation target.\n` +
133
+ `export const cartViewAvailable = ${cartViewAvailable}\n\n` +
134
+ `export const checkoutAvailable = ${checkoutAvailable}\n`
115
135
  )
116
136
  }
117
137
 
@@ -0,0 +1,86 @@
1
+ // Environment-file helpers for the scaffold.
2
+ //
3
+ // `create` starts a storefront in mock mode with a minimal, working `.env.local`
4
+ // and never writes a blank `MEDUSA_*` value; `setup` later upserts the real-backend
5
+ // keys without disturbing anything else in the file. Both behaviours are pure
6
+ // enough to unit-test here rather than only through the network-heavy create path.
7
+ import { existsSync, readFileSync } from 'node:fs'
8
+ import { writeFile } from 'node:fs/promises'
9
+ import { randomBytes } from 'node:crypto'
10
+ import { join } from 'node:path'
11
+
12
+ // The minimal mock environment `create` writes: an explicit mock data source, the
13
+ // store identity, and the per-install session secret the guest cart and checkout
14
+ // cookie signing requires. No backend key is written here.
15
+ export function initialEnvLocal(name, sessionSecret) {
16
+ return [
17
+ 'THOR_DATA_SOURCE=mock',
18
+ `THOR_STORE_ID=${name}`,
19
+ `THOR_SESSION_SECRET=${sessionSecret}`,
20
+ ].join('\n') + '\n'
21
+ }
22
+
23
+ // The committed example documenting both modes. The real-backend keys are blank
24
+ // placeholders — `storefront setup` fills them.
25
+ export function envExample(name) {
26
+ return [
27
+ '# Data source: "mock" for explicit demo data (no backend needed), "medusa" for a real backend.',
28
+ 'THOR_DATA_SOURCE=mock',
29
+ '# Store identity (written by create; required only in real mode).',
30
+ `THOR_STORE_ID=${name}`,
31
+ '# Real backend configuration (required only when THOR_DATA_SOURCE=medusa).',
32
+ '# Run `storefront setup` to write these: it takes the sales channel id (from the',
33
+ '# Medusa admin) and derives the currency from the selected region.',
34
+ 'MEDUSA_BASE_URL=http://localhost:9000',
35
+ 'MEDUSA_PUBLISHABLE_KEY=',
36
+ 'MEDUSA_REGION_ID=',
37
+ 'MEDUSA_SALES_CHANNEL_ID=',
38
+ 'MEDUSA_CURRENCY_CODE=',
39
+ '# Host-only cookie signing secret; >=16 chars, 32 recommended. Generated by create.',
40
+ 'THOR_SESSION_SECRET=',
41
+ ].join('\n') + '\n'
42
+ }
43
+
44
+ // Write the initial `.env.local` only when one is absent (an existing file is
45
+ // user-owned — a `setup` run or a hand edit — and is never clobbered), and always
46
+ // refresh the committed `.env.example`. Returns whether `.env.local` was written.
47
+ export async function writeInitialEnv(target, name) {
48
+ const envPath = join(target, '.env.local')
49
+ let wrote = false
50
+ if (!existsSync(envPath)) {
51
+ const sessionSecret = randomBytes(32).toString('base64url')
52
+ await writeFile(envPath, initialEnvLocal(name, sessionSecret), 'utf8')
53
+ wrote = true
54
+ }
55
+ await writeFile(join(target, '.env.example'), envExample(name), 'utf8')
56
+ return wrote
57
+ }
58
+
59
+ // Replace the managed keys in a dotenv-style file, appending any that are absent,
60
+ // and leave every other line (comments, blank lines, unrelated keys) exactly as it
61
+ // was. Idempotent: re-running with the same values produces the same file.
62
+ export function upsertEnv(text, managed) {
63
+ const seen = new Set()
64
+ const lines = text.length ? text.split('\n') : []
65
+ const out = lines.map((line) => {
66
+ const match = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line)
67
+ if (match && Object.prototype.hasOwnProperty.call(managed, match[1])) {
68
+ seen.add(match[1])
69
+ return `${match[1]}=${managed[match[1]]}`
70
+ }
71
+ return line
72
+ })
73
+ const missing = Object.keys(managed).filter((key) => !seen.has(key))
74
+ // Drop trailing blank lines before appending so re-runs do not accumulate them.
75
+ while (out.length && out[out.length - 1] === '') out.pop()
76
+ if (missing.length) {
77
+ if (out.length) out.push('')
78
+ for (const key of missing) out.push(`${key}=${managed[key]}`)
79
+ }
80
+ return out.join('\n') + '\n'
81
+ }
82
+
83
+ // Read a dotenv file as text, or '' when it does not exist.
84
+ export function readEnvFile(path) {
85
+ return existsSync(path) ? readFileSync(path, 'utf8') : ''
86
+ }