@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,76 @@
1
+ # AGENTS.md — __SITE_NAME__ storefront
2
+
3
+ This storefront was generated by `@thorprovider/storefront`. It is governed by a
4
+ manifest (`storefront.config.json` at the project root) and produced by the
5
+ `storefront` CLI. This file is managed: `storefront sync` regenerates it from the
6
+ package template, and `storefront doctor` reports it as drift when edited by
7
+ hand. Read it before changing anything.
8
+
9
+ ## RULE 0 — Non-negotiable
10
+
11
+ 1. **The `storefront` CLI is the authority.** Every structural change — adding or
12
+ removing a view or feature, composing sections, changing the theme mode,
13
+ connecting the backend — goes through `npx @thorprovider/storefront …`. Never
14
+ copy templates or scaffold structure by hand.
15
+ 2. **Derived files are CLI-only; never hand-edit them.** The CLI is the only
16
+ writer of derived files. If a change seems to need a capability the CLI does
17
+ not expose, first check the CLI subcommands and the `plan` → `apply`
18
+ operations envelope; if it is still missing, **STOP and report the gap** —
19
+ do not hand-edit a derived file. A hand edit is silently overwritten by the
20
+ next `sync`.
21
+ 3. **Hand-edit only user-owned files and the bounded external surface.** Manual
22
+ edits are limited to the declared user-owned files:
23
+ `lib/<name>/views/<view>/{schema,content,props,data}.ts`, `lib/<name>/mappers.ts`,
24
+ `lib/<name>/icon-map.ts`, and `lib/<name>/shell/**` — plus the bounded external
25
+ customization surface of `app/globals.css` and `app/layout.tsx`: theme design
26
+ tokens and their foreground pairs, font wiring, and global identity/metadata.
27
+ Prefer the deterministic `storefront theme` controls (colors, radius, fonts,
28
+ presets, imports) over hand edits; the bounded surface remains editable when a
29
+ value has no CLI control.
30
+ Editing the external files must preserve the CLI-managed wiring: the library
31
+ stylesheet import, the provider import and marker, the theme-mode marker, and
32
+ the block token contract. The CLI only appends that wiring idempotently, so a
33
+ restructure that removes it is drift, not a customization. Everything else is
34
+ derived (CLI-only).
35
+ 4. **Respect the architecture invariants.** UI components come only from
36
+ `@thorprovider/blocks`; data comes only from `@thorprovider/adapters` and
37
+ `@thorprovider/runtime/commerce` (never HTTP directly); routing stays the
38
+ catch-all `app/[[...slug]]/page.tsx`; `@thorprovider/medusa-extended` is not
39
+ used.
40
+ 5. **Verify after every change.** Run `npx @thorprovider/storefront sync`, then
41
+ `npx @thorprovider/storefront doctor`. `doctor` must report no drift; a build
42
+ (`yarn build`) is the final acceptance.
43
+ 6. **Drive non-trivial customization through the spec-driven loop.** A change
44
+ that needs judgment — content authoring, prop/data mapping, composition
45
+ reshaping, or a cross-file redesign whose acceptance is not a CLI assertion —
46
+ is **non-trivial**: drive it through the spec-driven loop and surface the five
47
+ stages to the user, **explore → plan → build → verify → inform**. A change the
48
+ CLI performs deterministically (theme mode, `view add|remove`,
49
+ `feature add|remove`, `setup`) is recorded in `storefront.config.json` and does
50
+ **not** require an OpenSpec change — the manifest is its record. Never ask the
51
+ user to run `openspec` or to approve individual artifacts.
52
+
53
+ ## Workflow model
54
+
55
+ **bootstrap → compose → customize → verify.** The manifest
56
+ (`storefront.config.json`) is the structural source of truth; the CLI is the only
57
+ writer of derived files; the developer (or agent) edits only user-owned files and
58
+ the bounded external surface of `app/globals.css` and `app/layout.tsx`.
59
+ Customization that needs judgment runs through the spec-driven loop surfaced as
60
+ explore → plan → build → verify → inform.
61
+ See the `storefront-workflow` skill for the full model and the ownership classes.
62
+
63
+ ## Router — intent → skill → CLI
64
+
65
+ The user states the intent in natural language; load the skill and delegate the
66
+ write to the CLI. Never edit a derived file to achieve the intent.
67
+
68
+ | Intent | Load skill | Delegate to CLI |
69
+ |---|---|---|
70
+ | Add or remove a view; compose a view's sections | `sf-view-gen` | `storefront views --json`, then `storefront view add\|remove …`; sections via `plan` → `apply` |
71
+ | Customize the theme: colors, radius, fonts, presets; identity/SEO | `sf-theme-gen` | `storefront theme` — `--brand orange`, `--primary <color>`, `--accent <color>`, `--radius <length>`, `--font-sans google:Inter`, `--set light.primary=#f97316`, `--preset <ref>`, `--import <src>`, `--export <file>`, `--mode light\|dark\|system`. Visual values live in `app/globals.css`; the mode in `storefront.config.json`. Inspect with `storefront theme --inspect`. The bounded external surface stays editable by hand |
72
+ | Add or remove a feature (e.g. `checkout`) | `sf-feature` | `storefront features --json`, then `storefront feature add\|remove …` |
73
+ | Connect or re-point the Medusa backend; change region, sales channel, or publishable key | `sf-setup` | `storefront setup --backend-url … --publishable-key … --sales-channel-id … --region-id …` |
74
+ | Edit a view's content/props/data; map props or data; reshape composition; any change whose acceptance is not a CLI assertion (non-trivial) | `storefront-workflow` | spec-driven loop surfaced as **explore → plan → build → verify → inform** — the agent drives it and never asks the user to confirm OpenSpec artifacts; structural edits still go through `storefront …`, verified with `storefront sync` then `storefront doctor` |
75
+
76
+ The step-by-step procedures live in the skills, not in this file.
@@ -1,6 +1,7 @@
1
1
  // Generated by Thor Commerce storefront (template: assets/templates/app/app/[[...slug]]/page.tsx)
2
2
 
3
3
  import { notFound } from 'next/navigation'
4
+ import { buildBreadcrumbListJsonLd, serializeJsonLd } from '@thorprovider/runtime/contracts'
4
5
  import { __STOREFRONT_CAMEL__App } from '@/lib/__STOREFRONT__/runtime'
5
6
  import { readCartStateForRender } from '@/lib/__STOREFRONT__/cart-state'
6
7
  import { StorefrontRenderer } from './renderer'
@@ -24,5 +25,45 @@ export default async function Page({
24
25
  const cartState = await readCartStateForRender()
25
26
  const initialState = { ...(data.initialState ?? {}), ...cartState }
26
27
 
27
- return <StorefrontRenderer {...data} initialState={initialState} />
28
+ // Entity structured data is server-rendered from the real loaded entity and
29
+ // sanitized so entity text cannot terminate the script element.
30
+ const jsonLd = data.entitySeo?.jsonLd
31
+ // The entity OpenGraph type (`product`/`CollectionPage`) is emitted here, not
32
+ // through Next metadata: Next validates `og:type` against the OpenGraph
33
+ // vocabulary and rejects those entity types, so a raw `property` tag is the
34
+ // only way to carry the entity type into the rendered `<head>`.
35
+ const ogType = data.entitySeo?.openGraph?.type
36
+ // The navigability feature's real trail is server-rendered as a sanitized
37
+ // `BreadcrumbList` alongside the entity structured data. It is built from the
38
+ // loader's own state, so it always matches the rendered breadcrumb.
39
+ const breadcrumbItems = (data.initialState as Record<string, unknown> | undefined)
40
+ ?.breadcrumbItems
41
+ const breadcrumbJsonLd = buildBreadcrumbListJsonLd(
42
+ Array.isArray(breadcrumbItems)
43
+ ? (breadcrumbItems as { label?: string; href?: string }[])
44
+ : undefined,
45
+ )
46
+
47
+ return (
48
+ <>
49
+ {ogType ? <meta property="og:type" content={ogType} /> : null}
50
+ {jsonLd ? (
51
+ <script
52
+ type="application/ld+json"
53
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(jsonLd) }}
54
+ />
55
+ ) : null}
56
+ {breadcrumbJsonLd ? (
57
+ <script
58
+ type="application/ld+json"
59
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(breadcrumbJsonLd) }}
60
+ />
61
+ ) : null}
62
+ <StorefrontRenderer
63
+ spec={data.spec}
64
+ layoutSpec={data.layoutSpec}
65
+ initialState={initialState}
66
+ />
67
+ </>
68
+ )
28
69
  }
@@ -2,9 +2,20 @@
2
2
 
3
3
  // Generated by Thor Commerce storefront (template: assets/templates/app/app/[[...slug]]/renderer.tsx)
4
4
 
5
+ import { useRef } from 'react'
5
6
  import { PageRenderer } from '@json-render/next'
6
7
  import type { PageData } from '@json-render/next'
7
8
 
8
- export function StorefrontRenderer(data: PageData) {
9
- return <PageRenderer {...data} />
9
+ export function StorefrontRenderer({ initialState, ...data }: PageData) {
10
+ // The server recomputes `initialState` on every render, and a Server Action
11
+ // triggers an RSC refresh — so the prop is a fresh object each time even when
12
+ // its content is unchanged. `@json-render/react`'s StateProvider resets the
13
+ // client store whenever the `initialState` reference changes, which would wipe
14
+ // client-owned state (for example the account order history) after an action.
15
+ // Keep a STABLE reference while the serialized content is unchanged; a genuine
16
+ // server-side change (for example the cart) still propagates a new value.
17
+ const key = JSON.stringify(initialState ?? {})
18
+ const stable = useRef({ key, value: initialState })
19
+ if (stable.current.key !== key) stable.current = { key, value: initialState }
20
+ return <PageRenderer {...data} initialState={stable.current.value} />
10
21
  }
@@ -28,10 +28,10 @@
28
28
  --color-border: var(--border);
29
29
  --color-input: var(--input);
30
30
  --color-ring: var(--ring);
31
- --radius-sm: var(--radius-sm);
32
- --radius-md: var(--radius-md);
33
- --radius-lg: var(--radius-lg);
34
- --radius-xl: var(--radius-xl);
31
+ --radius-sm: calc(var(--radius) - 4px);
32
+ --radius-md: calc(var(--radius) - 2px);
33
+ --radius-lg: var(--radius);
34
+ --radius-xl: calc(var(--radius) + 4px);
35
35
  --font-sans: var(--font-geist-sans);
36
36
  }
37
37
 
@@ -59,10 +59,7 @@
59
59
  --border: oklch(0.88 0.003 90);
60
60
  --input: oklch(0.92 0.003 90);
61
61
  --ring: oklch(0.15 0 0);
62
- --radius-sm: 0.25rem;
63
- --radius-md: 0.375rem;
64
- --radius-lg: 0.5rem;
65
- --radius-xl: 0.75rem;
62
+ --radius: 0.5rem;
66
63
  }
67
64
 
68
65
  .dark {
@@ -4,8 +4,6 @@ import type { Metadata } from 'next'
4
4
  import { Geist } from 'next/font/google'
5
5
  import { ThemeProvider } from 'next-themes'
6
6
  import { StorefrontProvider } from '@thorprovider/runtime/host-glue/providers'
7
- import { themeVarsToCss, themeTokensToVars } from '@thorprovider/runtime/render-core/theme'
8
- import { DEFAULT_THEME_TOKENS } from '@thorprovider/runtime/contracts'
9
7
  import { registry } from '@/lib/__STOREFRONT__/registry'
10
8
  import { getSiteUrl } from '@/lib/__STOREFRONT__/store-context'
11
9
  import './globals.css'
@@ -15,8 +13,6 @@ const geist = Geist({
15
13
  subsets: ['latin'],
16
14
  })
17
15
 
18
- const themeCss = themeVarsToCss(themeTokensToVars(DEFAULT_THEME_TOKENS))
19
-
20
16
  // The canonical public origin is optional: when THOR_SITE_URL is unset no base
21
17
  // URL is invented. `metadataBase` is left undefined in that case.
22
18
  const siteUrl = getSiteUrl()
@@ -39,7 +35,7 @@ export default function RootLayout({
39
35
  <html lang="es" className={geist.variable} suppressHydrationWarning>
40
36
  <body className="font-sans antialiased">
41
37
  <ThemeProvider attribute="class" defaultTheme="__STOREFRONT_MODE__" enableSystem>
42
- <StorefrontProvider themeCss={themeCss} registry={registry}>
38
+ <StorefrontProvider registry={registry}>
43
39
  {children}
44
40
  </StorefrontProvider>
45
41
  </ThemeProvider>
@@ -1,13 +1,19 @@
1
1
  // Generated by Thor Commerce storefront (template: assets/templates/app/app/robots.ts)
2
- // Always served. Points at the sitemap only when THOR_SITE_URL is configured.
2
+ // Always served. Allows public pages, disallows private surfaces, and points at
3
+ // the sitemap only when THOR_SITE_URL is configured.
3
4
 
4
5
  import type { MetadataRoute } from 'next'
5
- import { robotsSitemapUrl } from '@/lib/__STOREFRONT__/spec/seo'
6
+ import { __STOREFRONT_CAMEL__Spec } from '@/lib/__STOREFRONT__/spec'
7
+ import { robotsDisallowPaths, robotsSitemapUrl } from '@/lib/__STOREFRONT__/spec/seo'
6
8
  import { getSiteUrl } from '@/lib/__STOREFRONT__/store-context'
7
9
 
8
10
  export default function robots(): MetadataRoute.Robots {
9
11
  return {
10
- rules: { userAgent: '*', allow: '/' },
12
+ rules: {
13
+ userAgent: '*',
14
+ allow: '/',
15
+ disallow: robotsDisallowPaths(__STOREFRONT_CAMEL__Spec.routes ?? {}),
16
+ },
11
17
  sitemap: robotsSitemapUrl(getSiteUrl()),
12
18
  }
13
19
  }
@@ -1,12 +1,71 @@
1
1
  // Generated by Thor Commerce storefront (template: assets/templates/app/app/sitemap.ts)
2
- // Lists the storefront's fixed routes. Absolute URLs are emitted only when
3
- // THOR_SITE_URL is configured; otherwise the sitemap is empty (no invented URL).
2
+ // Lists the fixed public routes plus every public catalog entity, enumerated
3
+ // across adapter pagination with a bound and a cycle guard. Absolute URLs are
4
+ // emitted only when THOR_SITE_URL is configured; otherwise the sitemap is empty
5
+ // (no invented URL). An enumeration failure fails loudly (no partial sitemap).
4
6
 
5
7
  import type { MetadataRoute } from 'next'
8
+ import { getCatalog, getCatalogResults } from '@thorprovider/runtime/commerce'
6
9
  import { __STOREFRONT_CAMEL__Spec } from '@/lib/__STOREFRONT__/spec'
7
- import { sitemapUrls } from '@/lib/__STOREFRONT__/spec/seo'
10
+ import {
11
+ buildSitemapEntries,
12
+ enumeratePublicEntities,
13
+ SitemapEnumerationError,
14
+ type PublicEntityPage,
15
+ } from '@/lib/__STOREFRONT__/spec/seo'
8
16
  import { getSiteUrl } from '@/lib/__STOREFRONT__/store-context'
9
17
 
10
- export default function sitemap(): MetadataRoute.Sitemap {
11
- return sitemapUrls(__STOREFRONT_CAMEL__Spec.routes ?? {}, getSiteUrl()).map((url) => ({ url }))
18
+ const ENTITY_PAGE_SIZE = 100
19
+ const MAX_PAGES = 100
20
+
21
+ async function productPage(cursor: string | undefined): Promise<PublicEntityPage> {
22
+ const page = cursor ? Number(cursor) : 1
23
+ const outcome = await getCatalogResults({
24
+ page,
25
+ pageSize: ENTITY_PAGE_SIZE,
26
+ sort: 'name',
27
+ })
28
+ if (!outcome.ok) {
29
+ throw new SitemapEnumerationError({
30
+ kind: 'enumeration',
31
+ message: outcome.error.message,
32
+ })
33
+ }
34
+ const items = outcome.results.products.map((product) => ({
35
+ id: product.id,
36
+ path: `/products/${product.handle}`,
37
+ }))
38
+ const total = outcome.results.total
39
+ const hasMore =
40
+ typeof total === 'number' ? page * ENTITY_PAGE_SIZE < total : items.length >= ENTITY_PAGE_SIZE
41
+ return hasMore ? { items, nextCursor: String(page + 1) } : { items }
42
+ }
43
+
44
+ async function categoryPage(): Promise<PublicEntityPage> {
45
+ const catalog = await getCatalog()
46
+ return {
47
+ items: catalog.categoriesRaw.map((category) => ({
48
+ id: category.id,
49
+ path: `/categories/${category.handle}`,
50
+ })),
51
+ }
52
+ }
53
+
54
+ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
55
+ const siteUrl = getSiteUrl()
56
+ if (!siteUrl) return []
57
+
58
+ const routes = __STOREFRONT_CAMEL__Spec.routes ?? {}
59
+ const [products, categories] = await Promise.all([
60
+ enumeratePublicEntities(productPage, { maxPages: MAX_PAGES }),
61
+ enumeratePublicEntities(categoryPage, { maxPages: MAX_PAGES }),
62
+ ])
63
+ if (!products.ok) throw new SitemapEnumerationError(products.failure)
64
+ if (!categories.ok) throw new SitemapEnumerationError(categories.failure)
65
+
66
+ return buildSitemapEntries({
67
+ routes,
68
+ siteUrl,
69
+ entities: [...products.entities, ...categories.entities],
70
+ })
12
71
  }
@@ -29,12 +29,15 @@ import {
29
29
  loadCheckoutCountries,
30
30
  loadCustomer,
31
31
  loadOrders,
32
+ loadOrdersPage,
32
33
  loadPaymentMethods,
34
+ loadProductDetailById,
33
35
  loadShippingMethods,
34
36
  readCart,
35
37
  reconcileOrder,
36
38
  registerShopper,
37
39
  removeLine,
40
+ resolveProductCartTarget,
38
41
  saveShippingAddress,
39
42
  selectShipping,
40
43
  setLineQuantity,
@@ -91,6 +94,11 @@ type OrdersActionResult =
91
94
  | { ok: true; orders: AccountOrderView[] }
92
95
  | { ok: false; error: string }
93
96
 
97
+ /** Structured result for a paginated customer order-history load. */
98
+ type OrdersPageActionResult =
99
+ | { ok: true; orders: AccountOrderView[]; count: number; limit: number; offset: number }
100
+ | { ok: false; error: string }
101
+
94
102
  /** Basic email shape check; a boundary guard, not a full RFC validation. */
95
103
  const EMAIL_PATTERN = /\S+@\S+\.\S+/
96
104
 
@@ -152,6 +160,34 @@ export async function addToCartAction(input: {
152
160
  }
153
161
  }
154
162
 
163
+ /**
164
+ * Add a product's FIRST purchasable variant by backend product id — the
165
+ * product-card shortcut. The variant is resolved server-side from the backend
166
+ * detail (card data carries no variant ids); a product with no purchasable
167
+ * variant returns a truthful error and adds nothing. The product detail remains
168
+ * the explicit-selection surface (change storefront-card-add-to-cart).
169
+ */
170
+ export async function addToCartByProductAction(input: {
171
+ productId: string
172
+ quantity?: number
173
+ }): Promise<CartActionResult> {
174
+ try {
175
+ const productId = typeof input?.productId === 'string' ? input.productId : ''
176
+ if (productId === '') return { ok: false, error: 'Producto no válido.' }
177
+ const quantity =
178
+ Number.isFinite(input?.quantity) && (input.quantity as number) > 0
179
+ ? Math.floor(input.quantity as number)
180
+ : 1
181
+ const detail = await loadProductDetailById(productId)
182
+ const target = resolveProductCartTarget(detail)
183
+ if (!target.ok) return { ok: false, error: target.error }
184
+ const { cartId, context } = await ensureActiveCartId()
185
+ return { ok: true, cart: await addVariant(cartId, target.variantId, quantity, context) }
186
+ } catch (error) {
187
+ return { ok: false, error: toError(error, 'No se pudo agregar el producto.') }
188
+ }
189
+ }
190
+
155
191
  /** Set a backend cart line to an ABSOLUTE quantity by its line-item id. */
156
192
  export async function setLineQuantityAction(input: {
157
193
  lineId: string
@@ -540,3 +576,37 @@ export async function loadOrdersAction(): Promise<OrdersActionResult> {
540
576
  return { ok: false, error: toError(error, 'No se pudieron cargar tus pedidos.') }
541
577
  }
542
578
  }
579
+
580
+ /**
581
+ * Load ONE bounded page of the authenticated customer's channel-scoped order
582
+ * history through the additive paginated contract, returning the backend's real
583
+ * `count`. A missing token resolves to an empty page with `count: 0` without
584
+ * contacting the backend. This action touches ONLY the auth cookie — never the
585
+ * guest session cookie or the cart — and no token is logged or returned.
586
+ */
587
+ export async function loadOrdersPageAction(input?: {
588
+ page?: number
589
+ pageSize?: number
590
+ }): Promise<OrdersPageActionResult> {
591
+ const context = getStoreContext()
592
+ const token = await readAuthToken(context)
593
+ const page = Number.isInteger(input?.page) && (input?.page as number) > 0 ? (input?.page as number) : 1
594
+ const pageSize =
595
+ Number.isInteger(input?.pageSize) && (input?.pageSize as number) > 0
596
+ ? (input?.pageSize as number)
597
+ : 10
598
+ if (!token) return { ok: true, orders: [], count: 0, limit: pageSize, offset: (page - 1) * pageSize }
599
+ try {
600
+ const result = await loadOrdersPage(token, { page, pageSize }, context)
601
+ return {
602
+ ok: true,
603
+ orders: result.orders,
604
+ count: result.count,
605
+ limit: result.limit,
606
+ offset: result.offset,
607
+ }
608
+ } catch (error) {
609
+ logServerError('loadOrdersPageAction', error)
610
+ return { ok: false, error: toError(error, 'No se pudieron cargar tus pedidos.') }
611
+ }
612
+ }
@@ -7,11 +7,13 @@ import { defineCatalog } from '@json-render/core'
7
7
  import { schema } from '@json-render/react/schema'
8
8
  import { z } from 'zod'
9
9
  import {
10
+ backToTopPropsSchema,
10
11
  bannerPropsSchema,
11
- cartDrawerPropsSchema,
12
12
  footerPropsSchema,
13
+ mobileBottomNavPropsSchema,
13
14
  navbarPropsSchema,
14
15
  productCardPropsSchema,
16
+ skipToContentPropsSchema,
15
17
  storefrontNavbarPropsSchema,
16
18
  } from './schema'
17
19
  import { views } from './views'
@@ -43,10 +45,6 @@ const baseComponents = {
43
45
  props: storefrontNavbarPropsSchema,
44
46
  description: 'Store navigation bar with category menu and cart.',
45
47
  },
46
- CartDrawer: {
47
- props: cartDrawerPropsSchema,
48
- description: 'Slide-over shopping cart.',
49
- },
50
48
  Banner: {
51
49
  props: bannerPropsSchema,
52
50
  description: 'Announcement banner above the header.',
@@ -59,6 +57,18 @@ const baseComponents = {
59
57
  props: footerPropsSchema,
60
58
  description: 'Site footer.',
61
59
  },
60
+ SkipToContent: {
61
+ props: skipToContentPropsSchema,
62
+ description: 'Keyboard bypass link to the main content landmark.',
63
+ },
64
+ BackToTop: {
65
+ props: backToTopPropsSchema,
66
+ description: 'Scroll-to-top control revealed past a threshold.',
67
+ },
68
+ MobileBottomNav: {
69
+ props: mobileBottomNavPropsSchema,
70
+ description: 'Mobile-only bottom navigation for primary destinations.',
71
+ },
62
72
  }
63
73
 
64
74
  const viewComponents = Object.assign({}, ...views.map((view) => view.catalog))
@@ -39,14 +39,17 @@ import {
39
39
  getCheckoutCountries,
40
40
  getCurrentCustomer,
41
41
  getCustomerOrders,
42
+ getCustomerOrdersPage,
42
43
  getEligibleShippingMethods,
43
44
  getPaymentMethodsByRegion,
45
+ getProductDetailById,
44
46
  getProductDetailByHandle,
45
47
  loginCustomer,
46
48
  logoutCustomer,
47
49
  reconcileCart,
48
50
  registerCustomer,
49
51
  removeCartLines,
52
+ selectFirstPurchasableVariant,
50
53
  selectShippingMethod,
51
54
  toAbsoluteLineUpdate,
52
55
  updateCartLines,
@@ -263,6 +266,38 @@ export async function loadProductDetail(handle: string): Promise<CatalogDetailRe
263
266
  return { detail, source }
264
267
  }
265
268
 
269
+ /** Directed product-detail lookup by backend id, priced in the store context region. */
270
+ export async function loadProductDetailById(
271
+ productId: string,
272
+ ): Promise<CatalogProductDetail | undefined> {
273
+ const context = getStoreContext()
274
+ assertBackendConfigured()
275
+ return getProductDetailById(productId, context)
276
+ }
277
+
278
+ /** The resolved add-to-cart target for a product-card shortcut. */
279
+ export type ProductCartTarget =
280
+ | { ok: true; variantId: string }
281
+ | { ok: false; error: string }
282
+
283
+ /**
284
+ * Resolve the product-card add-to-cart target: the product's FIRST purchasable
285
+ * variant (available for sale AND with a resolvable price), identified by its
286
+ * backend variant id. This is the explicit card exception to the product-detail
287
+ * rule that never falls back to a first variant (change
288
+ * storefront-card-add-to-cart). A product with no purchasable variant — or an
289
+ * unknown product — returns a truthful error and adds nothing.
290
+ */
291
+ export function resolveProductCartTarget(
292
+ detail: CatalogProductDetail | undefined,
293
+ ): ProductCartTarget {
294
+ const variant = detail ? selectFirstPurchasableVariant(detail) : undefined
295
+ if (!variant) {
296
+ return { ok: false, error: 'Este producto no tiene variantes disponibles.' }
297
+ }
298
+ return { ok: true, variantId: variant.id }
299
+ }
300
+
266
301
  /** The context-scoped backend cart projected for the UI, or undefined when absent. */
267
302
  export async function readCart(
268
303
  cartId: string,
@@ -605,4 +640,40 @@ export async function loadOrders(
605
640
  return orders.map(toAccountOrderView)
606
641
  }
607
642
 
643
+ /**
644
+ * The additive paginated history window projected for the account view.
645
+ * `count` is the backend's real total for the authenticated customer on the
646
+ * store's channel; it is never the page length.
647
+ */
648
+ export interface AccountOrdersPage {
649
+ orders: AccountOrderView[]
650
+ count: number
651
+ limit: number
652
+ offset: number
653
+ }
654
+
655
+ /**
656
+ * Load one bounded page of the authenticated customer's channel-scoped history
657
+ * through the additive paginated contract. A missing token yields an empty page
658
+ * with `count: 0` (the runtime builds no provider); it never fabricates an order.
659
+ */
660
+ export async function loadOrdersPage(
661
+ token: string | null,
662
+ options: { page?: number; pageSize?: number } = {},
663
+ context: StoreContext = getStoreContext(),
664
+ ): Promise<AccountOrdersPage> {
665
+ const page = await getCustomerOrdersPage({
666
+ token,
667
+ page: options.page,
668
+ pageSize: options.pageSize,
669
+ salesChannelId: context.salesChannelId,
670
+ })
671
+ return {
672
+ orders: page.orders.map(toAccountOrderView),
673
+ count: page.count,
674
+ limit: page.limit,
675
+ offset: page.offset,
676
+ }
677
+ }
678
+
608
679
  export type { Money }
@@ -0,0 +1,26 @@
1
+ // Generated by Thor Commerce storefront (composition composer: features).
2
+ // Do not edit by hand — the manifest is the source of truth; run `storefront sync`.
3
+ // Installed manifest catalog features, in stable order, with their persisted
4
+ // config. This is the generation seam the catalog URL boundary consumes to
5
+ // reject parameters owned by a feature that is not installed, and the seam the
6
+ // single controlled CategoryFilter2 reads to select its control
7
+ // visibility/config from the installed manifest only.
8
+
9
+ import type { CatalogFeatureName } from '@thorprovider/runtime/contracts'
10
+
11
+ export const mountedFeatures: readonly CatalogFeatureName[] = []
12
+
13
+ // Every installed feature name, unfiltered by the catalog URL boundary. The
14
+ // general presence seam cross-cutting features read to gate their own
15
+ // contributions (e.g. the navigability feature's shell chrome).
16
+ export const mountedFeatureNames: readonly string[] = []
17
+
18
+ export const mountedFeatureConfigs: Partial<Record<string, Record<string, unknown>>> = {
19
+ }
20
+
21
+ // Typed route/action availability derived from the installed manifest. The
22
+ // generated host uses these to hide links to routes that are not installed,
23
+ // so a removed view never leaves an active dead-end navigation target.
24
+ export const cartViewAvailable = false
25
+
26
+ export const checkoutAvailable = false
@@ -15,6 +15,7 @@ import { useActions, useStateStore } from '@json-render/react'
15
15
  import { useNextApp } from '@json-render/next'
16
16
  import {
17
17
  addToCartAction,
18
+ addToCartByProductAction,
18
19
  applyQuantityDeltaAction,
19
20
  ensureCartAction,
20
21
  loadCartStateAction,
@@ -22,6 +23,8 @@ import {
22
23
  setLineQuantityAction,
23
24
  } from './actions'
24
25
  import type { CartStatePatch } from './commerce'
26
+ import { cartViewAvailable } from './composition'
27
+ import { recordConfirmedTransition } from './section-observation'
25
28
 
26
29
  /** A product-like object the views leave in `/lastClicked` (card/hero shortcut). */
27
30
  interface ClickedProduct {
@@ -81,10 +84,30 @@ export function useCoreHandlers() {
81
84
  if (result.ok) {
82
85
  applyCart(result.cart)
83
86
  set('/cartOpen', true)
87
+ // A confirmed cart transition — observed only after the mutation
88
+ // succeeded, and only when a section origin exists (default: off).
89
+ recordConfirmedTransition('cart_add_confirmed')
84
90
  } else fail(result.error)
85
91
  })
86
92
  .catch(onError('addToCart'))
87
93
  },
94
+ addClickedToCart: () => {
95
+ // Product-card shortcut: add the clicked product's first purchasable
96
+ // variant. The backend variant id is resolved server-side from the
97
+ // product id (the card carries no variant ids).
98
+ const product = get('/lastClicked') as ClickedProduct | null
99
+ if (!product?.id) return
100
+ begin()
101
+ void addToCartByProductAction({ productId: String(product.id) })
102
+ .then((result) => {
103
+ if (result.ok) {
104
+ applyCart(result.cart)
105
+ set('/cartOpen', true)
106
+ recordConfirmedTransition('cart_add_confirmed')
107
+ } else fail(result.error)
108
+ })
109
+ .catch(onError('addClickedToCart'))
110
+ },
88
111
  ensureCart: () => {
89
112
  begin()
90
113
  void ensureCartAction()
@@ -141,17 +164,26 @@ export function useCoreHandlers() {
141
164
  openProduct: () => {
142
165
  const product = get('/lastClicked') as ClickedProduct | null
143
166
  if (!product?.id) return
144
- navigate(`/productos/${product.id}`)
167
+ navigate(`/products/${product.id}`)
145
168
  },
146
169
  openCategory: () => {
147
- const handle = get('/lastCategory') as string | null
148
- navigate(handle ? `/categorias/${handle}` : '/categorias')
170
+ // `/lastCategory` already holds the category href the card was given
171
+ // (`StoreCategory.href`, e.g. `/categories/jackets`); navigate it as-is
172
+ // so the route scope is not prefixed twice.
173
+ const href = get('/lastCategory') as string | null
174
+ navigate(href ?? '/categories')
149
175
  },
150
176
  goHome: () => {
151
177
  set('/cartOpen', false)
152
178
  navigate('/')
153
179
  },
154
- goToCart: () => set('/cartOpen', true),
180
+ goToCart: () => {
181
+ // Visibility only: dismiss the lateral and go to the review route. The
182
+ // navigation is guarded by the installed composition so a store without
183
+ // the cart view never routes to a missing page.
184
+ set('/cartOpen', false)
185
+ if (cartViewAvailable) navigate('/cart')
186
+ },
155
187
  submitReview: () => {},
156
188
  toggleReviewLike: () => {},
157
189
  }