@fayz-ai/storefront 0.6.0 → 0.7.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 (94) hide show
  1. package/dist/auth.d.ts +12 -2
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/blocks.d.ts +3 -3
  4. package/dist/blocks.d.ts.map +1 -1
  5. package/dist/component-contracts.d.ts +124 -0
  6. package/dist/component-contracts.d.ts.map +1 -0
  7. package/dist/component-selectors.d.ts +61 -0
  8. package/dist/component-selectors.d.ts.map +1 -0
  9. package/dist/components/CartDrawer.d.ts.map +1 -1
  10. package/dist/components/ProductCard.d.ts +2 -5
  11. package/dist/components/ProductCard.d.ts.map +1 -1
  12. package/dist/components/ProductEnquiryForm.d.ts +8 -0
  13. package/dist/components/ProductEnquiryForm.d.ts.map +1 -0
  14. package/dist/components/ProductGrid.d.ts.map +1 -1
  15. package/dist/components/RelatedProducts.d.ts +1 -1
  16. package/dist/components/RelatedProducts.d.ts.map +1 -1
  17. package/dist/components/SmoothImage.d.ts +7 -0
  18. package/dist/components/SmoothImage.d.ts.map +1 -0
  19. package/dist/components/StorefrontFooter.d.ts.map +1 -1
  20. package/dist/components/StorefrontHeader.d.ts.map +1 -1
  21. package/dist/components/sections/CategoryShowcase.d.ts.map +1 -1
  22. package/dist/components/sections/HeroSection.d.ts.map +1 -1
  23. package/dist/components/sections/MediaCarousel.d.ts +11 -0
  24. package/dist/components/sections/MediaCarousel.d.ts.map +1 -0
  25. package/dist/components/sections/MiscSections.d.ts.map +1 -1
  26. package/dist/components/sections/ProductRail.d.ts.map +1 -1
  27. package/dist/components/sections/ProductSlider.d.ts +15 -0
  28. package/dist/components/sections/ProductSlider.d.ts.map +1 -0
  29. package/dist/config.d.ts +38 -21
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/createStorefrontApp.d.ts +12 -0
  32. package/dist/createStorefrontApp.d.ts.map +1 -1
  33. package/dist/define.d.ts +17 -0
  34. package/dist/define.d.ts.map +1 -0
  35. package/dist/hooks/index.d.ts +1 -0
  36. package/dist/hooks/index.d.ts.map +1 -1
  37. package/dist/hooks/useStorefront.d.ts +35 -0
  38. package/dist/hooks/useStorefront.d.ts.map +1 -0
  39. package/dist/index.cjs +904 -185
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.ts +20 -10
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +797 -95
  44. package/dist/index.js.map +1 -1
  45. package/dist/pages/CheckoutPage.d.ts.map +1 -1
  46. package/dist/pages/HomePage.d.ts +5 -2
  47. package/dist/pages/HomePage.d.ts.map +1 -1
  48. package/dist/pages/MyPurchasesPage.d.ts.map +1 -1
  49. package/dist/pages/OrderConfirmationPage.d.ts.map +1 -1
  50. package/dist/pages/ProductDetailPage.d.ts.map +1 -1
  51. package/dist/sections.d.ts +45 -2
  52. package/dist/sections.d.ts.map +1 -1
  53. package/dist/testids.d.ts +20 -0
  54. package/dist/testids.d.ts.map +1 -1
  55. package/dist/theme.d.ts +4 -0
  56. package/dist/theme.d.ts.map +1 -1
  57. package/package.json +7 -9
  58. package/src/auth.ts +47 -10
  59. package/src/blocks.tsx +7 -3
  60. package/src/component-contracts.ts +140 -0
  61. package/src/component-selectors.ts +31 -0
  62. package/src/components/CartDrawer.tsx +2 -1
  63. package/src/components/ProductCard.tsx +39 -23
  64. package/src/components/ProductEnquiryForm.tsx +175 -0
  65. package/src/components/ProductGrid.tsx +12 -3
  66. package/src/components/RelatedProducts.tsx +13 -4
  67. package/src/components/SmoothImage.tsx +59 -0
  68. package/src/components/StorefrontFooter.tsx +24 -14
  69. package/src/components/StorefrontHeader.tsx +43 -38
  70. package/src/components/sections/CategoryShowcase.tsx +3 -2
  71. package/src/components/sections/HeroSection.tsx +18 -2
  72. package/src/components/sections/MediaCarousel.tsx +141 -0
  73. package/src/components/sections/MiscSections.tsx +2 -1
  74. package/src/components/sections/ProductRail.tsx +11 -3
  75. package/src/components/sections/ProductSlider.tsx +119 -0
  76. package/src/config.ts +65 -24
  77. package/src/createStorefrontApp.tsx +62 -9
  78. package/src/define.ts +34 -0
  79. package/src/hooks/index.ts +1 -0
  80. package/src/hooks/useStorefront.ts +58 -0
  81. package/src/index.ts +61 -11
  82. package/src/pages/CheckoutPage.tsx +2 -1
  83. package/src/pages/HomePage.tsx +6 -2
  84. package/src/pages/MyPurchasesPage.tsx +2 -1
  85. package/src/pages/OrderConfirmationPage.tsx +2 -1
  86. package/src/pages/ProductDetailPage.tsx +138 -62
  87. package/src/scaffold.tsx +1 -1
  88. package/src/sections.ts +49 -2
  89. package/src/testids.ts +20 -0
  90. package/src/theme.ts +4 -0
  91. package/src/workflows/checkout.ts +1 -1
  92. package/dist/slot-contracts.d.ts +0 -34
  93. package/dist/slot-contracts.d.ts.map +0 -1
  94. package/src/slot-contracts.ts +0 -20
@@ -0,0 +1,58 @@
1
+ import { getShopProvider } from '@fayz-ai/shop/runtime'
2
+ import type { CreateProductEnquiryInput } from '@fayz-ai/shop/types'
3
+ import { useStorefrontConfig } from '../config'
4
+ import { navigateTo } from '../router'
5
+ import { useCatalogStore } from '../stores/catalog.store'
6
+ import { useCartStore } from '../stores/cart.store'
7
+ import type { ProductOptionSelection } from '../product-options'
8
+ import type { Product } from '@fayz-ai/shop/types'
9
+
10
+ export function useStorefront() {
11
+ return useStorefrontConfig()
12
+ }
13
+
14
+ export function useCatalog() {
15
+ const config = useStorefrontConfig()
16
+ const catalog = useCatalogStore()
17
+ return {
18
+ ...catalog,
19
+ catalogPath: config.catalogPath,
20
+ config,
21
+ }
22
+ }
23
+
24
+ export function useCart() {
25
+ return useCartStore()
26
+ }
27
+
28
+ export function useEnquiry() {
29
+ const config = useStorefrontConfig()
30
+ const createProductEnquiry = async (input: CreateProductEnquiryInput) => {
31
+ const provider = getShopProvider()
32
+ if (!provider.createProductEnquiry) {
33
+ throw new Error('The active shop provider does not support product enquiries.')
34
+ }
35
+ return provider.createProductEnquiry(input)
36
+ }
37
+
38
+ return {
39
+ enquiry: config.enquiry,
40
+ createProductEnquiry,
41
+ }
42
+ }
43
+
44
+ export function useStorefrontActions() {
45
+ const addItem = useCartStore((state) => state.addItem)
46
+ const openDrawer = useCartStore((state) => state.openDrawer)
47
+ const closeDrawer = useCartStore((state) => state.closeDrawer)
48
+ const { createProductEnquiry } = useEnquiry()
49
+
50
+ return {
51
+ navigateTo,
52
+ addToCart: (product: Product, quantity?: number, options?: ProductOptionSelection) =>
53
+ addItem(product, quantity, options),
54
+ openCart: openDrawer,
55
+ closeCart: closeDrawer,
56
+ createProductEnquiry,
57
+ }
58
+ }
package/src/index.ts CHANGED
@@ -1,15 +1,44 @@
1
1
  // Factory
2
- export { createStorefrontApp, initStorefrontRuntime, StorefrontShell } from './createStorefrontApp'
2
+ export { createStorefront, createStorefrontApp, initStorefrontRuntime, StorefrontShell } from './createStorefrontApp'
3
+ export type { CreateStorefrontOptions } from './createStorefrontApp'
4
+ export {
5
+ defineStorefrontConfig,
6
+ defineStorefrontComponents,
7
+ defineStorefrontRoutes,
8
+ defineStorefrontSections,
9
+ defineStorefrontApp,
10
+ } from './define'
3
11
  export type {
4
12
  StorefrontConfig,
5
13
  ResolvedStorefrontConfig,
6
14
  StorefrontRouteComponentProps,
7
15
  StorefrontRouteChrome,
16
+ StorefrontCommerceMode,
17
+ StorefrontImageLoadingConfig,
18
+ StorefrontImageLoadingMode,
8
19
  StorefrontRouteDefinition,
9
20
  StorefrontRouteKind,
10
21
  StorefrontRouteParams,
22
+ StorefrontEnquiryConfig,
11
23
  } from './config'
12
- export { useStorefrontConfig } from './config'
24
+ export type {
25
+ StorefrontActions,
26
+ StorefrontComponents,
27
+ ProductCardProps,
28
+ ProductDetailProps,
29
+ ProductGalleryProps,
30
+ ProductPriceProps,
31
+ ProductActionsProps,
32
+ ProductEnquiryFormContractProps,
33
+ CategoryCardProps,
34
+ CollectionSectionProps,
35
+ HeroSectionContractProps,
36
+ MediaCarouselProps as MediaCarouselContractProps,
37
+ ProductSliderProps as ProductSliderContractProps,
38
+ StorefrontChromeProps,
39
+ StorefrontStateProps,
40
+ } from './component-contracts'
41
+ export { useStorefrontConfig, useStorefrontConfigOptional } from './config'
13
42
 
14
43
  // Manifest path (renderApp(defineStorefront(config)) / storefront scaffold)
15
44
  export { defineStorefront, StorefrontScaffold } from './scaffold'
@@ -41,17 +70,31 @@ export {
41
70
  getCustomerAuthAdapter,
42
71
  resolveAuthAdapter,
43
72
  } from './auth'
44
- export type { StorefrontAuthAdapter, EstablishSessionOptions } from './auth'
73
+ export type { StorefrontAuthAdapter, StorefrontAuthConfig, EstablishSessionOptions } from './auth'
45
74
  export { useCatalogStore } from './stores/catalog.store'
46
75
  export type { CatalogState, CatalogSort } from './stores/catalog.store'
47
76
 
48
77
  // Hooks
49
- export { useProducts, useProduct, useCategories, useMyOrders, useDiscountValidator } from './hooks'
78
+ export {
79
+ useProducts,
80
+ useProduct,
81
+ useCategories,
82
+ useStorefront,
83
+ useCatalog,
84
+ useCart,
85
+ useEnquiry,
86
+ useStorefrontActions,
87
+ useMyOrders,
88
+ useDiscountValidator,
89
+ } from './hooks'
50
90
 
51
91
  // Components (for custom layouts)
52
92
  export { StorefrontHeader } from './components/StorefrontHeader'
93
+ export { SmoothImage } from './components/SmoothImage'
94
+ export type { SmoothImageProps } from './components/SmoothImage'
53
95
  export { ProductCard } from './components/ProductCard'
54
- export type { ProductCardProps } from './components/ProductCard'
96
+ export { ProductEnquiryForm } from './components/ProductEnquiryForm'
97
+ export type { ProductEnquiryFormProps } from './components/ProductEnquiryForm'
55
98
  export { OrderTrackingTimeline } from './components/OrderTrackingTimeline'
56
99
  export { ProductGrid } from './components/ProductGrid'
57
100
  export { FiltersPanel } from './components/FiltersPanel'
@@ -93,7 +136,7 @@ export type {
93
136
  HeroVariant,
94
137
  CardStyle,
95
138
  } from './theme'
96
- export type { HomeSection, HomeConfig, HeroSlide, NavLink, FooterConfig } from './sections'
139
+ export type { StorefrontSection, HomeSection, HomeConfig, HeroSlide, MediaCarouselItem, NavLink, FooterConfig } from './sections'
97
140
  export { bannerPlaceholder } from './sections'
98
141
  export {
99
142
  storefrontTemplates,
@@ -105,10 +148,14 @@ export {
105
148
  export type { StorefrontTemplate, StorefrontTemplateId } from './presets'
106
149
 
107
150
  // Home sections (for custom compositions)
108
- export { HomePage } from './pages/HomePage'
151
+ export { HomePage, StorefrontSections } from './pages/HomePage'
109
152
  export { HeroSection } from './components/sections/HeroSection'
153
+ export { MediaCarousel } from './components/sections/MediaCarousel'
154
+ export type { MediaCarouselProps } from './components/sections/MediaCarousel'
110
155
  export { CategoryShowcase } from './components/sections/CategoryShowcase'
111
156
  export { ProductRail } from './components/sections/ProductRail'
157
+ export { ProductSlider } from './components/sections/ProductSlider'
158
+ export type { ProductSliderProps } from './components/sections/ProductSlider'
112
159
  export {
113
160
  BenefitsRow,
114
161
  PromoBanner,
@@ -126,10 +173,13 @@ export type { RevealProps } from './motion'
126
173
  export { formatMoney, roundCents } from './format'
127
174
  export { TID } from './testids'
128
175
  export {
129
- productCardSlotContract,
130
- storefrontSlotContracts,
131
- } from './slot-contracts'
132
- export type { ProductCardSlotContract } from './slot-contracts'
176
+ productCardComponentContract,
177
+ storefrontComponentContracts,
178
+ } from './component-selectors'
179
+ export type {
180
+ ProductCardComponentContract,
181
+ ProductDetailComponentContract,
182
+ } from './component-selectors'
133
183
 
134
184
  // ---------------------------------------------------------------------------
135
185
  // Front-door re-exports: storefront apps depend on @fayz-ai/storefront alone.
@@ -18,6 +18,7 @@ import { useDiscountValidator } from '../hooks/useDiscountValidator'
18
18
  import { toast } from '../stores/toast.store'
19
19
  import { useStorefrontHead } from '../hooks/useStorefrontHead'
20
20
  import { SignInModal } from '../components/SignInModal'
21
+ import { SmoothImage } from '../components/SmoothImage'
21
22
 
22
23
  interface CheckoutForm {
23
24
  email: string
@@ -471,7 +472,7 @@ export function CheckoutPage() {
471
472
  {cart.lines.map((line) => (
472
473
  <li key={line.lineId ?? line.productId} className="flex gap-3 text-sm">
473
474
  <div className="relative h-16 w-16 flex-none overflow-hidden rounded-lg border bg-background">
474
- {line.imageUrl && <img src={line.imageUrl} alt={line.name} className="h-full w-full object-cover" />}
475
+ {line.imageUrl && <SmoothImage src={line.imageUrl} alt={line.name} className="h-full w-full object-cover" />}
475
476
  <span className="absolute -right-1 -top-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-muted-foreground px-1.5 text-[10px] font-bold text-background">
476
477
  {line.quantity}
477
478
  </span>
@@ -1,12 +1,16 @@
1
1
  import React from 'react'
2
2
  import { renderBlocks } from '@fayz-ai/core'
3
- import type { HomeSection } from '../sections'
3
+ import type { StorefrontSection } from '../sections'
4
4
  import { sectionsToBlocks } from '../blocks'
5
5
 
6
6
  // The home page is now a block tree resolved through the universal block
7
7
  // registry (see ../blocks). Section components and their props are unchanged;
8
8
  // only the dispatch mechanism moved from a switch to the registry, so the same
9
9
  // composition works from a pure-data AppManifest.
10
- export function HomePage({ sections }: { sections: HomeSection[] }) {
10
+ export function StorefrontSections({ sections }: { sections: readonly StorefrontSection[] }) {
11
11
  return <main>{renderBlocks(sectionsToBlocks(sections))}</main>
12
12
  }
13
+
14
+ export function HomePage({ sections }: { sections: readonly StorefrontSection[] }) {
15
+ return <StorefrontSections sections={sections} />
16
+ }
@@ -12,6 +12,7 @@ import { CustomerAccountShell, type AccountSection } from '../components/Custome
12
12
  import { OrderTrackingTimeline } from '../components/OrderTrackingTimeline'
13
13
  import { financialStatusBadge } from '../order-status'
14
14
  import { useStorefrontHead } from '../hooks/useStorefrontHead'
15
+ import { SmoothImage } from '../components/SmoothImage'
15
16
 
16
17
  function AuthForm() {
17
18
  const [mode, setMode] = useState<'signin' | 'signup'>('signin')
@@ -155,7 +156,7 @@ function OrdersPanel() {
155
156
  <ul className="mt-3 flex flex-wrap gap-2">
156
157
  {order.items.map((item) => (
157
158
  <li key={item.id} className="flex items-center gap-2 rounded-lg border px-2 py-1 text-xs">
158
- {item.imageUrl && <img src={item.imageUrl} alt={item.name} className="h-6 w-6 rounded object-cover" />}
159
+ {item.imageUrl && <SmoothImage src={item.imageUrl} alt={item.name} className="h-6 w-6 rounded object-cover" />}
159
160
  {item.name} × {item.quantity}
160
161
  </li>
161
162
  ))}
@@ -10,6 +10,7 @@ import { CustomerAccountShell } from '../components/CustomerAccountShell'
10
10
  import { OrderTrackingTimeline } from '../components/OrderTrackingTimeline'
11
11
  import { financialStatusBadge } from '../order-status'
12
12
  import { useStorefrontHead } from '../hooks/useStorefrontHead'
13
+ import { SmoothImage } from '../components/SmoothImage'
13
14
 
14
15
  const CONFETTI_COLORS = ['hsl(var(--primary))', '#f59e0b', '#10b981', '#3b82f6', '#ec4899']
15
16
 
@@ -139,7 +140,7 @@ export function OrderConfirmationPage({ orderId }: { orderId: string }) {
139
140
  <ul className="mt-8 space-y-3 text-left">
140
141
  {order.items.map((item) => (
141
142
  <li key={item.id} className="flex items-center gap-3 text-sm">
142
- {item.imageUrl && <img src={item.imageUrl} alt={item.name} className="h-12 w-12 rounded-lg border object-cover" />}
143
+ {item.imageUrl && <SmoothImage src={item.imageUrl} alt={item.name} className="h-12 w-12 rounded-lg border object-cover" />}
143
144
  <span className="flex-1">
144
145
  {item.name} <span className="text-muted-foreground">× {item.quantity}</span>
145
146
  </span>
@@ -2,19 +2,24 @@ import React, { useEffect, useState } from 'react'
2
2
  import { ChevronLeft, RefreshCcw, ShieldCheck, Truck } from 'lucide-react'
3
3
  import { useProduct } from '../hooks/useProduct'
4
4
  import { useCartStore } from '../stores/cart.store'
5
- import { useStorefrontConfig } from '../config'
5
+ import { getStorefrontComponents, useStorefrontConfig } from '../config'
6
6
  import { useStorefrontHead } from '../hooks/useStorefrontHead'
7
+ import { useStorefrontActions } from '../hooks/useStorefront'
7
8
  import { Link } from '../router'
8
9
  import { Price } from '../components/Price'
9
10
  import { QuantityInput } from '../components/QuantityInput'
10
11
  import { ProductOptionSelector } from '../components/ProductOptionSelector'
11
12
  import { ProductSpecs } from '../components/ProductSpecs'
12
13
  import { RelatedProducts } from '../components/RelatedProducts'
14
+ import { ProductEnquiryForm } from '../components/ProductEnquiryForm'
15
+ import { SmoothImage } from '../components/SmoothImage'
13
16
  import { getProductOptionGroups, type ProductOptionSelection } from '../product-options'
17
+ import { storefrontComponentContracts } from '../component-selectors'
14
18
  import { TID } from '../testids'
15
19
 
16
20
  export function ProductDetailPage({ slug }: { slug: string }) {
17
21
  const config = useStorefrontConfig()
22
+ const actions = useStorefrontActions()
18
23
  const { product, loading } = useProduct(slug)
19
24
  const addItem = useCartStore((s) => s.addItem)
20
25
  const [qty, setQty] = useState(1)
@@ -61,21 +66,52 @@ export function ProductDetailPage({ slug }: { slug: string }) {
61
66
  const soldOut = product.inventoryCount <= 0
62
67
  const lowStock = !soldOut && product.inventoryCount <= 5
63
68
  const image = product.images.find((i) => i.isPrimary) ?? product.images[0]
69
+ const components = getStorefrontComponents(config)
70
+ const ProductDetailComponent = components.ProductDetail
71
+ const addToCart = () => addItem(product, qty, selectedOptions)
72
+ const openEnquiry = () => {
73
+ document.getElementById('storefront-product-enquiry')?.scrollIntoView({ behavior: 'smooth', block: 'start' })
74
+ }
64
75
  const discountPct =
65
76
  product.compareAtPrice && product.compareAtPrice > product.price
66
77
  ? Math.round((1 - product.price / product.compareAtPrice) * 100)
67
78
  : 0
68
79
 
80
+ if (ProductDetailComponent) {
81
+ return (
82
+ <ProductDetailComponent
83
+ product={product}
84
+ config={config}
85
+ commerceMode={config.commerceMode}
86
+ primaryImage={image}
87
+ optionGroups={optionGroups}
88
+ selectedOptions={selectedOptions}
89
+ setSelectedOptions={setSelectedOptions}
90
+ quantity={qty}
91
+ setQuantity={setQty}
92
+ soldOut={soldOut}
93
+ lowStock={lowStock}
94
+ actions={actions}
95
+ addToCart={addToCart}
96
+ openEnquiry={openEnquiry}
97
+ />
98
+ )
99
+ }
100
+
69
101
  return (
70
- <main className="mx-auto max-w-5xl px-4 py-8 sm:px-6">
102
+ <main {...storefrontComponentContracts.productDetail.root} className="mx-auto max-w-5xl px-4 py-8 sm:px-6">
71
103
  <Link to="/" className="mb-6 inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
72
104
  <ChevronLeft className="h-4 w-4" /> Continuar comprando
73
105
  </Link>
74
106
 
75
107
  <div className="grid animate-fade-up gap-10 md:grid-cols-2">
76
- <div className="group overflow-hidden border bg-muted" style={{ borderRadius: 'var(--sf-radius-card)' }}>
108
+ <div
109
+ {...storefrontComponentContracts.productDetail.gallery}
110
+ className="group overflow-hidden border bg-muted"
111
+ style={{ borderRadius: 'var(--sf-radius-card)' }}
112
+ >
77
113
  {image && (
78
- <img
114
+ <SmoothImage
79
115
  src={image.url}
80
116
  alt={image.altText ?? product.name}
81
117
  className="aspect-square w-full object-cover transition-transform duration-700 ease-out group-hover:scale-105"
@@ -100,43 +136,47 @@ export function ProductDetailPage({ slug }: { slug: string }) {
100
136
  {product.name}
101
137
  </h1>
102
138
 
103
- <div className="mt-3 flex items-center gap-3">
104
- <div className="text-xl">
105
- <Price
106
- value={product.price}
107
- compareAt={product.compareAtPrice}
108
- testId={TID.pdpPrice}
109
- compareTestId={TID.pdpComparePrice}
110
- />
139
+ {config.commerceMode === 'checkout' && (
140
+ <div className="mt-3 flex items-center gap-3">
141
+ <div className="text-xl">
142
+ <Price
143
+ value={product.price}
144
+ compareAt={product.compareAtPrice}
145
+ testId={TID.pdpPrice}
146
+ compareTestId={TID.pdpComparePrice}
147
+ />
148
+ </div>
149
+ {discountPct > 0 && (
150
+ <span className="rounded-full bg-primary/10 px-2.5 py-1 text-xs font-bold text-primary">
151
+ −{discountPct}%
152
+ </span>
153
+ )}
111
154
  </div>
112
- {discountPct > 0 && (
113
- <span className="rounded-full bg-primary/10 px-2.5 py-1 text-xs font-bold text-primary">
114
- −{discountPct}%
115
- </span>
116
- )}
117
- </div>
155
+ )}
118
156
 
119
157
  <p data-testid={TID.pdpDescription} className="mt-5 leading-relaxed text-muted-foreground">
120
158
  {product.description}
121
159
  </p>
122
160
 
123
- <div className="mt-4 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm">
124
- {soldOut ? (
125
- <span className="inline-flex items-center gap-2 font-medium text-muted-foreground">
126
- <span className="h-2 w-2 rounded-full bg-muted-foreground" /> Sem estoque
127
- </span>
128
- ) : lowStock ? (
129
- <span className="inline-flex items-center gap-2 font-semibold text-amber-600">
130
- <span className="h-2 w-2 animate-pulse rounded-full bg-amber-500" />
131
- Últimas unidades {product.inventoryCount} restantes
132
- </span>
133
- ) : (
134
- <span className="inline-flex items-center gap-2 font-medium text-emerald-600">
135
- <span className="h-2 w-2 rounded-full bg-emerald-500" /> Em estoque
136
- </span>
137
- )}
138
- {product.sku && <span className="text-muted-foreground">SKU: {product.sku}</span>}
139
- </div>
161
+ {config.commerceMode === 'checkout' && (
162
+ <div className="mt-4 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm">
163
+ {soldOut ? (
164
+ <span className="inline-flex items-center gap-2 font-medium text-muted-foreground">
165
+ <span className="h-2 w-2 rounded-full bg-muted-foreground" /> Sem estoque
166
+ </span>
167
+ ) : lowStock ? (
168
+ <span className="inline-flex items-center gap-2 font-semibold text-amber-600">
169
+ <span className="h-2 w-2 animate-pulse rounded-full bg-amber-500" />
170
+ Últimas unidades — só {product.inventoryCount} restantes
171
+ </span>
172
+ ) : (
173
+ <span className="inline-flex items-center gap-2 font-medium text-emerald-600">
174
+ <span className="h-2 w-2 rounded-full bg-emerald-500" /> Em estoque
175
+ </span>
176
+ )}
177
+ {product.sku && <span className="text-muted-foreground">SKU: {product.sku}</span>}
178
+ </div>
179
+ )}
140
180
 
141
181
  <ProductOptionSelector
142
182
  groups={optionGroups}
@@ -144,39 +184,75 @@ export function ProductDetailPage({ slug }: { slug: string }) {
144
184
  onChange={setSelectedOptions}
145
185
  />
146
186
 
147
- <div className="mt-8 flex items-center gap-4">
148
- <QuantityInput
149
- value={qty}
150
- onChange={setQty}
151
- max={soldOut ? 1 : product.inventoryCount}
152
- testId={TID.pdpQtyInput}
153
- />
154
- <button
155
- type="button"
156
- data-testid={TID.pdpAddToCart}
157
- disabled={soldOut}
158
- onClick={() => addItem(product, qty, selectedOptions)}
159
- className="sf-cta flex-1 bg-primary py-3.5 font-semibold text-primary-foreground shadow-md transition-all hover:-translate-y-0.5 hover:shadow-lg disabled:cursor-not-allowed disabled:opacity-40 disabled:shadow-none disabled:hover:translate-y-0"
160
- style={{ borderRadius: 'var(--sf-radius-button)' }}
161
- >
162
- {soldOut ? 'Esgotado' : 'Adicionar ao carrinho'}
163
- </button>
187
+ <div {...storefrontComponentContracts.productDetail.actions} className="mt-8">
188
+ {config.commerceMode === 'checkout' ? (
189
+ <div className="flex items-center gap-4">
190
+ <QuantityInput
191
+ value={qty}
192
+ onChange={setQty}
193
+ max={soldOut ? 1 : product.inventoryCount}
194
+ testId={TID.pdpQtyInput}
195
+ />
196
+ <button
197
+ type="button"
198
+ data-testid={TID.pdpAddToCart}
199
+ disabled={soldOut}
200
+ onClick={addToCart}
201
+ className="sf-cta flex-1 bg-primary py-3.5 font-semibold text-primary-foreground shadow-md transition-all hover:-translate-y-0.5 hover:shadow-lg disabled:cursor-not-allowed disabled:opacity-40 disabled:shadow-none disabled:hover:translate-y-0"
202
+ style={{ borderRadius: 'var(--sf-radius-button)' }}
203
+ >
204
+ {soldOut ? 'Esgotado' : 'Adicionar ao carrinho'}
205
+ </button>
206
+ </div>
207
+ ) : config.commerceMode === 'enquiry' ? (
208
+ <button
209
+ type="button"
210
+ {...storefrontComponentContracts.productDetail.enquiryButton}
211
+ onClick={openEnquiry}
212
+ className="sf-cta w-full bg-primary py-3.5 font-semibold text-primary-foreground shadow-md transition-all hover:-translate-y-0.5 hover:shadow-lg"
213
+ style={{ borderRadius: 'var(--sf-radius-button)' }}
214
+ >
215
+ {config.enquiry.label}
216
+ </button>
217
+ ) : (
218
+ <Link
219
+ to={config.catalogPath}
220
+ className="inline-flex border px-5 py-3 text-sm font-semibold transition-colors hover:bg-muted"
221
+ style={{ borderRadius: 'var(--sf-radius-button)' }}
222
+ >
223
+ Voltar ao catálogo
224
+ </Link>
225
+ )}
164
226
  </div>
165
227
 
166
- <div className="mt-6 grid grid-cols-3 gap-3 border-t pt-6 text-center">
167
- <div className="flex flex-col items-center gap-1.5 text-[11px] font-medium text-muted-foreground">
168
- <Truck className="h-5 w-5 text-primary" /> Envio rápido
169
- </div>
170
- <div className="flex flex-col items-center gap-1.5 text-[11px] font-medium text-muted-foreground">
171
- <RefreshCcw className="h-5 w-5 text-primary" /> Troca fácil
172
- </div>
173
- <div className="flex flex-col items-center gap-1.5 text-[11px] font-medium text-muted-foreground">
174
- <ShieldCheck className="h-5 w-5 text-primary" /> Pagamento seguro
228
+ {config.commerceMode === 'checkout' && (
229
+ <div className="mt-6 grid grid-cols-3 gap-3 border-t pt-6 text-center">
230
+ <div className="flex flex-col items-center gap-1.5 text-[11px] font-medium text-muted-foreground">
231
+ <Truck className="h-5 w-5 text-primary" /> Envio rápido
232
+ </div>
233
+ <div className="flex flex-col items-center gap-1.5 text-[11px] font-medium text-muted-foreground">
234
+ <RefreshCcw className="h-5 w-5 text-primary" /> Troca fácil
235
+ </div>
236
+ <div className="flex flex-col items-center gap-1.5 text-[11px] font-medium text-muted-foreground">
237
+ <ShieldCheck className="h-5 w-5 text-primary" /> Pagamento seguro
238
+ </div>
175
239
  </div>
176
- </div>
240
+ )}
177
241
  </div>
178
242
  </div>
179
243
 
244
+ {config.commerceMode === 'enquiry' && (
245
+ <section id="storefront-product-enquiry" className="mt-12 border-t pt-10">
246
+ <div className="mx-auto max-w-3xl">
247
+ <h2 className="sf-heading text-2xl font-semibold">Need more information?</h2>
248
+ <p className="mt-2 text-muted-foreground">Send an enquiry about this product.</p>
249
+ <div className="mt-6">
250
+ <ProductEnquiryForm product={product} />
251
+ </div>
252
+ </div>
253
+ </section>
254
+ )}
255
+
180
256
  <ProductSpecs product={product} />
181
257
  <RelatedProducts product={product} />
182
258
  </main>
package/src/scaffold.tsx CHANGED
@@ -80,7 +80,7 @@ function manifestToStorefrontConfig(manifest: AppManifest, surfaceName: string):
80
80
  provider: manifest.backend?.provider as StorefrontBackendProvider,
81
81
  url: manifest.backend?.url,
82
82
  },
83
- slots: runtimeConfig?.slots,
83
+ components: runtimeConfig?.components,
84
84
  routes: runtimeConfig?.routes,
85
85
  provider: runtimeConfig?.provider,
86
86
  auth: runtimeConfig?.auth,
package/src/sections.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { HeroVariant } from './theme'
2
+ import type React from 'react'
2
3
 
3
4
  // ---------------------------------------------------------------------------
4
5
  // Home-page section blueprint — the storefront's PageConfig equivalent.
@@ -19,8 +20,27 @@ export interface HeroSlide {
19
20
  boxed?: boolean
20
21
  }
21
22
 
22
- export type HomeSection =
23
+ export interface MediaCarouselItem {
24
+ title: string
25
+ subtitle?: string
26
+ eyebrow?: string
27
+ cta?: string
28
+ href?: string
29
+ image: string
30
+ imageAlt?: string
31
+ align?: 'start' | 'center' | 'end'
32
+ }
33
+
34
+ export type StorefrontSection =
23
35
  | { type: 'hero'; variant: HeroVariant; slides: HeroSlide[]; height?: 'tall' | 'medium' }
36
+ | {
37
+ type: 'mediaCarousel'
38
+ items: MediaCarouselItem[]
39
+ height?: 'medium' | 'tall' | 'screen'
40
+ autoplayMs?: number
41
+ overlay?: 'dark' | 'soft' | 'none'
42
+ className?: string
43
+ }
24
44
  | { type: 'categories'; style: 'bubbles' | 'tiles'; title?: string }
25
45
  | {
26
46
  type: 'products'
@@ -32,14 +52,30 @@ export type HomeSection =
32
52
  filter?: 'sale' | 'new' | 'all'
33
53
  limit?: number
34
54
  }
55
+ | {
56
+ type: 'productSlider'
57
+ title: string
58
+ subtitle?: string
59
+ eyebrow?: string
60
+ filter?: 'sale' | 'new' | 'all'
61
+ categoryName?: string
62
+ collection?: string
63
+ limit?: number
64
+ cta?: string
65
+ href?: string
66
+ className?: string
67
+ }
35
68
  | { type: 'benefits'; items: Array<{ icon: string; title: string; text?: string }> }
36
69
  | { type: 'banner'; title: string; subtitle?: string; eyebrow?: string; cta?: string; href?: string; image?: string; hue?: number }
37
70
  | { type: 'manifesto'; title?: string; text: string }
38
71
  | { type: 'testimonials'; title?: string; items: Array<{ quote: string; author: string }> }
39
72
  | { type: 'newsletter'; title?: string; subtitle?: string }
40
73
 
74
+ /** @deprecated Use StorefrontSection. Kept for existing generated stores. */
75
+ export type HomeSection = StorefrontSection
76
+
41
77
  export interface HomeConfig {
42
- sections: HomeSection[]
78
+ sections: readonly StorefrontSection[]
43
79
  }
44
80
 
45
81
  export interface NavLink {
@@ -51,6 +87,17 @@ export interface FooterConfig {
51
87
  about?: string
52
88
  contact?: { phone?: string; email?: string; address?: string }
53
89
  social?: Array<{ label: string; href: string }>
90
+ credit?: React.ReactNode
91
+ labels?: {
92
+ categories?: string
93
+ navigation?: string
94
+ contact?: string
95
+ viewAll?: string
96
+ purchases?: string
97
+ privacy?: string
98
+ terms?: string
99
+ returns?: string
100
+ }
54
101
  }
55
102
 
56
103
  /** Wide gradient SVG data-URI — deterministic hero/banner art, no network. */
package/src/testids.ts CHANGED
@@ -15,6 +15,13 @@ export const TID = {
15
15
  hero: 'home-hero',
16
16
  heroPrev: 'hero-prev',
17
17
  heroNext: 'hero-next',
18
+ mediaCarousel: 'media-carousel',
19
+ mediaCarouselPrev: 'media-carousel-prev',
20
+ mediaCarouselNext: 'media-carousel-next',
21
+ productSlider: 'product-slider',
22
+ productSliderViewport: 'product-slider-viewport',
23
+ productSliderPrev: 'product-slider-prev',
24
+ productSliderNext: 'product-slider-next',
18
25
  categoryShowcase: 'category-showcase',
19
26
  productRail: 'product-rail',
20
27
  benefits: 'benefits-row',
@@ -49,6 +56,19 @@ export const TID = {
49
56
  productOptionValue: (id: string, value: string) => `product-option-${id.toLowerCase().replace(/\s+/g, '-')}-${value.toLowerCase().replace(/\s+/g, '-')}`,
50
57
  pdpQtyInput: 'pdp-qty-input',
51
58
  pdpAddToCart: 'pdp-add-to-cart',
59
+ pdpRoot: 'pdp-root',
60
+ pdpGallery: 'pdp-gallery',
61
+ pdpActions: 'pdp-actions',
62
+ pdpEnquiryButton: 'pdp-enquiry-button',
63
+ enquiryForm: 'enquiry-form',
64
+ enquiryName: 'enquiry-name',
65
+ enquiryEmail: 'enquiry-email',
66
+ enquiryPhone: 'enquiry-phone',
67
+ enquirySubject: 'enquiry-subject',
68
+ enquiryMessage: 'enquiry-message',
69
+ enquirySubmit: 'enquiry-submit',
70
+ enquirySuccess: 'enquiry-success',
71
+ enquiryError: 'enquiry-error',
52
72
  // cart
53
73
  cartDrawer: 'cart-drawer',
54
74
  cartLine: 'cart-line',