@thorprovider/storefront 0.3.1 → 0.4.0-mvp.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +321 -152
- package/VENDOR.md +69 -0
- package/assets/openspec/config.yaml +38 -0
- package/assets/templates/AGENTS.md +76 -0
- package/assets/templates/app/app/[[...slug]]/page.tsx +54 -2
- package/assets/templates/app/app/[[...slug]]/renderer.tsx +13 -2
- package/assets/templates/app/app/globals.css +5 -8
- package/assets/templates/app/app/layout.tsx +17 -7
- package/assets/templates/app/app/robots.ts +19 -0
- package/assets/templates/app/app/sitemap.ts +71 -0
- package/assets/templates/app/lib/__STOREFRONT__/actions.ts +612 -0
- package/assets/templates/app/lib/__STOREFRONT__/cart-state.ts +66 -0
- package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +50 -57
- package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +679 -0
- package/assets/templates/app/lib/__STOREFRONT__/composition.ts +26 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +197 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.ts +5 -36
- package/assets/templates/app/lib/__STOREFRONT__/icon-map.ts +27 -0
- package/assets/templates/app/lib/__STOREFRONT__/mappers.ts +13 -11
- package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +264 -51
- package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +6 -3
- package/assets/templates/app/lib/__STOREFRONT__/schema.ts +136 -0
- package/assets/templates/app/lib/__STOREFRONT__/section-observation.ts +182 -0
- package/assets/templates/app/lib/__STOREFRONT__/section-sources.ts +130 -0
- package/assets/templates/app/lib/__STOREFRONT__/session.core.ts +470 -0
- package/assets/templates/app/lib/__STOREFRONT__/session.ts +169 -0
- package/assets/templates/app/lib/__STOREFRONT__/shell/content.ts +120 -0
- package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +38 -0
- package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +129 -10
- package/assets/templates/app/lib/__STOREFRONT__/spec/page-root.ts +5 -1
- package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +191 -0
- package/assets/templates/app/lib/__STOREFRONT__/state.core.ts +257 -0
- package/assets/templates/app/lib/__STOREFRONT__/state.ts +7 -31
- package/assets/templates/app/lib/__STOREFRONT__/store-context.ts +161 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.client.tsx +25 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.tsx +16 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.client.tsx +24 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.tsx +5 -18
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/index.ts +18 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/logoCloud.tsx +26 -14
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/pricing.tsx +36 -18
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.client.tsx +100 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.tsx +19 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.cta.ts +19 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.faq.ts +18 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.products.ts +39 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/statistics.tsx +35 -14
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/testimonials.tsx +31 -13
- package/assets/templates/app/lib/__STOREFRONT__/views/types.ts +5 -3
- package/assets/templates/features/checkout/handlers.ts +159 -0
- package/assets/templates/features/checkout/state.ts +61 -0
- package/assets/templates/features/customer-auth/handlers.ts +59 -0
- package/assets/templates/features/customer-auth/state.ts +45 -0
- package/assets/templates/features/recommendations/state.ts +11 -0
- package/assets/templates/views/account/content.ts +35 -0
- package/assets/templates/views/account/index.tsx +114 -0
- package/assets/templates/views/account/registry.tsx +439 -0
- package/assets/templates/views/account/schema.ts +53 -0
- package/assets/templates/views/cart/content.ts +11 -0
- package/assets/templates/views/cart/data.ts +23 -0
- package/assets/templates/views/cart/index.tsx +53 -0
- package/assets/templates/views/cart/props.ts +12 -0
- package/assets/templates/views/cart/registry.tsx +81 -0
- package/assets/templates/views/cart/schema.ts +52 -0
- package/assets/templates/views/category/assembly/assembly.json +29 -0
- package/assets/templates/views/category/assembly/index.skeleton.tsx +251 -0
- package/assets/templates/views/category/assembly/registry.skeleton.tsx +34 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/binding.json +5 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/catalog.part +4 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/element.part +7 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/registry.part +18 -0
- package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/catalog.part +5 -0
- package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/element.part +34 -0
- package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/registry.part +71 -0
- package/assets/templates/views/category/content.ts +30 -0
- package/assets/templates/views/category/data.ts +12 -0
- package/assets/templates/views/category/index.tsx +299 -0
- package/assets/templates/views/category/props.ts +23 -0
- package/assets/templates/views/category/registry.tsx +122 -0
- package/assets/templates/views/category/schema.ts +91 -0
- package/assets/templates/views/checkout/content.ts +105 -0
- package/assets/templates/views/checkout/data.ts +23 -0
- package/assets/templates/views/checkout/index.tsx +111 -0
- package/assets/templates/views/checkout/props.ts +49 -0
- package/assets/templates/views/checkout/registry.tsx +217 -0
- package/assets/templates/views/checkout/schema.ts +229 -0
- package/assets/templates/views/home/assembly/assembly.json +33 -0
- package/assets/templates/views/home/assembly/index.skeleton.tsx +90 -0
- package/assets/templates/views/home/assembly/registry.skeleton.tsx +25 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/element.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/registry.part +11 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/element.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/registry.part +10 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/element.part +5 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/registry.part +16 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/element.part +5 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/registry.part +26 -0
- package/assets/templates/views/home/content.ts +70 -0
- package/assets/templates/views/home/data.ts +24 -0
- package/assets/templates/views/home/index.tsx +57 -85
- package/assets/templates/views/home/props.ts +22 -0
- package/assets/templates/views/home/registry.tsx +38 -22
- package/assets/templates/views/home/schema.ts +93 -0
- package/assets/templates/views/product/content.ts +25 -0
- package/assets/templates/views/product/data.ts +50 -0
- package/assets/templates/views/product/index.tsx +148 -0
- package/assets/templates/views/product/props.ts +27 -0
- package/assets/templates/views/product/registry.tsx +103 -0
- package/assets/templates/views/product/schema.ts +121 -0
- package/assets/templates/views/static/content.ts +99 -0
- package/assets/templates/views/static/index.tsx +60 -0
- package/assets/templates/views/static/registry.tsx +66 -0
- package/assets/templates/views/static/schema.ts +45 -0
- package/bin/storefront.js +2424 -148
- package/examples/merchandising/category-led.json +37 -0
- package/examples/merchandising/collection-led.json +37 -0
- package/package.json +15 -9
- package/recipes/archetypes.json +28 -14
- package/recipes/catalog.json +400 -0
- package/recipes/catalog.mjs +128 -0
- package/recipes/catalog.schema.json +64 -0
- package/recipes/feature.schema.json +89 -0
- package/recipes/features.json +162 -0
- package/recipes/manifest.schema.json +69 -0
- package/recipes/operations.schema.json +43 -0
- package/recipes/recipe.schema.json +11 -2
- package/recipes/recipes.json +76 -19
- package/recipes/sections.json +34 -5
- package/recipes/validate.mjs +838 -32
- package/scripts/candidate.mjs +236 -0
- package/scripts/composers.mjs +160 -0
- package/scripts/env-file.mjs +86 -0
- package/scripts/file-classes.mjs +121 -0
- package/scripts/logical-controls.mjs +61 -0
- package/scripts/manifest.mjs +298 -0
- package/scripts/migrate-english-routes.mjs +276 -0
- package/scripts/operation-registry.mjs +256 -0
- package/scripts/operations.mjs +720 -0
- package/scripts/ownership-conflict.mjs +356 -0
- package/scripts/pinning.mjs +573 -0
- package/scripts/plan-apply.mjs +987 -0
- package/scripts/preflight.mjs +51 -0
- package/scripts/reconcile.mjs +549 -0
- package/scripts/render-templates.mjs +338 -68
- package/scripts/schema-validate.mjs +132 -0
- package/scripts/section-compat.mjs +300 -0
- package/scripts/section-config.mjs +253 -0
- package/scripts/section-operations.mjs +508 -0
- package/scripts/starter.mjs +17 -0
- package/scripts/sync-assets.mjs +22 -15
- package/scripts/theme/color.mjs +185 -0
- package/scripts/theme/css-theme.mjs +233 -0
- package/scripts/theme/fonts.mjs +123 -0
- package/scripts/theme/import-export.mjs +125 -0
- package/scripts/theme/preset.mjs +93 -0
- package/scripts/theme/theme-plan.mjs +292 -0
- package/scripts/theme/tokens.mjs +137 -0
- package/scripts/view-assembly.mjs +337 -0
- package/scripts/view-selection.mjs +211 -0
- package/skills/sf-feature/SKILL.md +80 -0
- package/skills/sf-setup/SKILL.md +79 -0
- package/skills/sf-theme-gen/SKILL.md +100 -30
- package/skills/sf-theme-gen/references/shadcn-theming.md +87 -0
- package/skills/{shadcn-theming/references/templates → sf-theme-gen/references}/theme-setup.md +15 -19
- package/skills/{shadcn-theming → sf-theme-gen}/references/theming-guide.md +6 -11
- package/skills/sf-view-gen/SKILL.md +146 -93
- package/skills/sf-view-gen/assets/recipes/archetypes.json +28 -14
- package/skills/sf-view-gen/assets/recipes/catalog.json +400 -0
- package/skills/sf-view-gen/assets/recipes/catalog.schema.json +64 -0
- package/skills/sf-view-gen/assets/recipes/features.json +162 -0
- package/skills/sf-view-gen/assets/recipes/recipes.json +76 -19
- package/skills/sf-view-gen/assets/recipes/sections.json +34 -5
- package/skills/sf-view-gen/assets/templates/views/sections/cta.client.tsx +25 -0
- package/skills/sf-view-gen/assets/templates/views/sections/cta.tsx +16 -0
- package/skills/sf-view-gen/assets/templates/views/sections/faq.client.tsx +13 -9
- package/skills/sf-view-gen/assets/templates/views/sections/faq.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.client.tsx +12 -8
- package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/pricing.client.tsx +14 -10
- package/skills/sf-view-gen/assets/templates/views/sections/pricing.tsx +2 -9
- package/skills/sf-view-gen/assets/templates/views/sections/products.client.tsx +100 -0
- package/skills/sf-view-gen/assets/templates/views/sections/products.tsx +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.cta.ts +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.faq.ts +18 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.logoCloud.ts +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.pricing.ts +26 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.products.ts +39 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.statistics.ts +25 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.testimonials.ts +23 -0
- package/skills/sf-view-gen/assets/templates/views/sections/statistics.client.tsx +16 -8
- package/skills/sf-view-gen/assets/templates/views/sections/statistics.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/testimonials.client.tsx +13 -9
- package/skills/sf-view-gen/assets/templates/views/sections/testimonials.tsx +2 -5
- package/skills/{json-render-core/SKILL.md → sf-view-gen/references/json-render-core.md} +4 -27
- package/skills/{json-render-next/SKILL.md → sf-view-gen/references/json-render-next.md} +5 -8
- package/skills/{json-render-react/SKILL.md → sf-view-gen/references/json-render-react.md} +3 -5
- package/skills/{json-render-shadcn/SKILL.md → sf-view-gen/references/json-render-shadcn.md} +3 -5
- package/skills/sf-view-gen/references/views.md +58 -33
- package/skills/sf-view-gen/scripts/catalog.mjs +128 -0
- package/skills/sf-view-gen/scripts/schema-validate.mjs +132 -0
- package/skills/sf-view-gen/scripts/validate.mjs +838 -32
- package/skills/storefront-workflow/SKILL.md +179 -0
- package/skills/{storefront-best-practices/reference/design.md → storefront-workflow/references/design-consistency.md} +13 -29
- package/CHANGELOG.md +0 -61
- package/commands/sf-add-view.md +0 -26
- package/commands/sf-theme.md +0 -20
- package/commands/sf-view.md +0 -25
- package/skills/building-storefronts/SKILL.md +0 -178
- package/skills/building-storefronts/references/frontend-integration.md +0 -229
- package/skills/json-render-remotion/SKILL.md +0 -111
- package/skills/json-render-solid/SKILL.md +0 -204
- package/skills/nextjs-shadcn/SKILL.md +0 -367
- package/skills/nextjs-shadcn/references/architecture.md +0 -499
- package/skills/nextjs-shadcn/references/project-setup.md +0 -162
- package/skills/nextjs-shadcn/references/shadcn-platform.md +0 -261
- package/skills/nextjs-shadcn/references/sidebar.md +0 -274
- package/skills/nextjs-shadcn/references/styling.md +0 -555
- package/skills/sf-view-gen/assets/templates/views/category/index.tsx +0 -86
- package/skills/sf-view-gen/assets/templates/views/category/registry.tsx +0 -48
- package/skills/sf-view-gen/assets/templates/views/checkout/index.tsx +0 -192
- package/skills/sf-view-gen/assets/templates/views/checkout/registry.tsx +0 -45
- package/skills/sf-view-gen/assets/templates/views/product/index.tsx +0 -122
- package/skills/sf-view-gen/assets/templates/views/product/registry.tsx +0 -49
- package/skills/sf-view-gen/assets/templates/views/user-panel/index.tsx +0 -168
- package/skills/sf-view-gen/assets/templates/views/user-panel/registry.tsx +0 -49
- package/skills/shadcn-component-discovery/SKILL.md +0 -273
- package/skills/shadcn-component-discovery/references/registries.md +0 -226
- package/skills/shadcn-theming/SKILL.md +0 -104
- package/skills/storefront-best-practices/SKILL.md +0 -421
- package/skills/storefront-best-practices/reference/components/breadcrumbs.md +0 -123
- package/skills/storefront-best-practices/reference/components/cart-popup.md +0 -189
- package/skills/storefront-best-practices/reference/components/country-selector.md +0 -298
- package/skills/storefront-best-practices/reference/components/footer.md +0 -112
- package/skills/storefront-best-practices/reference/components/hero.md +0 -241
- package/skills/storefront-best-practices/reference/components/megamenu.md +0 -239
- package/skills/storefront-best-practices/reference/components/navbar.md +0 -397
- package/skills/storefront-best-practices/reference/components/popups.md +0 -221
- package/skills/storefront-best-practices/reference/components/product-card.md +0 -125
- package/skills/storefront-best-practices/reference/components/product-reviews.md +0 -217
- package/skills/storefront-best-practices/reference/components/product-slider.md +0 -174
- package/skills/storefront-best-practices/reference/components/search.md +0 -101
- package/skills/storefront-best-practices/reference/connecting-to-backend.md +0 -391
- package/skills/storefront-best-practices/reference/features/promotions.md +0 -307
- package/skills/storefront-best-practices/reference/features/wishlist.md +0 -230
- package/skills/storefront-best-practices/reference/layouts/account.md +0 -380
- package/skills/storefront-best-practices/reference/layouts/cart.md +0 -316
- package/skills/storefront-best-practices/reference/layouts/checkout.md +0 -486
- package/skills/storefront-best-practices/reference/layouts/home-page.md +0 -264
- package/skills/storefront-best-practices/reference/layouts/order-confirmation.md +0 -231
- package/skills/storefront-best-practices/reference/layouts/product-details.md +0 -527
- package/skills/storefront-best-practices/reference/layouts/product-listing.md +0 -520
- package/skills/storefront-best-practices/reference/layouts/static-pages.md +0 -356
- package/skills/storefront-best-practices/reference/medusa.md +0 -307
- /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/mobile-responsiveness.md +0 -0
- /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/seo.md +0 -0
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
// Wires the json-render app to the Medusa backend via @thorprovider/adapters
|
|
3
3
|
// (through @thorprovider/runtime/commerce). Loaders come from each view module.
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type { StorefrontLoaderFn } from '@thorprovider/runtime/render-core/app'
|
|
6
6
|
import { createStorefrontApp } from '@thorprovider/runtime/render-core/app'
|
|
7
7
|
import { __STOREFRONT_CAMEL__Spec } from './spec'
|
|
8
|
+
import { entitySeoExtractors } from './spec/seo'
|
|
9
|
+
import { getSiteUrl } from './store-context'
|
|
8
10
|
import { views } from './views'
|
|
9
11
|
|
|
10
|
-
const loaders: Record<string,
|
|
12
|
+
const loaders: Record<string, StorefrontLoaderFn> = Object.assign({}, ...views.map((view) => view.loader))
|
|
11
13
|
|
|
12
14
|
export const __STOREFRONT_CAMEL__App = createStorefrontApp({
|
|
13
15
|
spec: __STOREFRONT_CAMEL__Spec,
|
|
14
16
|
loaders,
|
|
17
|
+
entitySeo: { siteUrl: getSiteUrl(), extract: entitySeoExtractors },
|
|
15
18
|
})
|
|
16
19
|
|
|
17
|
-
export type {
|
|
20
|
+
export type { StorefrontLoaderFn }
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Generated by Thor Commerce storefront (template: assets/templates/app/lib/__STOREFRONT__/schema.ts)
|
|
2
|
+
// Zod contracts for the shell-level base components. Single source shared by
|
|
3
|
+
// the server-side catalog (catalog.ts) and the client-side registry
|
|
4
|
+
// (registry.tsx) — json-render does not validate props against the catalog at
|
|
5
|
+
// render time, so the registry parses here.
|
|
6
|
+
|
|
7
|
+
import { z } from 'zod'
|
|
8
|
+
|
|
9
|
+
export const ctaSchema = z.object({
|
|
10
|
+
label: z.string(),
|
|
11
|
+
href: z.string().optional(),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export const navLinkSchema = z.object({
|
|
15
|
+
label: z.string(),
|
|
16
|
+
href: z.string().optional(),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const footerLinkSchema = z.object({
|
|
20
|
+
label: z.string(),
|
|
21
|
+
href: z.string().optional(),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const footerLinkColumnSchema = z.object({
|
|
25
|
+
title: z.string(),
|
|
26
|
+
links: z.array(footerLinkSchema),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// Declared locally so the scaffold's zod version is the only one in play.
|
|
30
|
+
// Cross-major schema assignment (@thorprovider/blocks ships zod v3) breaks the
|
|
31
|
+
// defineCatalog contract, so the block contract is mirrored here instead.
|
|
32
|
+
export const shellProductSchema = z.object({
|
|
33
|
+
id: z.union([z.string(), z.number()]),
|
|
34
|
+
title: z.string(),
|
|
35
|
+
price: z.number(),
|
|
36
|
+
originalPrice: z.number().optional(),
|
|
37
|
+
// Required: the ProductCard1 block contract demands `image: string`, and the
|
|
38
|
+
// runtime's card projection always fills it (PRODUCT_IMAGE_FALLBACK).
|
|
39
|
+
image: z.string(),
|
|
40
|
+
badge: z.string().optional(),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export const navbarPropsSchema = z.object({
|
|
44
|
+
navLinks: z.array(navLinkSchema),
|
|
45
|
+
logoText: z.string(),
|
|
46
|
+
logoHref: z.string().optional(),
|
|
47
|
+
currentPath: z.string().optional(),
|
|
48
|
+
signIn: ctaSchema.optional(),
|
|
49
|
+
cta: ctaSchema.optional(),
|
|
50
|
+
cartCount: z.number().optional(),
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// Mirrors the StorefrontNavbar block contract in this scaffold. `storeCategories`
|
|
54
|
+
// is bound to /navCategories and `items` is derived from /cart in the registry,
|
|
55
|
+
// so only the static chrome is declared here.
|
|
56
|
+
export const navCategorySchema = z.object({
|
|
57
|
+
label: z.string(),
|
|
58
|
+
href: z.string(),
|
|
59
|
+
children: z.array(z.object({ label: z.string(), href: z.string() })).optional(),
|
|
60
|
+
emphasis: z.boolean().optional(),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// Optional brand seam mirroring the Navbar4 / FooterSection1 accent props and
|
|
64
|
+
// the StorefrontHero1 editorial image. Declared locally (see shellProductSchema)
|
|
65
|
+
// so the scaffold's zod version is the only one in play.
|
|
66
|
+
export const shellAccentSchema = z.object({
|
|
67
|
+
text: z.string(),
|
|
68
|
+
className: z.string().optional(),
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
export const storefrontNavbarPropsSchema = z.object({
|
|
72
|
+
storeCategories: z.array(navCategorySchema),
|
|
73
|
+
shippingNote: z.string().optional(),
|
|
74
|
+
utilityLinks: z.array(z.object({ label: z.string(), href: z.string() })),
|
|
75
|
+
logoText: z.string(),
|
|
76
|
+
logoHref: z.string().optional(),
|
|
77
|
+
logoAccent: shellAccentSchema.optional(),
|
|
78
|
+
className: z.string().optional(),
|
|
79
|
+
accountHref: z.string().optional(),
|
|
80
|
+
// Held global search (E2 task 5.4): the generated shell opts out of the
|
|
81
|
+
// navbar search control while no verified global search contract exists.
|
|
82
|
+
showSearch: z.boolean().optional(),
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
export const bannerPropsSchema = z.object({
|
|
86
|
+
badge: z.string().optional(),
|
|
87
|
+
message: z.string(),
|
|
88
|
+
href: z.string(),
|
|
89
|
+
linkLabel: z.string(),
|
|
90
|
+
avatars: z.array(z.string()).optional(),
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
export const productCardPropsSchema = z.object({
|
|
94
|
+
products: z.array(shellProductSchema),
|
|
95
|
+
heading: z.string().optional(),
|
|
96
|
+
maxItems: z.number().optional(),
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
export const footerPropsSchema = z.object({
|
|
100
|
+
brandName: z.string(),
|
|
101
|
+
brandAccent: shellAccentSchema.optional(),
|
|
102
|
+
brandDescription: z.string().optional(),
|
|
103
|
+
linkColumns: z.array(footerLinkColumnSchema),
|
|
104
|
+
className: z.string().optional(),
|
|
105
|
+
newsletter: z
|
|
106
|
+
.object({ placeholder: z.string().optional(), buttonLabel: z.string().optional() })
|
|
107
|
+
.optional(),
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// Navigability chrome contracts, mirrored from @thorprovider/blocks so the
|
|
111
|
+
// shell catalog/registry share the scaffold's zod version.
|
|
112
|
+
export const skipToContentPropsSchema = z.object({
|
|
113
|
+
targetId: z.string(),
|
|
114
|
+
label: z.string().optional(),
|
|
115
|
+
className: z.string().optional(),
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
export const backToTopPropsSchema = z.object({
|
|
119
|
+
threshold: z.number().optional(),
|
|
120
|
+
label: z.string().optional(),
|
|
121
|
+
className: z.string().optional(),
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
export const mobileBottomNavItemSchema = z.object({
|
|
125
|
+
id: z.string().optional(),
|
|
126
|
+
label: z.string(),
|
|
127
|
+
href: z.string(),
|
|
128
|
+
icon: z.string().optional(),
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
export const mobileBottomNavPropsSchema = z.object({
|
|
132
|
+
items: z.array(mobileBottomNavItemSchema),
|
|
133
|
+
currentHref: z.string().optional(),
|
|
134
|
+
ariaLabel: z.string().optional(),
|
|
135
|
+
className: z.string().optional(),
|
|
136
|
+
})
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Generated by Thor Commerce storefront (host: section observation)
|
|
4
|
+
//
|
|
5
|
+
// Optional, host-owned merchandising observation. DISABLED BY DEFAULT: nothing is
|
|
6
|
+
// observed, transmitted, or stored until the host registers an adapter through
|
|
7
|
+
// `configureSectionObservation`. The pure controller (validation, impression
|
|
8
|
+
// dedupe, conservative lineage) lives in @thorprovider/runtime/contracts; this
|
|
9
|
+
// module only adds the browser glue (visibility, per-view-visit identity) and the
|
|
10
|
+
// adapter seam. Adapter failures never interrupt navigation, cart, or checkout.
|
|
11
|
+
|
|
12
|
+
import { useEffect, useRef, useState } from 'react'
|
|
13
|
+
import {
|
|
14
|
+
createInMemoryRecorder,
|
|
15
|
+
createObservationController,
|
|
16
|
+
type ConfirmedEvent,
|
|
17
|
+
type ObservationContext,
|
|
18
|
+
type ObservationController,
|
|
19
|
+
type SectionObservationAdapter,
|
|
20
|
+
} from '@thorprovider/runtime/contracts'
|
|
21
|
+
|
|
22
|
+
let controller: ObservationController | null = null
|
|
23
|
+
let viewVisitId: string | null = null
|
|
24
|
+
let viewVisitSeq = 0
|
|
25
|
+
|
|
26
|
+
function nextViewVisitId(): string {
|
|
27
|
+
viewVisitSeq += 1
|
|
28
|
+
return `${Date.now().toString(36)}-${viewVisitSeq}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Stable for one view visit (page load). */
|
|
32
|
+
function getViewVisitId(): string {
|
|
33
|
+
if (!viewVisitId) viewVisitId = nextViewVisitId()
|
|
34
|
+
return viewVisitId
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function reportFailure(scope: string, error: unknown): void {
|
|
38
|
+
console.error(
|
|
39
|
+
`[storefront/observation] ${scope} failed`,
|
|
40
|
+
error instanceof Error ? error.message : error,
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Register (or clear) the host adapter. Null disables observation entirely. */
|
|
45
|
+
export function configureSectionObservation(adapter: SectionObservationAdapter | null): void {
|
|
46
|
+
controller = adapter
|
|
47
|
+
? createObservationController({
|
|
48
|
+
adapter,
|
|
49
|
+
onError: (error) => reportFailure('adapter', error),
|
|
50
|
+
})
|
|
51
|
+
: null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isSectionObservationEnabled(): boolean {
|
|
55
|
+
return controller !== null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getSectionObservationController(): ObservationController | null {
|
|
59
|
+
return controller
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** An in-memory recorder for verification/tests (never a production sink). */
|
|
63
|
+
export function createInMemorySectionRecorder(): ReturnType<typeof createInMemoryRecorder> {
|
|
64
|
+
return createInMemoryRecorder()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function setSectionObservationContext(context?: ObservationContext): void {
|
|
68
|
+
try {
|
|
69
|
+
controller?.setContext(context)
|
|
70
|
+
} catch (error) {
|
|
71
|
+
reportFailure('setContext', error)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Observe a confirmed host transition. Only recorded when a section origin
|
|
77
|
+
* exists for the current session/context; otherwise it stays unattributed.
|
|
78
|
+
*/
|
|
79
|
+
export function recordConfirmedTransition(
|
|
80
|
+
event: ConfirmedEvent,
|
|
81
|
+
input?: { productHandle?: string; destination?: string },
|
|
82
|
+
): void {
|
|
83
|
+
try {
|
|
84
|
+
controller?.noteConfirmed(event, input)
|
|
85
|
+
} catch (error) {
|
|
86
|
+
reportFailure(event, error)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface UseSectionObservationInput {
|
|
91
|
+
sectionId: string
|
|
92
|
+
sectionType: string
|
|
93
|
+
sectionVariant?: string
|
|
94
|
+
objective?: string
|
|
95
|
+
context?: ObservationContext
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Observe a section's exposure and product clicks. When observation is disabled
|
|
100
|
+
* no IntersectionObserver is attached and the returned callback is a no-op.
|
|
101
|
+
*/
|
|
102
|
+
export function useSectionObservation(input: UseSectionObservationInput) {
|
|
103
|
+
const enabled = isSectionObservationEnabled() && input.sectionId.length > 0
|
|
104
|
+
const containerRef = useRef<HTMLDivElement | null>(null)
|
|
105
|
+
const [inView, setInView] = useState(false)
|
|
106
|
+
const { sectionId, sectionType, sectionVariant, objective, context } = input
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!enabled) return
|
|
110
|
+
const element = containerRef.current
|
|
111
|
+
if (!element || typeof IntersectionObserver === 'undefined') return
|
|
112
|
+
const observer = new IntersectionObserver(
|
|
113
|
+
(entries) => {
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
if (entry.intersectionRatio >= 0.5) setInView(true)
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
{ threshold: [0.5] },
|
|
119
|
+
)
|
|
120
|
+
observer.observe(element)
|
|
121
|
+
return () => observer.disconnect()
|
|
122
|
+
}, [enabled])
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (!enabled || !inView) return
|
|
126
|
+
const timer = setTimeout(() => {
|
|
127
|
+
controller?.observeImpression({
|
|
128
|
+
viewVisitId: getViewVisitId(),
|
|
129
|
+
sectionId,
|
|
130
|
+
sectionType,
|
|
131
|
+
...(sectionVariant ? { sectionVariant } : {}),
|
|
132
|
+
...(objective ? { objective } : {}),
|
|
133
|
+
...(context ? { context } : {}),
|
|
134
|
+
})
|
|
135
|
+
}, 1000)
|
|
136
|
+
return () => clearTimeout(timer)
|
|
137
|
+
}, [enabled, inView, sectionId, sectionType, sectionVariant, objective, context])
|
|
138
|
+
|
|
139
|
+
const noteProductClick = (product: { id: string | number; href?: string }): void => {
|
|
140
|
+
if (!enabled) return
|
|
141
|
+
try {
|
|
142
|
+
controller?.noteClick({
|
|
143
|
+
viewVisitId: getViewVisitId(),
|
|
144
|
+
sectionId,
|
|
145
|
+
sectionType,
|
|
146
|
+
...(sectionVariant ? { sectionVariant } : {}),
|
|
147
|
+
...(objective ? { objective } : {}),
|
|
148
|
+
...(context ? { context } : {}),
|
|
149
|
+
productHandle: String(product.id),
|
|
150
|
+
...(product.href ? { destination: product.href } : {}),
|
|
151
|
+
})
|
|
152
|
+
} catch (error) {
|
|
153
|
+
reportFailure('noteClick', error)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { containerRef, noteProductClick, enabled }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Observe a successfully rendered product destination. Recorded only when it
|
|
162
|
+
* matches the current section origin; otherwise it stays unattributed.
|
|
163
|
+
*/
|
|
164
|
+
export function useProductVisitObservation(
|
|
165
|
+
productHandle: string | null | undefined,
|
|
166
|
+
destination?: string,
|
|
167
|
+
): void {
|
|
168
|
+
const enabled = isSectionObservationEnabled()
|
|
169
|
+
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (!enabled || !productHandle) return
|
|
172
|
+
try {
|
|
173
|
+
controller?.noteProductVisit({
|
|
174
|
+
viewVisitId: getViewVisitId(),
|
|
175
|
+
productHandle: String(productHandle),
|
|
176
|
+
...(destination ? { destination } : {}),
|
|
177
|
+
})
|
|
178
|
+
} catch (error) {
|
|
179
|
+
reportFailure('noteProductVisit', error)
|
|
180
|
+
}
|
|
181
|
+
}, [enabled, productHandle, destination])
|
|
182
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Derived host helper: per-instance resolution for data-bound sections.
|
|
2
|
+
//
|
|
3
|
+
// A data-bound section (for example `products`) keeps its *selection* in a
|
|
4
|
+
// user-owned, per-instance config keyed by the section's stable id
|
|
5
|
+
// (`views/<view>/data.ts` -> `sectionSources`). This module is the single host
|
|
6
|
+
// boundary that turns that config into resolved, validated view state:
|
|
7
|
+
//
|
|
8
|
+
// * it validates each source with the canonical `sectionSourceSchema` (an
|
|
9
|
+
// invalid or missing source yields an `unconfigured` state, never a guess);
|
|
10
|
+
// * it resolves each selection through the existing canonical catalog bridge
|
|
11
|
+
// (`getCatalogResults`), so equal queries share request-local work, differing
|
|
12
|
+
// store/channel/region/currency contexts stay isolated, and unsupported
|
|
13
|
+
// criteria are reported rather than dropped;
|
|
14
|
+
// * it maps each canonical product to the shared presentation contract via the
|
|
15
|
+
// host mapper, so an unresolved price stays truthful instead of becoming $0;
|
|
16
|
+
// * it returns one discriminated state per section id, so a failing section
|
|
17
|
+
// never erases its siblings and a real failure is never replaced by mock data.
|
|
18
|
+
//
|
|
19
|
+
// Orphan sources (ids no longer referenced by the composition) are ignored.
|
|
20
|
+
|
|
21
|
+
import { getCatalogResults } from '@thorprovider/runtime/commerce'
|
|
22
|
+
import {
|
|
23
|
+
MERCHANDISING_SECTION_MAX_ITEMS,
|
|
24
|
+
sectionSourceSchema,
|
|
25
|
+
} from '@thorprovider/runtime/contracts'
|
|
26
|
+
import {
|
|
27
|
+
PRODUCT_IMAGE_FALLBACK,
|
|
28
|
+
isCompleteFiniteDecimal,
|
|
29
|
+
type ProductPresentation,
|
|
30
|
+
} from '@thorprovider/blocks'
|
|
31
|
+
import type { Product, StoreContext } from '@thorprovider/types'
|
|
32
|
+
|
|
33
|
+
/** Per-instance section selection config, keyed by the section's stable id. */
|
|
34
|
+
export type SectionSources = Record<string, unknown>
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Canonical `Product` (unified provider output) → `ProductPresentation` for the
|
|
38
|
+
* merchandising product sections. Unlike the card mapper, this NEVER drops a
|
|
39
|
+
* product with an unresolved price: the canonical `Money` is passed through and
|
|
40
|
+
* the block renders a truthful "unavailable" state instead of inventing `$0`.
|
|
41
|
+
* Optional fields (image alt, rating, badge) are omitted — never `""` or a
|
|
42
|
+
* fabricated value — and a missing image falls back to the shared placeholder so
|
|
43
|
+
* no `<img>` is rendered without a `src`.
|
|
44
|
+
*
|
|
45
|
+
* This lives in the derived host helper (not the user-owned mappers module) so an
|
|
46
|
+
* upgraded home whose `mappers.ts` predates this capability still compiles.
|
|
47
|
+
*/
|
|
48
|
+
export function toProductPresentation(product: Product): ProductPresentation {
|
|
49
|
+
const min = product.priceRange?.minVariantPrice
|
|
50
|
+
const compareAt = product.variants.find(
|
|
51
|
+
(variant) => variant.compareAtPrice && isCompleteFiniteDecimal(variant.compareAtPrice.amount),
|
|
52
|
+
)?.compareAtPrice
|
|
53
|
+
const image = product.featuredImage?.url || product.images[0]?.url || PRODUCT_IMAGE_FALLBACK
|
|
54
|
+
const imageAlt = product.featuredImage?.altText || undefined
|
|
55
|
+
return {
|
|
56
|
+
id: product.handle,
|
|
57
|
+
title: product.title,
|
|
58
|
+
image,
|
|
59
|
+
imageAlt,
|
|
60
|
+
href: `/products/${product.handle}`,
|
|
61
|
+
price: min ?? { amount: '', currencyCode: '' },
|
|
62
|
+
compareAtPrice: compareAt,
|
|
63
|
+
rating: undefined,
|
|
64
|
+
badge: undefined,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The resolved state the view state model carries for one data-bound section. */
|
|
69
|
+
export interface SectionViewState {
|
|
70
|
+
status:
|
|
71
|
+
| 'unconfigured'
|
|
72
|
+
| 'ready'
|
|
73
|
+
| 'empty'
|
|
74
|
+
| 'invalid-query'
|
|
75
|
+
| 'unsupported-query'
|
|
76
|
+
| 'unmounted-capability'
|
|
77
|
+
| 'data-error'
|
|
78
|
+
products: ProductPresentation[]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Read the optional `sectionSources` export from a user-owned data module
|
|
83
|
+
* without hard-importing it, so a home scaffolded before this capability (whose
|
|
84
|
+
* `data.ts` has no such export) still compiles and simply renders every
|
|
85
|
+
* data-bound section as `unconfigured`.
|
|
86
|
+
*/
|
|
87
|
+
export function resolveSectionSources(module: unknown): SectionSources {
|
|
88
|
+
const candidate = (module as { sectionSources?: unknown } | null | undefined)?.sectionSources
|
|
89
|
+
return candidate && typeof candidate === 'object' ? (candidate as SectionSources) : {}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolve the state for every referenced data-bound section id. Sections resolve
|
|
94
|
+
* concurrently and independently; each entry is always present so the registry
|
|
95
|
+
* can render a truthful state for its section.
|
|
96
|
+
*/
|
|
97
|
+
export async function resolveSectionViewStates(
|
|
98
|
+
ids: readonly string[],
|
|
99
|
+
sources: SectionSources,
|
|
100
|
+
context: StoreContext,
|
|
101
|
+
): Promise<Record<string, SectionViewState>> {
|
|
102
|
+
const states: Record<string, SectionViewState> = {}
|
|
103
|
+
await Promise.all(
|
|
104
|
+
ids.map(async (id) => {
|
|
105
|
+
const parsed = sectionSourceSchema.safeParse(sources[id])
|
|
106
|
+
if (!parsed.success) {
|
|
107
|
+
states[id] = { status: 'unconfigured', products: [] }
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
const source = parsed.data
|
|
111
|
+
try {
|
|
112
|
+
const outcome = await getCatalogResults(source.query, context)
|
|
113
|
+
if (!outcome.ok) {
|
|
114
|
+
states[id] = { status: outcome.error.code, products: [] }
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
const maxItems = source.maxItems ?? MERCHANDISING_SECTION_MAX_ITEMS
|
|
118
|
+
const products = outcome.results.products
|
|
119
|
+
.slice(0, maxItems)
|
|
120
|
+
.map(toProductPresentation)
|
|
121
|
+
states[id] = { status: products.length > 0 ? 'ready' : 'empty', products }
|
|
122
|
+
} catch {
|
|
123
|
+
// A transport/provider failure is surfaced as a per-section data error,
|
|
124
|
+
// never as fabricated or mock products, and never discards siblings.
|
|
125
|
+
states[id] = { status: 'data-error', products: [] }
|
|
126
|
+
}
|
|
127
|
+
}),
|
|
128
|
+
)
|
|
129
|
+
return states
|
|
130
|
+
}
|