@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,169 @@
1
+ // Generated by Thor Commerce storefront (composition core: session host).
2
+ //
3
+ // Host session cookie — the Next-bound half of the guest session. `./session.core`
4
+ // owns the pure signing and validation; this module owns the cookie.
5
+ //
6
+ // Next.js 16 `cookies()` is async and may only be MUTATED from a Server Action
7
+ // or a Route Handler — never during a Server Component render. `readHostSession`
8
+ // is therefore the only render-safe export; every other export writes or clears
9
+ // the cookie and MUST be called from an action or route. This is why a guest
10
+ // identity is created in an action/route bootstrap, never in a read-only render.
11
+ //
12
+ // The signing secret is required host configuration (`THOR_SESSION_SECRET`),
13
+ // independent of the backend. It is never embedded, generated as a constant, or
14
+ // printed; a missing or too-short secret throws before any cookie is written.
15
+ // No external SDK is called here; commerce data stays behind adapters/runtime.
16
+
17
+ import { cookies } from 'next/headers'
18
+ import type { StoreContext } from '@thorprovider/types'
19
+ import {
20
+ AUTH_TOKEN_TTL_SECONDS,
21
+ SESSION_TTL_SECONDS,
22
+ authCookieName,
23
+ bindSessionCart,
24
+ completeSessionCart,
25
+ createHostAuth,
26
+ createHostSession,
27
+ decodeHostAuth,
28
+ decodeHostSession,
29
+ encodeHostAuth,
30
+ encodeHostSession,
31
+ sessionCookieName,
32
+ type HostSessionPayload,
33
+ } from './session.core'
34
+
35
+ /** Host environment variable that carries the cookie signing secret. */
36
+ export const SESSION_SECRET_ENV = 'THOR_SESSION_SECRET'
37
+
38
+ /** Read the required host signing secret; throws (loud) when unset. */
39
+ export function getSessionSecret(): string {
40
+ const secret = process.env[SESSION_SECRET_ENV]
41
+ if (!secret || secret.trim() === '') {
42
+ throw new Error(
43
+ `${SESSION_SECRET_ENV} is required to sign the storefront session cookie; set it in the host environment (never commit or log it).`,
44
+ )
45
+ }
46
+ return secret
47
+ }
48
+
49
+ /** Hardened cookie flags: httpOnly, SameSite=Lax, secure in production, TTL. */
50
+ function cookieOptions() {
51
+ return {
52
+ httpOnly: true,
53
+ sameSite: 'lax' as const,
54
+ secure: process.env.NODE_ENV === 'production',
55
+ path: '/',
56
+ maxAge: SESSION_TTL_SECONDS,
57
+ }
58
+ }
59
+
60
+ /**
61
+ * READ-ONLY — safe during a Server Component render.
62
+ *
63
+ * Returns the verified session, or null for a missing/tampered/expired/wrong-store
64
+ * cookie. A null result means "no trusted identity"; the caller must obtain one
65
+ * from a Server Action or Route Handler, never from client-supplied ids.
66
+ */
67
+ export async function readHostSession(
68
+ context: StoreContext,
69
+ ): Promise<HostSessionPayload | null> {
70
+ const store = await cookies()
71
+ const raw = store.get(sessionCookieName(context.storeId))?.value
72
+ return decodeHostSession(raw, getSessionSecret(), { expectedContext: context })
73
+ }
74
+
75
+ /** MUTATION — Server Action or Route Handler only. Issues a fresh identity. */
76
+ export async function issueHostSession(context: StoreContext): Promise<HostSessionPayload> {
77
+ const payload = createHostSession(context)
78
+ await writeHostSession(context, payload)
79
+ return payload
80
+ }
81
+
82
+ /**
83
+ * MUTATION — Server Action or Route Handler only. Attaches the active backend
84
+ * cart to the current identity, issuing one first if the cookie is absent or
85
+ * untrusted (regenerate instead of trusting a client id).
86
+ */
87
+ export async function bindHostSessionCart(
88
+ context: StoreContext,
89
+ cartId: string,
90
+ ): Promise<HostSessionPayload> {
91
+ const current = await readHostSession(context)
92
+ const next = bindSessionCart(current ?? createHostSession(context), cartId)
93
+ await writeHostSession(context, next)
94
+ return next
95
+ }
96
+
97
+ /**
98
+ * MUTATION — Server Action or Route Handler only.
99
+ *
100
+ * Clears the active cart ONLY on a confirmed order and preserves `receiptCartId`
101
+ * for recovery. `receiptCartId` must come from the host's own completed cart,
102
+ * never from client input. On any error or unresolved completion the caller
103
+ * must NOT call this, so the active cart is retained and no receipt is recorded.
104
+ */
105
+ export async function completeHostSessionCart(
106
+ context: StoreContext,
107
+ receiptCartId: string,
108
+ ): Promise<HostSessionPayload> {
109
+ const current = await readHostSession(context)
110
+ const next = completeSessionCart(current ?? createHostSession(context), receiptCartId)
111
+ await writeHostSession(context, next)
112
+ return next
113
+ }
114
+
115
+ /** MUTATION — Server Action or Route Handler only. Clears the session cookie. */
116
+ export async function clearHostSession(context: StoreContext): Promise<void> {
117
+ const store = await cookies()
118
+ store.delete(sessionCookieName(context.storeId))
119
+ }
120
+
121
+ async function writeHostSession(
122
+ context: StoreContext,
123
+ payload: HostSessionPayload,
124
+ ): Promise<void> {
125
+ const store = await cookies()
126
+ store.set(
127
+ sessionCookieName(context.storeId),
128
+ encodeHostSession(payload, getSessionSecret()),
129
+ cookieOptions(),
130
+ )
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Customer auth cookie (change: storefront-customer-auth, task 2.1)
135
+ // ---------------------------------------------------------------------------
136
+ //
137
+ // The auth cookie is separate from the guest session cookie and reuses the same
138
+ // host-only secret and hardened flags. It carries only the customer JWT; the
139
+ // guest session payload/version and the guest cart are never read or written
140
+ // here. As with the guest session, only `readAuthToken` is render-safe.
141
+
142
+ /**
143
+ * READ-ONLY — safe during a Server Component render.
144
+ *
145
+ * Returns the verified customer token, or null for a missing/tampered/expired/
146
+ * wrong-store cookie. A null result means "not authenticated".
147
+ */
148
+ export async function readAuthToken(context: StoreContext): Promise<string | null> {
149
+ const store = await cookies()
150
+ const raw = store.get(authCookieName(context.storeId))?.value
151
+ const payload = decodeHostAuth(raw, getSessionSecret(), { expectedStoreId: context.storeId })
152
+ return payload?.token ?? null
153
+ }
154
+
155
+ /** MUTATION — Server Action or Route Handler only. Persists the customer token. */
156
+ export async function writeAuthToken(context: StoreContext, token: string): Promise<void> {
157
+ const store = await cookies()
158
+ store.set(
159
+ authCookieName(context.storeId),
160
+ encodeHostAuth(createHostAuth(context.storeId, token), getSessionSecret()),
161
+ { ...cookieOptions(), maxAge: AUTH_TOKEN_TTL_SECONDS },
162
+ )
163
+ }
164
+
165
+ /** MUTATION — Server Action or Route Handler only. Clears the auth cookie. */
166
+ export async function clearAuthToken(context: StoreContext): Promise<void> {
167
+ const store = await cookies()
168
+ store.delete(authCookieName(context.storeId))
169
+ }
@@ -0,0 +1,96 @@
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 (`/`, `/categorias`, `/checkout`, and the static pages `/contacto`,
10
+ // `/sobre-nosotros`, `/terminos`, `/privacidad`). 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
+ /** Neutral footer links — only routes the storefront serves; no dead `#` links. */
71
+ export const linkColumns: ShellLinkColumn[] = [
72
+ {
73
+ title: 'Comprar',
74
+ links: [
75
+ { label: 'Inicio', href: '/' },
76
+ { label: 'Todas las categorías', href: '/categorias' },
77
+ { label: 'Finalizar compra', href: '/checkout' },
78
+ ],
79
+ },
80
+ {
81
+ title: 'Información',
82
+ links: [
83
+ { label: 'Contacto', href: '/contacto' },
84
+ { label: 'Sobre nosotros', href: '/sobre-nosotros' },
85
+ { label: 'Términos y condiciones', href: '/terminos' },
86
+ { label: 'Política de privacidad', href: '/privacidad' },
87
+ ],
88
+ },
89
+ ]
90
+
91
+ /**
92
+ * Footer newsletter content. The scaffold has no working submit handler, so it
93
+ * is omitted by default (`false`). Setting it without a host-provided callback
94
+ * renders no form — never a fake one.
95
+ */
96
+ export const newsletter: false | ShellNewsletter = false
@@ -0,0 +1,34 @@
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
+ } from './content'
16
+
17
+ export const navbarShellProps = {
18
+ logoAccent,
19
+ className: navbarClassName,
20
+ // Global navigation search is held until a verified backend search contract
21
+ // exists (E2 task 5.4), so the generated storefront presents NO search
22
+ // control. A merchant may set this to `true` to opt in once the contract is
23
+ // verified; the `Navbar4` block still defaults to visible for legacy
24
+ // consumers that do not set this prop.
25
+ showSearch: false,
26
+ }
27
+
28
+ export const footerShellProps = {
29
+ brandAccent,
30
+ brandDescription,
31
+ linkColumns,
32
+ className: footerClassName,
33
+ ...(newsletter ? { newsletter } : {}),
34
+ }
@@ -3,23 +3,110 @@
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'
7
9
  import type { StorefrontElement } from './types'
8
10
 
9
11
  export type { StorefrontElement }
10
12
 
13
+ // The spec's state is a plain record; spreading the typed initial state keeps
14
+ // the single source of truth in ../state while satisfying the index signature.
15
+ const initialState: Record<string, unknown> = { ...storefrontInitialState }
16
+
11
17
  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
- ]),
18
+ views.flatMap((view) => {
19
+ // A view declares one route or several; normalise to an array and flatten so
20
+ // a multi-route view (content-driven static pages) contributes one page per
21
+ // declared route while a single-route view is unchanged.
22
+ const viewRoutes = Array.isArray(view.route) ? view.route : [view.route]
23
+ return viewRoutes.map((route) => [
24
+ route.path,
25
+ {
26
+ layout: route.layout,
27
+ loader: route.loaderName,
28
+ metadata: route.metadata,
29
+ ...route.entry,
30
+ },
31
+ ])
32
+ }),
21
33
  )
22
34
 
35
+ // The default layout is the storefront chrome shared by every page: the
36
+ // system notice region sits in the notices slot (always first, above the
37
+ // header), the announcement banner and navbar live in the header slot, the
38
+ // footer in the footer slot, and the cart drawer is a page-level child of
39
+ // Layout (rendered outside the slots so it can overlay the whole shell). This
40
+ // mirrors the reference app so a fresh scaffold ships a real storefront, not a
41
+ // bare page.
42
+ const shellElements: Record<string, StorefrontElement> = {
43
+ // Reserved top region for system/status messages. Only the shell layout
44
+ // references it, so it renders once and always as the first element.
45
+ notice: {
46
+ type: 'SystemNotice',
47
+ props: {},
48
+ },
49
+ banner: {
50
+ type: 'Banner',
51
+ props: {
52
+ badge: 'Novedades',
53
+ message: 'Descubre el catálogo de __SITE_NAME__.',
54
+ href: '/categorias',
55
+ linkLabel: 'Ver catálogo',
56
+ },
57
+ visible: { $state: '/bannerDismissed', not: true },
58
+ on: { dismissBanner: { action: 'dismissBanner' } },
59
+ },
60
+ navbar: {
61
+ type: 'StorefrontNavbar',
62
+ props: {
63
+ storeCategories: { $state: '/navCategories' },
64
+ // Neutral store info: no delivery/COD/shipping promise is made by default.
65
+ shippingNote: 'Catálogo en línea',
66
+ utilityLinks: [],
67
+ logoText: '__SITE_NAME__',
68
+ logoHref: '/',
69
+ // Optional wordmark accent and surface classes, configured in the
70
+ // user-owned shell seam. Spread last so merchant edits win.
71
+ ...navbarShellProps,
72
+ },
73
+ on: {
74
+ openCart: { action: 'openCart' },
75
+ goToCheckout: { action: 'goToCheckout' },
76
+ applyQuantityDelta: { action: 'applyQuantityDelta' },
77
+ removeFromCart: { action: 'removeFromCart' },
78
+ },
79
+ },
80
+ 'cart.drawer': {
81
+ type: 'CartDrawer',
82
+ props: {
83
+ // No fabricated free-shipping threshold or reward: the backend is the only
84
+ // source of shipping amounts, and a promotion promise is not supported by
85
+ // the default fixture. A merchant may add promotions as customization.
86
+ promotions: [],
87
+ taxRate: 0.0,
88
+ },
89
+ visible: { $state: '/cartOpen', eq: true },
90
+ on: {
91
+ updateQuantity: { action: 'updateQuantity' },
92
+ removeFromCart: { action: 'removeFromCart' },
93
+ goToCheckout: { action: 'goToCheckout' },
94
+ closeCart: { action: 'closeCart' },
95
+ refreshCart: { action: 'refreshCart' },
96
+ },
97
+ },
98
+ footer: {
99
+ type: 'Footer',
100
+ props: {
101
+ brandName: '__SITE_NAME__',
102
+ // Footer brand description, links, accent, and surface classes all come
103
+ // from the user-owned shell seam. Spread last so merchant edits win; no
104
+ // delivery/shipping promise and no dead `#` link is shipped by default.
105
+ ...footerShellProps,
106
+ },
107
+ },
108
+ }
109
+
23
110
  export const __STOREFRONT_CAMEL__Spec: NextAppSpec = {
24
111
  metadata: {
25
112
  title: {
@@ -28,7 +115,7 @@ export const __STOREFRONT_CAMEL__Spec: NextAppSpec = {
28
115
  },
29
116
  description: 'Tienda online de __SITE_NAME__.',
30
117
  },
31
- state: {},
118
+ state: initialState,
32
119
  layouts: {
33
120
  default: {
34
121
  root: 'layout',
@@ -36,11 +123,13 @@ export const __STOREFRONT_CAMEL__Spec: NextAppSpec = {
36
123
  layout: {
37
124
  type: 'Layout',
38
125
  props: {},
39
- children: ['slot'],
126
+ slots: { notices: ['notice'], header: ['banner', 'navbar'], footer: ['footer'] },
127
+ children: ['slot', 'cart.drawer'],
40
128
  },
41
129
  // The page content is injected at the Slot element by @json-render/next.
42
130
  // Without it the rendered layout drops the page and the store looks empty.
43
131
  slot: { type: 'Slot', props: {}, children: [] },
132
+ ...shellElements,
44
133
  },
45
134
  },
46
135
  },
@@ -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,26 @@
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
+ /** The routes the storefront can serve as fixed pages (no dynamic `[` segment). */
7
+ export function fixedRoutePaths(routes: Record<string, unknown>): string[] {
8
+ return Object.keys(routes).filter((path) => !path.includes('['))
9
+ }
10
+
11
+ /**
12
+ * Absolute sitemap URLs for the fixed routes. Emits nothing when no site URL is
13
+ * configured, so no canonical URL is invented.
14
+ */
15
+ export function sitemapUrls(
16
+ routes: Record<string, unknown>,
17
+ siteUrl: string | undefined,
18
+ ): string[] {
19
+ if (!siteUrl) return []
20
+ return fixedRoutePaths(routes).map((path) => new URL(path, siteUrl).toString())
21
+ }
22
+
23
+ /** Absolute URL of the sitemap for `robots.txt`, or undefined without a site URL. */
24
+ export function robotsSitemapUrl(siteUrl: string | undefined): string | undefined {
25
+ return siteUrl ? new URL('/sitemap.xml', siteUrl).toString() : undefined
26
+ }