@fayz-ai/storefront 0.6.0 → 0.8.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 (102) hide show
  1. package/README.md +3 -1
  2. package/dist/auth.d.ts +12 -2
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/blocks.d.ts +3 -3
  5. package/dist/blocks.d.ts.map +1 -1
  6. package/dist/component-contracts.d.ts +124 -0
  7. package/dist/component-contracts.d.ts.map +1 -0
  8. package/dist/component-selectors.d.ts +61 -0
  9. package/dist/component-selectors.d.ts.map +1 -0
  10. package/dist/components/CartDrawer.d.ts.map +1 -1
  11. package/dist/components/ProductCard.d.ts +2 -5
  12. package/dist/components/ProductCard.d.ts.map +1 -1
  13. package/dist/components/ProductEnquiryForm.d.ts +8 -0
  14. package/dist/components/ProductEnquiryForm.d.ts.map +1 -0
  15. package/dist/components/ProductGrid.d.ts.map +1 -1
  16. package/dist/components/RelatedProducts.d.ts +1 -1
  17. package/dist/components/RelatedProducts.d.ts.map +1 -1
  18. package/dist/components/SmoothImage.d.ts +7 -0
  19. package/dist/components/SmoothImage.d.ts.map +1 -0
  20. package/dist/components/StorefrontFooter.d.ts.map +1 -1
  21. package/dist/components/StorefrontHeader.d.ts.map +1 -1
  22. package/dist/components/sections/CategoryShowcase.d.ts.map +1 -1
  23. package/dist/components/sections/HeroSection.d.ts.map +1 -1
  24. package/dist/components/sections/MediaCarousel.d.ts +11 -0
  25. package/dist/components/sections/MediaCarousel.d.ts.map +1 -0
  26. package/dist/components/sections/MiscSections.d.ts.map +1 -1
  27. package/dist/components/sections/ProductRail.d.ts.map +1 -1
  28. package/dist/components/sections/ProductSlider.d.ts +15 -0
  29. package/dist/components/sections/ProductSlider.d.ts.map +1 -0
  30. package/dist/config.d.ts +38 -21
  31. package/dist/config.d.ts.map +1 -1
  32. package/dist/createStorefrontApp.d.ts +12 -0
  33. package/dist/createStorefrontApp.d.ts.map +1 -1
  34. package/dist/define.d.ts +17 -0
  35. package/dist/define.d.ts.map +1 -0
  36. package/dist/hooks/index.d.ts +1 -0
  37. package/dist/hooks/index.d.ts.map +1 -1
  38. package/dist/hooks/useStorefront.d.ts +35 -0
  39. package/dist/hooks/useStorefront.d.ts.map +1 -0
  40. package/dist/index.cjs +1113 -277
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.ts +21 -11
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +1006 -188
  45. package/dist/index.js.map +1 -1
  46. package/dist/pages/CheckoutPage.d.ts.map +1 -1
  47. package/dist/pages/HomePage.d.ts +5 -2
  48. package/dist/pages/HomePage.d.ts.map +1 -1
  49. package/dist/pages/MyPurchasesPage.d.ts.map +1 -1
  50. package/dist/pages/OrderConfirmationPage.d.ts.map +1 -1
  51. package/dist/pages/ProductDetailPage.d.ts.map +1 -1
  52. package/dist/presets.d.ts +5 -0
  53. package/dist/presets.d.ts.map +1 -1
  54. package/dist/sections.d.ts +45 -2
  55. package/dist/sections.d.ts.map +1 -1
  56. package/dist/stores/cart.store.d.ts +5 -0
  57. package/dist/stores/cart.store.d.ts.map +1 -1
  58. package/dist/testids.d.ts +21 -0
  59. package/dist/testids.d.ts.map +1 -1
  60. package/dist/theme.d.ts +21 -1
  61. package/dist/theme.d.ts.map +1 -1
  62. package/dist/workflows/checkout.d.ts.map +1 -1
  63. package/package.json +10 -6
  64. package/src/auth.ts +47 -10
  65. package/src/blocks.tsx +7 -3
  66. package/src/component-contracts.ts +140 -0
  67. package/src/component-selectors.ts +31 -0
  68. package/src/components/CartDrawer.tsx +2 -1
  69. package/src/components/ProductCard.tsx +39 -23
  70. package/src/components/ProductEnquiryForm.tsx +175 -0
  71. package/src/components/ProductGrid.tsx +12 -3
  72. package/src/components/RelatedProducts.tsx +13 -4
  73. package/src/components/SmoothImage.tsx +59 -0
  74. package/src/components/StorefrontFooter.tsx +24 -14
  75. package/src/components/StorefrontHeader.tsx +74 -42
  76. package/src/components/sections/CategoryShowcase.tsx +3 -2
  77. package/src/components/sections/HeroSection.tsx +18 -2
  78. package/src/components/sections/MediaCarousel.tsx +141 -0
  79. package/src/components/sections/MiscSections.tsx +2 -1
  80. package/src/components/sections/ProductRail.tsx +11 -3
  81. package/src/components/sections/ProductSlider.tsx +119 -0
  82. package/src/config.ts +65 -24
  83. package/src/createStorefrontApp.tsx +80 -10
  84. package/src/define.ts +34 -0
  85. package/src/hooks/index.ts +1 -0
  86. package/src/hooks/useStorefront.ts +58 -0
  87. package/src/index.ts +62 -11
  88. package/src/pages/CheckoutPage.tsx +2 -1
  89. package/src/pages/HomePage.tsx +6 -2
  90. package/src/pages/MyPurchasesPage.tsx +2 -1
  91. package/src/pages/OrderConfirmationPage.tsx +2 -1
  92. package/src/pages/ProductDetailPage.tsx +138 -62
  93. package/src/presets.ts +66 -0
  94. package/src/scaffold.tsx +1 -1
  95. package/src/sections.ts +49 -2
  96. package/src/stores/cart.store.ts +16 -1
  97. package/src/testids.ts +21 -0
  98. package/src/theme.ts +36 -2
  99. package/src/workflows/checkout.ts +5 -2
  100. package/dist/slot-contracts.d.ts +0 -34
  101. package/dist/slot-contracts.d.ts.map +0 -1
  102. package/src/slot-contracts.ts +0 -20
@@ -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/presets.ts CHANGED
@@ -248,11 +248,77 @@ export const atelierTemplate: StorefrontTemplate = {
248
248
  }),
249
249
  }
250
250
 
251
+ /** food — warm appetizing delivery (steakhouse / burger / comfort food): dark inviting
252
+ * header, ember CTA, cream canvas, rounded cards, menu-first home with "mais pedidos"
253
+ * rail and social proof. Built for restaurant/food stores — override to go fully dark. */
254
+ export const foodTemplate: StorefrontTemplate = {
255
+ id: 'food',
256
+ inspiration: 'Delivery / restaurant storefront',
257
+ announcement: 'ENTREGA GRÁTIS ACIMA DE R$ 150 · PEÇA JÁ',
258
+ theme: {
259
+ name: 'food',
260
+ colors: {
261
+ background: '30 33% 97%',
262
+ foreground: '20 14% 15%',
263
+ primary: '14 84% 50%',
264
+ primaryForeground: '0 0% 100%',
265
+ card: '30 33% 99%',
266
+ cardForeground: '20 14% 15%',
267
+ muted: '28 20% 92%',
268
+ mutedForeground: '22 10% 42%',
269
+ border: '28 18% 85%',
270
+ headerBackground: '20 14% 11%',
271
+ headerForeground: '30 24% 96%',
272
+ announcementBackground: '14 84% 50%',
273
+ announcementForeground: '0 0% 100%',
274
+ },
275
+ font: { heading: 'Poppins', body: 'Inter' },
276
+ radius: 'round',
277
+ header: { variant: 'classic' },
278
+ productCard: { style: 'card', imageAspect: 'square' },
279
+ },
280
+ home: (storeName, images) => ({
281
+ sections: [
282
+ {
283
+ type: 'hero',
284
+ variant: 'banner',
285
+ height: 'tall',
286
+ slides: [
287
+ { title: 'Sabor de verdade, na sua porta', subtitle: `${storeName} — feito na hora, do jeito que você ama.`, cta: 'Ver cardápio', href: '/catalog', hue: 20, image: images?.hero?.[0] },
288
+ ],
289
+ },
290
+ {
291
+ type: 'benefits',
292
+ items: [
293
+ { icon: 'Flame', title: 'Feito na hora', text: 'Preparado com capricho' },
294
+ { icon: 'Truck', title: 'Entrega rápida', text: 'Frete grátis acima de R$ 150' },
295
+ { icon: 'CreditCard', title: 'Pagamento fácil', text: 'Pix, cartão e mais' },
296
+ ],
297
+ },
298
+ { type: 'categories', style: 'tiles', title: 'Nosso cardápio' },
299
+ { type: 'products', title: 'Mais pedidos', eyebrow: 'favoritos da casa', filter: 'all', limit: 8 },
300
+ { type: 'banner', title: 'Combo do dia', eyebrow: 'oferta', subtitle: 'Seleção especial com desconto', cta: 'Aproveitar', hue: 14, image: images?.banners?.[0] },
301
+ { type: 'products', title: 'Novidades no cardápio', filter: 'new', limit: 4 },
302
+ {
303
+ type: 'testimonials',
304
+ title: 'O que dizem por aí',
305
+ items: [
306
+ { quote: 'Chegou quentinho e absurdo de bom. Virei cliente!', author: 'Rafael M.' },
307
+ { quote: 'Sabor de restaurante em casa, sem complicação.', author: 'Beatriz L.' },
308
+ { quote: 'Entrega rápida e porção generosa. Recomendo demais.', author: 'Diego S.' },
309
+ ],
310
+ },
311
+ { type: 'newsletter', title: 'Receba nossas ofertas', subtitle: 'Promoções e novidades do cardápio, direto no seu e-mail.' },
312
+ ],
313
+ }),
314
+ }
315
+
251
316
  export const storefrontTemplates = {
252
317
  mare: mareTemplate,
253
318
  sertao: sertaoTemplate,
254
319
  volt: voltTemplate,
255
320
  atelier: atelierTemplate,
321
+ food: foodTemplate,
256
322
  } as const
257
323
 
258
324
  export type StorefrontTemplateId = keyof typeof storefrontTemplates
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. */
@@ -40,11 +40,16 @@ export interface CartState {
40
40
  clear(): void
41
41
  openDrawer(): void
42
42
  closeDrawer(): void
43
+ /** Drop persisted lines the active backend can no longer resolve (e.g. a cart
44
+ * built in an earlier mock-mode session, whose ids don't exist live). The
45
+ * resolver returns false only for a definitive "not found"; on error it must
46
+ * return true so a transient failure never discards a legit line. */
47
+ reconcile(resolve: (line: CartLine) => Promise<boolean>): Promise<void>
43
48
  }
44
49
 
45
50
  export const useCartStore = create<CartState>()(
46
51
  persist(
47
- (set) => ({
52
+ (set, get) => ({
48
53
  lines: [],
49
54
  discountCode: null,
50
55
  discountPercent: 0,
@@ -106,6 +111,16 @@ export const useCartStore = create<CartState>()(
106
111
  clear: () => set({ lines: [], discountCode: null, discountPercent: 0 }),
107
112
  openDrawer: () => set({ isOpen: true }),
108
113
  closeDrawer: () => set({ isOpen: false, justAddedLineId: null }),
114
+
115
+ reconcile: async (resolve) => {
116
+ const lines = get().lines
117
+ if (lines.length === 0) return
118
+ const keep = await Promise.all(
119
+ lines.map((line) => resolve(line).catch(() => true)),
120
+ )
121
+ const kept = lines.filter((_, i) => keep[i])
122
+ if (kept.length !== lines.length) set({ lines: kept })
123
+ },
109
124
  }),
110
125
  {
111
126
  name: 'fayz.storefront.cart.v1',
package/src/testids.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  export const TID = {
4
4
  // header
5
5
  headerSearch: 'header-search',
6
+ headerSearchToggle: 'header-search-toggle',
6
7
  cartButton: 'cart-button',
7
8
  cartCount: 'cart-count',
8
9
  accountLink: 'account-link',
@@ -15,6 +16,13 @@ export const TID = {
15
16
  hero: 'home-hero',
16
17
  heroPrev: 'hero-prev',
17
18
  heroNext: 'hero-next',
19
+ mediaCarousel: 'media-carousel',
20
+ mediaCarouselPrev: 'media-carousel-prev',
21
+ mediaCarouselNext: 'media-carousel-next',
22
+ productSlider: 'product-slider',
23
+ productSliderViewport: 'product-slider-viewport',
24
+ productSliderPrev: 'product-slider-prev',
25
+ productSliderNext: 'product-slider-next',
18
26
  categoryShowcase: 'category-showcase',
19
27
  productRail: 'product-rail',
20
28
  benefits: 'benefits-row',
@@ -49,6 +57,19 @@ export const TID = {
49
57
  productOptionValue: (id: string, value: string) => `product-option-${id.toLowerCase().replace(/\s+/g, '-')}-${value.toLowerCase().replace(/\s+/g, '-')}`,
50
58
  pdpQtyInput: 'pdp-qty-input',
51
59
  pdpAddToCart: 'pdp-add-to-cart',
60
+ pdpRoot: 'pdp-root',
61
+ pdpGallery: 'pdp-gallery',
62
+ pdpActions: 'pdp-actions',
63
+ pdpEnquiryButton: 'pdp-enquiry-button',
64
+ enquiryForm: 'enquiry-form',
65
+ enquiryName: 'enquiry-name',
66
+ enquiryEmail: 'enquiry-email',
67
+ enquiryPhone: 'enquiry-phone',
68
+ enquirySubject: 'enquiry-subject',
69
+ enquiryMessage: 'enquiry-message',
70
+ enquirySubmit: 'enquiry-submit',
71
+ enquirySuccess: 'enquiry-success',
72
+ enquiryError: 'enquiry-error',
52
73
  // cart
53
74
  cartDrawer: 'cart-drawer',
54
75
  cartLine: 'cart-line',
package/src/theme.ts CHANGED
@@ -33,6 +33,10 @@ export interface StorefrontThemeColors {
33
33
  /** Announcement bar */
34
34
  announcementBackground?: string
35
35
  announcementForeground?: string
36
+ /** Secondary brand accent (e.g. a gourmet gold beside the primary). Emitted
37
+ * as `--sf-accent` / `--sf-accent-foreground` and the `accent` tailwind token. */
38
+ accent?: string
39
+ accentForeground?: string
36
40
  }
37
41
 
38
42
  export interface StorefrontThemeFont {
@@ -40,7 +44,11 @@ export interface StorefrontThemeFont {
40
44
  heading: string
41
45
  /** Body font, e.g. 'Rubik' */
42
46
  body: string
43
- /** Google Fonts families to load (defaults to heading+body) */
47
+ /** Optional oversized display face for hero words (emitted as `--sf-font-display`). */
48
+ display?: string
49
+ /** Optional serif accent for quotes/editorial (emitted as `--sf-font-serif`). */
50
+ serif?: string
51
+ /** Google Fonts families to load (defaults to heading+body+display+serif) */
44
52
  googleFonts?: string[]
45
53
  /** Fallback stack — default sans-serif; use 'serif' for editorial themes */
46
54
  fallback?: 'sans-serif' | 'serif'
@@ -56,6 +64,13 @@ export interface StorefrontTheme {
56
64
  variant?: HeaderVariant
57
65
  /** Uppercase nav links (premium retail tone) */
58
66
  uppercaseNav?: boolean
67
+ /** Show category shortcuts after primary nav links. Default true. */
68
+ showCategories?: boolean
69
+ /** Show the storefront search in the header. Default true. */
70
+ showSearch?: boolean
71
+ /** Header search presentation: a full input ('full', default) or a compact
72
+ * icon button that expands into an input on click ('icon'). */
73
+ searchStyle?: 'full' | 'icon'
59
74
  }
60
75
  productCard?: {
61
76
  style?: CardStyle
@@ -63,6 +78,11 @@ export interface StorefrontTheme {
63
78
  }
64
79
  /** Uppercase CTA buttons with letter-spacing (Uyuni/Flex tone) */
65
80
  uppercaseButtons?: boolean
81
+ /** Brand-token passthrough. Each entry is emitted verbatim as a `--<key>` CSS
82
+ * custom property, so a store can add gradients, glows, extra colors, or
83
+ * letter-spacing the fixed schema doesn't enumerate — without editing the SDK.
84
+ * e.g. { 'grad-ember': 'linear-gradient(...)', 'glow-ember': '0 0 0 1px ...' }. */
85
+ tokens?: Record<string, string>
66
86
  }
67
87
 
68
88
  const RADIUS_MAP: Record<StorefrontRadius, { button: string; card: string; input: string }> = {
@@ -86,6 +106,8 @@ const COLOR_VAR_MAP: Record<keyof StorefrontThemeColors, string> = {
86
106
  headerForeground: '--sf-header-fg',
87
107
  announcementBackground: '--sf-announcement-bg',
88
108
  announcementForeground: '--sf-announcement-fg',
109
+ accent: '--sf-accent',
110
+ accentForeground: '--sf-accent-foreground',
89
111
  }
90
112
 
91
113
  export function themeToCss(theme: StorefrontTheme): string {
@@ -107,6 +129,14 @@ export function themeToCss(theme: StorefrontTheme): string {
107
129
  const fallback = theme.font.fallback ?? 'sans-serif'
108
130
  lines.push(`--font-family: '${theme.font.body}', ${fallback};`)
109
131
  lines.push(`--sf-font-heading: '${theme.font.heading}', ${fallback};`)
132
+ if (theme.font.display) lines.push(`--sf-font-display: '${theme.font.display}', ${fallback};`)
133
+ if (theme.font.serif) lines.push(`--sf-font-serif: '${theme.font.serif}', serif;`)
134
+ }
135
+
136
+ // Brand-token passthrough — emit each entry as a raw CSS custom property.
137
+ for (const [key, value] of Object.entries(theme.tokens ?? {})) {
138
+ const name = key.startsWith('--') ? key : `--${key}`
139
+ lines.push(`${name}: ${value};`)
110
140
  }
111
141
 
112
142
  const radius = RADIUS_MAP[theme.radius ?? 'soft']
@@ -131,7 +161,11 @@ export function themeToCss(theme: StorefrontTheme): string {
131
161
 
132
162
  function googleFontsHref(theme: StorefrontTheme): string | null {
133
163
  if (!theme.font) return null
134
- const families = theme.font.googleFonts ?? [theme.font.heading, theme.font.body]
164
+ const families =
165
+ theme.font.googleFonts ??
166
+ [theme.font.heading, theme.font.body, theme.font.display, theme.font.serif].filter(
167
+ (f): f is string => Boolean(f),
168
+ )
135
169
  const unique = [...new Set(families)]
136
170
  const params = unique
137
171
  .map((f) => `family=${encodeURIComponent(f).replace(/%20/g, '+')}:wght@300;400;500;600;700`)
@@ -67,7 +67,7 @@ export async function placeStorefrontOrder({
67
67
  }
68
68
 
69
69
  // Trusted placement: send only product ids + quantities. The provider
70
- // (shop_place_order RPC on Supabase, in-memory parity in mock) re-reads
70
+ // (shop_place_order RPC on Supabase, in-memory provider in mock) re-reads
71
71
  // prices, validates the discount, and decrements inventory server-side.
72
72
  const order = await provider.placeOrder({
73
73
  customerId: customerId ?? undefined,
@@ -84,7 +84,10 @@ export async function placeStorefrontOrder({
84
84
  })
85
85
 
86
86
  if (markPaid) {
87
- await provider.updateOrder(order.id, { financialStatus: 'paid' })
87
+ // Prefer the RPC seam — anon storefronts have no UPDATE grant on the
88
+ // orders table, so a direct updateOrder 401s on pool backends.
89
+ if (provider.confirmPayment) await provider.confirmPayment(order.id)
90
+ else await provider.updateOrder(order.id, { financialStatus: 'paid' })
88
91
  }
89
92
 
90
93
  return { order, customerId: customerId ?? order.customerId ?? '' }
@@ -1,34 +0,0 @@
1
- /**
2
- * Stable selectors that custom storefront slots must preserve.
3
- *
4
- * Apps can fully customize the visual tree, but catalog/product-rail slots still
5
- * need these anchors so checkout smoke tests, QA, and agents can operate stores.
6
- */
7
- export declare const productCardSlotContract: {
8
- readonly root: {
9
- readonly 'data-testid': "product-card";
10
- };
11
- readonly name: {
12
- readonly 'data-testid': "product-card-name";
13
- };
14
- readonly priceTestId: "product-card-price";
15
- readonly addButton: {
16
- readonly 'data-testid': "product-card-add";
17
- };
18
- };
19
- export declare const storefrontSlotContracts: {
20
- readonly productCard: {
21
- readonly root: {
22
- readonly 'data-testid': "product-card";
23
- };
24
- readonly name: {
25
- readonly 'data-testid': "product-card-name";
26
- };
27
- readonly priceTestId: "product-card-price";
28
- readonly addButton: {
29
- readonly 'data-testid': "product-card-add";
30
- };
31
- };
32
- };
33
- export type ProductCardSlotContract = typeof productCardSlotContract;
34
- //# sourceMappingURL=slot-contracts.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"slot-contracts.d.ts","sourceRoot":"","sources":["../src/slot-contracts.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;;CAK1B,CAAA;AAEV,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;CAE1B,CAAA;AAEV,MAAM,MAAM,uBAAuB,GAAG,OAAO,uBAAuB,CAAA"}