@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,141 @@
1
+ import React, { useEffect, useRef, useState } from 'react'
2
+ import { ChevronLeft, ChevronRight } from 'lucide-react'
3
+ import { Link } from '../../router'
4
+ import { TID } from '../../testids'
5
+ import { prefersReducedMotion } from '../../motion'
6
+ import type { MediaCarouselItem } from '../../sections'
7
+ import { useStorefrontConfig } from '../../config'
8
+
9
+ export interface MediaCarouselProps {
10
+ items: MediaCarouselItem[]
11
+ height?: 'medium' | 'tall' | 'screen'
12
+ autoplayMs?: number
13
+ overlay?: 'dark' | 'soft' | 'none'
14
+ className?: string
15
+ }
16
+
17
+ const heightClass = {
18
+ medium: 'h-[58vh] min-h-[420px]',
19
+ tall: 'h-[74vh] min-h-[520px]',
20
+ screen: 'min-h-[calc(100vh-64px)]',
21
+ }
22
+
23
+ const alignClass = {
24
+ start: 'items-start text-left',
25
+ center: 'items-center text-center',
26
+ end: 'items-end text-right',
27
+ }
28
+
29
+ export function MediaCarousel({
30
+ items,
31
+ height = 'tall',
32
+ autoplayMs = 7000,
33
+ overlay = 'dark',
34
+ className = '',
35
+ }: MediaCarouselProps) {
36
+ const [index, setIndex] = useState(0)
37
+ const [loadedItems, setLoadedItems] = useState<Record<number, boolean>>({})
38
+ const config = useStorefrontConfig()
39
+ const interacted = useRef(false)
40
+ const revealImages = config.imageLoading.mode !== 'none'
41
+ const revealStyle = (loaded: boolean): React.CSSProperties | undefined =>
42
+ revealImages
43
+ ? {
44
+ filter: config.imageLoading.blur && !loaded ? 'blur(10px)' : 'blur(0px)',
45
+ transition: `opacity 700ms ease, filter ${config.imageLoading.durationMs}ms ${config.imageLoading.easing}`,
46
+ }
47
+ : undefined
48
+
49
+ useEffect(() => {
50
+ if (items.length < 2 || autoplayMs <= 0 || prefersReducedMotion()) return
51
+ const timer = setInterval(() => {
52
+ if (!interacted.current) setIndex((i) => (i + 1) % items.length)
53
+ }, autoplayMs)
54
+ return () => clearInterval(timer)
55
+ }, [autoplayMs, items.length])
56
+
57
+ if (items.length === 0) return null
58
+
59
+ const active = items[Math.min(index, items.length - 1)]!
60
+ const goTo = (next: number) => {
61
+ interacted.current = true
62
+ setIndex((next + items.length) % items.length)
63
+ }
64
+ const overlayClass =
65
+ overlay === 'dark'
66
+ ? 'bg-gradient-to-r from-black/70 via-black/25 to-black/25'
67
+ : overlay === 'soft'
68
+ ? 'bg-gradient-to-t from-black/45 via-black/10 to-transparent'
69
+ : ''
70
+
71
+ return (
72
+ <section data-testid={TID.mediaCarousel} className={`relative isolate overflow-hidden ${heightClass[height]} ${className}`}>
73
+ {items.map((item, i) => (
74
+ <img
75
+ key={`${item.title}-${i}`}
76
+ src={item.image}
77
+ alt={item.imageAlt ?? item.title}
78
+ aria-hidden={i !== index}
79
+ loading={i === 0 ? 'eager' : 'lazy'}
80
+ decoding="async"
81
+ onLoad={() => setLoadedItems((current) => ({ ...current, [i]: true }))}
82
+ style={revealStyle(Boolean(loadedItems[i]))}
83
+ className={`absolute inset-0 -z-10 h-full w-full object-cover transition-opacity duration-700 ${
84
+ i === index && (!revealImages || loadedItems[i]) ? 'opacity-100' : 'opacity-0'
85
+ }`}
86
+ />
87
+ ))}
88
+ <div className={`absolute inset-0 -z-10 ${overlayClass}`} />
89
+ <div className={`flex h-full px-5 py-16 sm:px-10 lg:px-16 ${alignClass[active.align ?? 'start']}`}>
90
+ <div className="my-auto max-w-2xl text-white drop-shadow-md">
91
+ {active.eyebrow && (
92
+ <p className="mb-4 text-xs font-semibold uppercase tracking-[0.24em] text-white/80">{active.eyebrow}</p>
93
+ )}
94
+ <h1 className="sf-heading text-5xl font-normal leading-[0.95] sm:text-7xl lg:text-8xl">{active.title}</h1>
95
+ {active.subtitle && <p className="mt-5 max-w-xl text-base leading-8 text-white/88 sm:text-lg">{active.subtitle}</p>}
96
+ {active.cta && (
97
+ <Link
98
+ to={active.href ?? '/catalog'}
99
+ className="mt-8 inline-flex border border-white/75 px-6 py-3 text-xs font-semibold uppercase tracking-[0.18em] text-white transition hover:bg-white hover:text-black"
100
+ >
101
+ {active.cta}
102
+ </Link>
103
+ )}
104
+ </div>
105
+ </div>
106
+ {items.length > 1 && (
107
+ <>
108
+ <button
109
+ type="button"
110
+ aria-label="Previous slide"
111
+ data-testid={TID.mediaCarouselPrev}
112
+ onClick={() => goTo(index - 1)}
113
+ className="absolute left-4 top-1/2 inline-flex h-10 w-10 -translate-y-1/2 items-center justify-center border border-white/60 bg-black/20 text-white backdrop-blur transition hover:bg-white hover:text-black"
114
+ >
115
+ <ChevronLeft className="h-5 w-5" />
116
+ </button>
117
+ <button
118
+ type="button"
119
+ aria-label="Next slide"
120
+ data-testid={TID.mediaCarouselNext}
121
+ onClick={() => goTo(index + 1)}
122
+ className="absolute right-4 top-1/2 inline-flex h-10 w-10 -translate-y-1/2 items-center justify-center border border-white/60 bg-black/20 text-white backdrop-blur transition hover:bg-white hover:text-black"
123
+ >
124
+ <ChevronRight className="h-5 w-5" />
125
+ </button>
126
+ <div className="absolute bottom-5 left-1/2 flex -translate-x-1/2 gap-2">
127
+ {items.map((item, i) => (
128
+ <button
129
+ key={`${item.title}-dot-${i}`}
130
+ type="button"
131
+ aria-label={`Go to slide ${i + 1}`}
132
+ onClick={() => goTo(i)}
133
+ className={`h-2 rounded-full transition-all ${i === index ? 'w-8 bg-white' : 'w-2 bg-white/55 hover:bg-white'}`}
134
+ />
135
+ ))}
136
+ </div>
137
+ </>
138
+ )}
139
+ </section>
140
+ )
141
+ }
@@ -4,6 +4,7 @@ import { bannerPlaceholder } from '../../sections'
4
4
  import { Reveal } from '../../motion'
5
5
  import { Link } from '../../router'
6
6
  import { TID } from '../../testids'
7
+ import { SmoothImage } from '../SmoothImage'
7
8
 
8
9
  function Icon({ name, className }: { name: string; className?: string }) {
9
10
  const C = (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[name]
@@ -49,7 +50,7 @@ export function PromoBanner({
49
50
  }) {
50
51
  return (
51
52
  <section data-testid={TID.promoBanner} className="group relative my-12 overflow-hidden">
52
- <img
53
+ <SmoothImage
53
54
  src={image ?? bannerPlaceholder(title, hue, hue + 50, 1600, 420)}
54
55
  alt={title}
55
56
  className="h-80 w-full object-cover transition-transform duration-700 ease-out group-hover:scale-105"
@@ -1,6 +1,7 @@
1
1
  import React, { useMemo } from 'react'
2
2
  import { useProducts } from '../../hooks/useProducts'
3
- import { useStorefrontConfig } from '../../config'
3
+ import { getStorefrontComponents, useStorefrontConfig } from '../../config'
4
+ import { useStorefrontActions } from '../../hooks/useStorefront'
4
5
  import { ProductCard } from '../ProductCard'
5
6
  import { Reveal } from '../../motion'
6
7
  import { Link } from '../../router'
@@ -20,7 +21,9 @@ export function ProductRail({
20
21
  limit?: number
21
22
  }) {
22
23
  const config = useStorefrontConfig()
23
- const ProductCardComponent = config.slots?.ProductCard ?? ProductCard
24
+ const actions = useStorefrontActions()
25
+ const components = getStorefrontComponents(config)
26
+ const ProductCardComponent = components.ProductCard ?? ProductCard
24
27
  const { products, loading } = useProducts(
25
28
  filter === 'new'
26
29
  ? { status: 'active', orderBy: 'created_at', order: 'desc' }
@@ -54,7 +57,12 @@ export function ProductRail({
54
57
  <div className="grid grid-cols-2 gap-5 md:grid-cols-4">
55
58
  {visible.map((p, i) => (
56
59
  <Reveal key={p.id} delay={i * 90}>
57
- <ProductCardComponent product={p} />
60
+ <ProductCardComponent
61
+ product={p}
62
+ config={config}
63
+ commerceMode={config.commerceMode}
64
+ actions={actions}
65
+ />
58
66
  </Reveal>
59
67
  ))}
60
68
  </div>
@@ -0,0 +1,119 @@
1
+ import React, { useMemo, useRef } from 'react'
2
+ import { ChevronLeft, ChevronRight } from 'lucide-react'
3
+ import { getStorefrontComponents, useStorefrontConfig } from '../../config'
4
+ import { useProducts } from '../../hooks/useProducts'
5
+ import { useStorefrontActions } from '../../hooks/useStorefront'
6
+ import { ProductCard } from '../ProductCard'
7
+ import { Link } from '../../router'
8
+ import { TID } from '../../testids'
9
+
10
+ export interface ProductSliderProps {
11
+ title: string
12
+ subtitle?: string
13
+ eyebrow?: string
14
+ filter?: 'sale' | 'new' | 'all'
15
+ categoryName?: string
16
+ collection?: string
17
+ limit?: number
18
+ cta?: string
19
+ href?: string
20
+ className?: string
21
+ }
22
+
23
+ export function ProductSlider({
24
+ title,
25
+ subtitle,
26
+ eyebrow,
27
+ filter = 'all',
28
+ categoryName,
29
+ collection,
30
+ limit = 10,
31
+ cta = 'View all',
32
+ href,
33
+ className = '',
34
+ }: ProductSliderProps) {
35
+ const config = useStorefrontConfig()
36
+ const actions = useStorefrontActions()
37
+ const viewportRef = useRef<HTMLDivElement>(null)
38
+ const components = getStorefrontComponents(config)
39
+ const ProductCardComponent = components.ProductCard ?? ProductCard
40
+ const { products, loading } = useProducts(
41
+ filter === 'new'
42
+ ? { status: 'active', orderBy: 'created_at', order: 'desc' }
43
+ : { status: 'active' },
44
+ )
45
+
46
+ const visible = useMemo(() => {
47
+ let list = products
48
+ if (filter === 'sale') list = list.filter((p) => p.compareAtPrice != null && p.compareAtPrice > p.price)
49
+ if (categoryName) list = list.filter((p) => p.categoryName === categoryName)
50
+ if (collection) list = list.filter((p) => p.metadata?.collection === collection)
51
+ return list.slice(0, limit)
52
+ }, [categoryName, collection, filter, limit, products])
53
+
54
+ const scroll = (direction: -1 | 1) => {
55
+ viewportRef.current?.scrollBy({ left: direction * Math.max(320, viewportRef.current.clientWidth * 0.75), behavior: 'smooth' })
56
+ }
57
+
58
+ if (!loading && visible.length === 0) return null
59
+
60
+ return (
61
+ <section data-testid={TID.productSlider} className={`mx-auto max-w-7xl px-4 py-14 sm:px-6 ${className}`}>
62
+ <div className="mb-8 flex flex-col gap-5 sm:flex-row sm:items-end sm:justify-between">
63
+ <div className="max-w-2xl">
64
+ {eyebrow && <p className="mb-2 text-xs font-semibold uppercase tracking-[0.2em] text-primary">{eyebrow}</p>}
65
+ <h2 className="sf-heading text-3xl font-normal leading-tight tracking-normal sm:text-5xl">{title}</h2>
66
+ {subtitle && <p className="mt-3 leading-7 text-muted-foreground">{subtitle}</p>}
67
+ </div>
68
+ <div className="flex items-center gap-2">
69
+ <button
70
+ type="button"
71
+ aria-label="Previous products"
72
+ data-testid={TID.productSliderPrev}
73
+ onClick={() => scroll(-1)}
74
+ className="inline-flex h-10 w-10 items-center justify-center border border-border bg-background transition hover:bg-foreground hover:text-background"
75
+ >
76
+ <ChevronLeft className="h-5 w-5" />
77
+ </button>
78
+ <button
79
+ type="button"
80
+ aria-label="Next products"
81
+ data-testid={TID.productSliderNext}
82
+ onClick={() => scroll(1)}
83
+ className="inline-flex h-10 w-10 items-center justify-center border border-border bg-background transition hover:bg-foreground hover:text-background"
84
+ >
85
+ <ChevronRight className="h-5 w-5" />
86
+ </button>
87
+ </div>
88
+ </div>
89
+ <div
90
+ ref={viewportRef}
91
+ data-testid={TID.productSliderViewport}
92
+ className="flex snap-x gap-5 overflow-x-auto pb-5 [scrollbar-width:thin]"
93
+ >
94
+ {loading
95
+ ? Array.from({ length: Math.min(limit, 6) }, (_, i) => (
96
+ <div key={i} className="h-[360px] min-w-[260px] snap-start animate-pulse bg-muted" />
97
+ ))
98
+ : visible.map((product) => (
99
+ <div key={product.id} className="min-w-[260px] max-w-[300px] flex-[0_0_72vw] snap-start sm:flex-[0_0_300px]">
100
+ <ProductCardComponent
101
+ product={product}
102
+ config={config}
103
+ commerceMode={config.commerceMode}
104
+ actions={actions}
105
+ />
106
+ </div>
107
+ ))}
108
+ </div>
109
+ {cta && (
110
+ <Link
111
+ to={href ?? config.catalogPath}
112
+ className="mt-4 inline-flex border border-foreground/30 px-6 py-3 text-xs font-semibold uppercase tracking-[0.16em] transition hover:bg-foreground hover:text-background"
113
+ >
114
+ {cta}
115
+ </Link>
116
+ )}
117
+ </section>
118
+ )
119
+ }
package/src/config.ts CHANGED
@@ -1,14 +1,10 @@
1
1
  import React, { createContext, useContext } from 'react'
2
- import type { Product } from '@fayz-ai/shop/types'
3
2
  import type { ShopProvider } from '@fayz-ai/shop/provider'
4
3
  import type { MockShopSeed } from '@fayz-ai/shop/mock'
5
4
  import type { StorefrontTheme } from './theme'
6
5
  import type { HomeConfig, NavLink, FooterConfig } from './sections'
7
- import type { StorefrontAuthAdapter } from './auth'
8
-
9
- export interface ProductCardSlotProps {
10
- product: Product
11
- }
6
+ import type { StorefrontAuthConfig } from './auth'
7
+ import type { StorefrontComponents } from './component-contracts'
12
8
 
13
9
  export type StorefrontRouteKind =
14
10
  | 'home'
@@ -28,6 +24,15 @@ export interface StorefrontRouteComponentProps {
28
24
  }
29
25
 
30
26
  export type StorefrontRouteChrome = 'default' | 'focused'
27
+ export type StorefrontCommerceMode = 'checkout' | 'catalog' | 'enquiry'
28
+ export type StorefrontImageLoadingMode = 'fade' | 'none'
29
+
30
+ export interface StorefrontImageLoadingConfig {
31
+ mode?: StorefrontImageLoadingMode
32
+ durationMs?: number
33
+ easing?: string
34
+ blur?: boolean
35
+ }
31
36
 
32
37
  export interface StorefrontRouteDefinition {
33
38
  /**
@@ -54,22 +59,21 @@ export interface StorefrontRouteDefinition {
54
59
  component: React.ComponentType<StorefrontRouteComponentProps>
55
60
  }
56
61
 
57
- export interface StorefrontSlots {
58
- /**
59
- * Product card renderer used by catalog grids and product rails.
60
- *
61
- * Custom renderers must preserve `productCardSlotContract` selectors so
62
- * checkout smoke tests, QA, and agents can still operate the storefront.
63
- */
64
- ProductCard?: React.ComponentType<ProductCardSlotProps>
65
- }
66
-
67
62
  export interface StorefrontDiscountConfig {
68
63
  code: string
69
64
  percent: number
70
65
  title?: string
71
66
  }
72
67
 
68
+ export interface StorefrontEnquiryConfig {
69
+ label?: string
70
+ successMessage?: string
71
+ subjectPrefix?: string
72
+ email?: string
73
+ whatsappUrl?: string
74
+ requirePhone?: boolean
75
+ }
76
+
73
77
  export interface StorefrontConfig {
74
78
  /** Store display name (header, page title) */
75
79
  name: string
@@ -81,6 +85,8 @@ export interface StorefrontConfig {
81
85
  locale?: string
82
86
  /** Visual identity: colors, fonts, radius, header/card personality */
83
87
  theme?: StorefrontTheme
88
+ /** Checkout-first default, catalog-only browsing, or product enquiry flow. */
89
+ commerceMode?: StorefrontCommerceMode
84
90
  /** Promo message above the header (e.g. 'FRETE GRÁTIS ACIMA DE R$ 300') */
85
91
  announcement?: string
86
92
  /** Home page blueprint. When set, `/` renders it and the catalog moves to /catalog. */
@@ -108,7 +114,7 @@ export interface StorefrontConfig {
108
114
  */
109
115
  legal?: { privacy?: string; terms?: string; returns?: string }
110
116
  /** Customer auth — same AuthAdapter contract as createSaasApp. Default: mock. */
111
- auth?: { adapter?: StorefrontAuthAdapter }
117
+ auth?: StorefrontAuthConfig
112
118
  /** Shipping pricing — default { flatRate: 0 } */
113
119
  shipping?: { flatRate?: number; freeAbove?: number }
114
120
  /**
@@ -117,12 +123,16 @@ export interface StorefrontConfig {
117
123
  * a real payment webhook confirms settlement.
118
124
  */
119
125
  payments?: { mode?: 'mock' | 'pix-mercadopago' }
120
- /** Feature toggles discounts default off; accounts default on */
121
- features?: { discounts?: boolean; accounts?: boolean }
122
- /** Code-level customization slots. Kept out of serialized manifests. */
123
- slots?: StorefrontSlots
126
+ /** Product enquiry behavior for catalog/enquiry stores. */
127
+ enquiry?: StorefrontEnquiryConfig
128
+ /** Feature toggles defaults depend on commerceMode. */
129
+ features?: { discounts?: boolean; accounts?: boolean; cart?: boolean; checkout?: boolean }
130
+ /** Global storefront image reveal behavior. Default is a soft fade. */
131
+ imageLoading?: StorefrontImageLoadingConfig
132
+ /** Code-level component replacements. Kept out of serialized manifests. */
133
+ components?: StorefrontComponents
124
134
  /** Code-level route overrides/custom routes. Kept out of serialized manifests. */
125
- routes?: StorefrontRouteDefinition[]
135
+ routes?: readonly StorefrontRouteDefinition[]
126
136
  }
127
137
 
128
138
  export interface ResolvedStorefrontConfig extends StorefrontConfig {
@@ -130,7 +140,11 @@ export interface ResolvedStorefrontConfig extends StorefrontConfig {
130
140
  locale: string
131
141
  shipping: { flatRate: number; freeAbove?: number }
132
142
  payments: { mode: 'mock' | 'pix-mercadopago' }
133
- features: { discounts: boolean; accounts: boolean }
143
+ commerceMode: StorefrontCommerceMode
144
+ enquiry: Required<Pick<StorefrontEnquiryConfig, 'label' | 'successMessage' | 'subjectPrefix'>> &
145
+ Omit<StorefrontEnquiryConfig, 'label' | 'successMessage' | 'subjectPrefix'>
146
+ features: { discounts: boolean; accounts: boolean; cart: boolean; checkout: boolean }
147
+ imageLoading: Required<StorefrontImageLoadingConfig>
134
148
  /** Where the full product list lives ('/' without a home page, '/catalog' with one) */
135
149
  catalogPath: string
136
150
  nav: NavLink[]
@@ -138,12 +152,23 @@ export interface ResolvedStorefrontConfig extends StorefrontConfig {
138
152
 
139
153
  export function resolveConfig(config: StorefrontConfig): ResolvedStorefrontConfig {
140
154
  const catalogPath = config.home ? '/catalog' : '/'
155
+ const commerceMode = config.commerceMode ?? 'checkout'
156
+ const checkoutEnabled = commerceMode === 'checkout'
141
157
  return {
142
158
  ...config,
143
159
  currency: config.currency ?? 'BRL',
144
160
  locale: config.locale ?? 'pt-BR',
145
161
  shipping: { flatRate: config.shipping?.flatRate ?? 0, freeAbove: config.shipping?.freeAbove },
146
162
  payments: { mode: config.payments?.mode ?? 'mock' },
163
+ commerceMode,
164
+ enquiry: {
165
+ label: config.enquiry?.label ?? 'Contact me',
166
+ successMessage: config.enquiry?.successMessage ?? 'Thanks. We received your enquiry.',
167
+ subjectPrefix: config.enquiry?.subjectPrefix ?? 'Product enquiry',
168
+ email: config.enquiry?.email,
169
+ whatsappUrl: config.enquiry?.whatsappUrl,
170
+ requirePhone: config.enquiry?.requirePhone,
171
+ },
147
172
  features: {
148
173
  // Auto-enable the coupon UI when the store actually ships discount codes
149
174
  // (via config.discounts or a seeded catalog), unless explicitly overridden —
@@ -151,7 +176,15 @@ export function resolveConfig(config: StorefrontConfig): ResolvedStorefrontConfi
151
176
  discounts:
152
177
  config.features?.discounts ??
153
178
  Boolean(config.discounts?.length || config.catalog?.discounts?.length),
154
- accounts: config.features?.accounts ?? true,
179
+ accounts: config.features?.accounts ?? checkoutEnabled,
180
+ cart: config.features?.cart ?? checkoutEnabled,
181
+ checkout: config.features?.checkout ?? checkoutEnabled,
182
+ },
183
+ imageLoading: {
184
+ mode: config.imageLoading?.mode ?? 'fade',
185
+ durationMs: config.imageLoading?.durationMs ?? 420,
186
+ easing: config.imageLoading?.easing ?? 'cubic-bezier(0.22, 1, 0.36, 1)',
187
+ blur: config.imageLoading?.blur ?? true,
155
188
  },
156
189
  catalogPath,
157
190
  nav:
@@ -174,3 +207,11 @@ export function useStorefrontConfig(): ResolvedStorefrontConfig {
174
207
  if (!ctx) throw new Error('useStorefrontConfig must be used inside createStorefrontApp')
175
208
  return ctx
176
209
  }
210
+
211
+ export function useStorefrontConfigOptional(): ResolvedStorefrontConfig | null {
212
+ return useContext(StorefrontConfigContext)
213
+ }
214
+
215
+ export function getStorefrontComponents(config: ResolvedStorefrontConfig): StorefrontComponents {
216
+ return config.components ?? {}
217
+ }
@@ -3,9 +3,12 @@ import { setShopProvider } from '@fayz-ai/shop/runtime'
3
3
  import { createMockShopProvider } from '@fayz-ai/shop/mock'
4
4
  import type { MockShopSeed } from '@fayz-ai/shop/mock'
5
5
  import type { Discount } from '@fayz-ai/shop/types'
6
+ import type { ShopProvider } from '@fayz-ai/shop/provider'
6
7
  import { initCustomerAuth, resolveAuthAdapter } from './auth'
7
- import { StorefrontConfigProvider, resolveConfig, useStorefrontConfig } from './config'
8
+ import { getStorefrontComponents, StorefrontConfigProvider, resolveConfig, useStorefrontConfig } from './config'
8
9
  import type { StorefrontConfig, StorefrontDiscountConfig } from './config'
10
+ import type { StorefrontComponents } from './component-contracts'
11
+ import type { StorefrontSection } from './sections'
9
12
  import { useHashPath, matchPath } from './router'
10
13
  import { StorefrontThemeStyle } from './theme'
11
14
  import { StorefrontHeader } from './components/StorefrontHeader'
@@ -51,8 +54,8 @@ function RouteSwitch({ path }: { path: string }) {
51
54
  const order = matchPath('/order/:id', path)
52
55
  if (order?.id) return <OrderConfirmationPage orderId={order.id} />
53
56
 
54
- if (matchPath('/checkout', path)) return <CheckoutPage />
55
- if (matchPath('/account', path)) return <MyPurchasesPage />
57
+ if (matchPath('/checkout', path) && config.features.checkout) return <CheckoutPage />
58
+ if (matchPath('/account', path) && config.features.accounts) return <MyPurchasesPage />
56
59
  if (matchPath('/catalog', path)) return <CatalogPage />
57
60
  if (matchPath('/privacy', path)) return <PolicyPage kind="privacy" />
58
61
  if (matchPath('/terms', path)) return <PolicyPage kind="terms" />
@@ -112,7 +115,12 @@ function buildMockSeed(config: StorefrontConfig): MockShopSeed | undefined {
112
115
  }
113
116
 
114
117
  export function initStorefrontRuntime(config: StorefrontConfig): void {
115
- initCustomerAuth(resolveAuthAdapter(config.auth?.adapter))
118
+ if (resolveConfig(config).features.accounts) {
119
+ initCustomerAuth(resolveAuthAdapter(config.auth, {
120
+ supabaseUrl: config.supabaseUrl,
121
+ supabaseAnonKey: config.supabaseAnonKey,
122
+ }))
123
+ }
116
124
 
117
125
  if (config.provider) {
118
126
  setShopProvider(config.provider)
@@ -133,16 +141,34 @@ export function StorefrontShell() {
133
141
  const config = useStorefrontConfig()
134
142
  const path = useHashPath()
135
143
  const focused = isFocusedRoute(config, path)
136
- return (
137
- <div className="min-h-screen bg-background text-foreground">
144
+ const components = getStorefrontComponents(config)
145
+ const chromeProps = { config, commerceMode: config.commerceMode }
146
+ const body = (
147
+ <>
138
148
  {config.theme && <StorefrontThemeStyle theme={config.theme} />}
139
- {!focused && <StorefrontHeader />}
149
+ {!focused &&
150
+ (components.Header ? <components.Header {...chromeProps} /> : <StorefrontHeader />)}
140
151
  <StorefrontErrorBoundary key={path}>
141
152
  <RouteSwitch path={path} />
142
153
  </StorefrontErrorBoundary>
143
- {!focused && <CartDrawer />}
144
- {!focused && <StorefrontFooter />}
154
+ {!focused && config.features.cart && <CartDrawer />}
155
+ {!focused &&
156
+ (components.Footer ? <components.Footer {...chromeProps} /> : <StorefrontFooter />)}
145
157
  <Toaster />
158
+ </>
159
+ )
160
+
161
+ if (components.Shell) {
162
+ return (
163
+ <components.Shell {...chromeProps}>
164
+ {body}
165
+ </components.Shell>
166
+ )
167
+ }
168
+
169
+ return (
170
+ <div className="min-h-screen bg-background text-foreground">
171
+ {body}
146
172
  </div>
147
173
  )
148
174
  }
@@ -169,3 +195,30 @@ export function createStorefrontApp(config: StorefrontConfig): React.ComponentTy
169
195
  StorefrontApp.displayName = 'StorefrontApp'
170
196
  return StorefrontApp
171
197
  }
198
+
199
+ export interface CreateStorefrontOptions {
200
+ config: StorefrontConfig
201
+ provider?: ShopProvider
202
+ components?: StorefrontComponents
203
+ routes?: StorefrontConfig['routes']
204
+ pages?: StorefrontConfig['routes']
205
+ sections?: readonly StorefrontSection[]
206
+ }
207
+
208
+ export function createStorefront(options: CreateStorefrontOptions): React.ComponentType {
209
+ const routes = [...(options.pages ?? []), ...(options.routes ?? [])]
210
+ const config: StorefrontConfig = {
211
+ ...options.config,
212
+ provider: options.provider ?? options.config.provider,
213
+ components: {
214
+ ...options.config.components,
215
+ ...options.components,
216
+ },
217
+ routes: routes.length > 0 ? routes : options.config.routes,
218
+ home: options.sections
219
+ ? { ...(options.config.home ?? { sections: [] }), sections: options.sections }
220
+ : options.config.home,
221
+ }
222
+
223
+ return createStorefrontApp(config)
224
+ }
package/src/define.ts ADDED
@@ -0,0 +1,34 @@
1
+ import type {
2
+ StorefrontConfig,
3
+ StorefrontRouteDefinition,
4
+ } from './config'
5
+ import type { StorefrontComponents } from './component-contracts'
6
+ import type { StorefrontSection } from './sections'
7
+ import type { CreateStorefrontOptions } from './createStorefrontApp'
8
+
9
+ /**
10
+ * Identity helpers for generated/custom storefronts.
11
+ *
12
+ * They do not add runtime behavior; they make the intended client-app shape
13
+ * explicit and keep TypeScript inference tight across config, components, routes,
14
+ * sections and app creation options.
15
+ */
16
+ export function defineStorefrontConfig<const T extends StorefrontConfig>(config: T): T {
17
+ return config
18
+ }
19
+
20
+ export function defineStorefrontComponents<const T extends StorefrontComponents>(components: T): T {
21
+ return components
22
+ }
23
+
24
+ export function defineStorefrontRoutes<const T extends readonly StorefrontRouteDefinition[]>(routes: T): T {
25
+ return routes
26
+ }
27
+
28
+ export function defineStorefrontSections<const T extends readonly StorefrontSection[]>(sections: T): T {
29
+ return sections
30
+ }
31
+
32
+ export function defineStorefrontApp<const T extends CreateStorefrontOptions>(options: T): T {
33
+ return options
34
+ }
@@ -1,5 +1,6 @@
1
1
  export { useProducts } from './useProducts'
2
2
  export { useProduct } from './useProduct'
3
3
  export { useCategories } from './useCategories'
4
+ export { useStorefront, useCatalog, useCart, useEnquiry, useStorefrontActions } from './useStorefront'
4
5
  export { useMyOrders } from './useMyOrders'
5
6
  export { useDiscountValidator } from './useDiscountValidator'