@ecomiq/storefront 2.1.3 → 2.1.4

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.
@@ -1,1481 +0,0 @@
1
- import { Fragment, type CSSProperties, type ReactNode } from 'react'
2
- import type { Collection, ProductSummary } from '@ecomiq/core'
3
- import { buttonVariants } from '../ui/button'
4
- import { cn } from '../ui/cn'
5
- import {
6
- resolveStorefrontSectionStyle,
7
- scopeStorefrontCss,
8
- storefrontSectionCssScope,
9
- STOREFRONT_SITE_CSS_SCOPE,
10
- } from '@ecomiq/storefront-definition'
11
- import type {
12
- StorefrontPage,
13
- CollectionGridStorefrontSection,
14
- HeroStorefrontSection,
15
- ImageTextStorefrontSection,
16
- NewsletterStorefrontSection,
17
- ProductGridStorefrontSection,
18
- PromoBannerStorefrontSection,
19
- RichTextStorefrontSection,
20
- StorefrontAction,
21
- StorefrontDefinition,
22
- StorefrontFontFamily,
23
- StorefrontImage,
24
- StorefrontNavigationItem,
25
- StorefrontContentWidth,
26
- StorefrontNamedStyle,
27
- StorefrontSection,
28
- StorefrontSectionStyle,
29
- StorefrontSpace,
30
- StorefrontThemeTokens,
31
- TestimonialsStorefrontSection,
32
- FeatureGridStorefrontSection,
33
- FaqStorefrontSection,
34
- StatsStorefrontSection,
35
- LogoWallStorefrontSection,
36
- GalleryStorefrontSection,
37
- ColumnsStorefrontSection,
38
- VideoStorefrontSection,
39
- } from '@ecomiq/storefront-definition'
40
-
41
- export interface DefinitionData {
42
- productsBySectionId?: Readonly<
43
- Record<string, readonly ProductSummary[] | undefined>
44
- >
45
- collectionsBySectionId?: Readonly<
46
- Record<string, readonly Collection[] | undefined>
47
- >
48
- }
49
-
50
- export interface SectionContext {
51
- data: DefinitionData
52
- locale: string
53
- currency: string
54
- assets: AssetManifest
55
- styles: readonly StorefrontNamedStyle[]
56
- }
57
-
58
- export type AssetManifest = Readonly<Record<string, string>>
59
-
60
- export function storefrontImageSrc(
61
- image: StorefrontImage | undefined,
62
- assets: AssetManifest,
63
- ): string | undefined {
64
- if (image === undefined) return undefined
65
- if (typeof image === 'string') return image
66
- return assets[image.assetId]
67
- }
68
-
69
- export function storefrontImageAlt(
70
- image: StorefrontImage | undefined,
71
- fallback: string,
72
- ): string {
73
- return typeof image === 'object' && image.alt ? image.alt : fallback
74
- }
75
-
76
- export interface SectionProps<
77
- TSection extends StorefrontSection,
78
- > {
79
- section: TSection
80
- context: SectionContext
81
- }
82
-
83
- export interface SectionRegistry {
84
- hero: (
85
- props: SectionProps<HeroStorefrontSection>,
86
- ) => ReactNode
87
- 'product-grid': (
88
- props: SectionProps<ProductGridStorefrontSection>,
89
- ) => ReactNode
90
- 'collection-grid': (
91
- props: SectionProps<CollectionGridStorefrontSection>,
92
- ) => ReactNode
93
- 'promo-banner': (
94
- props: SectionProps<PromoBannerStorefrontSection>,
95
- ) => ReactNode
96
- 'image-text': (
97
- props: SectionProps<ImageTextStorefrontSection>,
98
- ) => ReactNode
99
- 'feature-grid': (
100
- props: SectionProps<FeatureGridStorefrontSection>,
101
- ) => ReactNode
102
- faq: (props: SectionProps<FaqStorefrontSection>) => ReactNode
103
- stats: (props: SectionProps<StatsStorefrontSection>) => ReactNode
104
- 'logo-wall': (
105
- props: SectionProps<LogoWallStorefrontSection>,
106
- ) => ReactNode
107
- gallery: (props: SectionProps<GalleryStorefrontSection>) => ReactNode
108
- columns: (props: SectionProps<ColumnsStorefrontSection>) => ReactNode
109
- video: (props: SectionProps<VideoStorefrontSection>) => ReactNode
110
- 'rich-text': (
111
- props: SectionProps<RichTextStorefrontSection>,
112
- ) => ReactNode
113
- testimonials: (
114
- props: SectionProps<TestimonialsStorefrontSection>,
115
- ) => ReactNode
116
- newsletter: (
117
- props: SectionProps<NewsletterStorefrontSection>,
118
- ) => ReactNode
119
- }
120
-
121
- export interface DefinitionSectionsProps {
122
- definition: StorefrontDefinition
123
- pageId?: string
124
- data?: DefinitionData
125
- locale?: string
126
- currency?: string
127
- registry?: SectionRegistry
128
- assets?: AssetManifest
129
- }
130
-
131
- export function storefrontPageFor(
132
- definition: StorefrontDefinition,
133
- pageId?: string,
134
- ): StorefrontPage | null {
135
- if (pageId === undefined) {
136
- return definition.pages.find((page) => page.kind === 'home') ?? null
137
- }
138
- return definition.pages.find((page) => page.id === pageId) ?? null
139
- }
140
-
141
- export interface DefinitionShellProps {
142
- definition: StorefrontDefinition
143
- children: ReactNode
144
- showChrome?: boolean
145
- className?: string
146
- }
147
-
148
- export interface DefinitionRendererProps extends DefinitionSectionsProps {
149
- showChrome?: boolean
150
- className?: string
151
- }
152
-
153
- type StorefrontThemeStyle = CSSProperties & {
154
- '--background': string
155
- '--foreground': string
156
- '--card': string
157
- '--card-foreground': string
158
- '--primary': string
159
- '--primary-foreground': string
160
- '--secondary': string
161
- '--secondary-foreground': string
162
- '--muted': string
163
- '--muted-foreground': string
164
- '--accent': string
165
- '--accent-foreground': string
166
- '--border': string
167
- '--input': string
168
- '--ring': string
169
- '--radius': string
170
- '--storefront-font-body': string
171
- '--storefront-font-heading': string
172
- '--storefront-section-padding': string
173
- }
174
-
175
- function fontStack(font: StorefrontFontFamily): string {
176
- if (font === 'editorial-serif') {
177
- return 'Iowan Old Style, Baskerville, Times New Roman, serif'
178
- }
179
- if (font === 'humanist-sans') {
180
- return 'Avenir Next, Avenir, Segoe UI, sans-serif'
181
- }
182
- return 'ui-sans-serif, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif'
183
- }
184
-
185
- function radiusValue(radius: StorefrontThemeTokens['radius']): string {
186
- if (radius === 'none') return '0rem'
187
- if (radius === 'small') return '0.375rem'
188
- if (radius === 'large') return '1rem'
189
- return '0.625rem'
190
- }
191
-
192
- function sectionPaddingValue(
193
- density: StorefrontThemeTokens['density'],
194
- ): string {
195
- if (density === 'compact') return '2.5rem'
196
- if (density === 'airy') return '5rem'
197
- return '4rem'
198
- }
199
-
200
- function themeStyle(theme: StorefrontThemeTokens): StorefrontThemeStyle {
201
- return {
202
- '--background': theme.colors.background,
203
- '--foreground': theme.colors.foreground,
204
- '--card': theme.colors.card,
205
- '--card-foreground': theme.colors.cardForeground,
206
- '--primary': theme.colors.primary,
207
- '--primary-foreground': theme.colors.primaryForeground,
208
- '--secondary': theme.colors.secondary,
209
- '--secondary-foreground': theme.colors.secondaryForeground,
210
- '--muted': theme.colors.muted,
211
- '--muted-foreground': theme.colors.mutedForeground,
212
- '--accent': theme.colors.accent,
213
- '--accent-foreground': theme.colors.accentForeground,
214
- '--border': theme.colors.border,
215
- '--input': theme.colors.border,
216
- '--ring': theme.colors.primary,
217
- '--radius': radiusValue(theme.radius),
218
- '--storefront-font-body': fontStack(theme.typography.body),
219
- '--storefront-font-heading': fontStack(theme.typography.heading),
220
- '--storefront-section-padding': sectionPaddingValue(theme.density),
221
- }
222
- }
223
-
224
- function sectionSurfaceClass(section: StorefrontSection): string {
225
- if (section.surface === 'muted') return 'bg-muted'
226
- if (section.surface === 'accent') {
227
- return 'bg-accent text-accent-foreground'
228
- }
229
- return 'bg-background'
230
- }
231
-
232
- const SPACE_VALUES: Record<StorefrontSpace, string> = {
233
- none: '0rem',
234
- xs: '1rem',
235
- sm: '2rem',
236
- md: '3.5rem',
237
- lg: '5.5rem',
238
- xl: '8rem',
239
- }
240
-
241
- const CONTENT_WIDTH_VALUES: Record<StorefrontContentWidth, string> = {
242
- narrow: '48rem',
243
- contained: '72rem',
244
- wide: '90rem',
245
- full: '100%',
246
- }
247
-
248
- const TEXT_STEPS: ReadonlyArray<readonly [string, number]> = [
249
- ['--text-sm', 0.875],
250
- ['--text-base', 1],
251
- ['--text-lg', 1.125],
252
- ['--text-xl', 1.25],
253
- ['--text-2xl', 1.5],
254
- ['--text-3xl', 1.875],
255
- ['--text-4xl', 2.25],
256
- ['--text-5xl', 3],
257
- ['--text-6xl', 3.75],
258
- ]
259
-
260
- const FONT_SCALE_VALUES = { sm: 0.875, md: 1, lg: 1.125, xl: 1.25 }
261
-
262
- /**
263
- * Superficie derivada del fondo de la seccion: en vez de dejar las tarjetas y
264
- * los bordes con el token del tema (blanco sobre un fondo oscuro), se acercan
265
- * al color del texto lo justo para seguir leyendose como una capa aparte.
266
- */
267
- function lift(background: string, amount = '8%'): string {
268
- return `color-mix(in oklab, ${background}, var(--foreground) ${amount})`
269
- }
270
-
271
- function readableOn(background: string): string {
272
- const value = background.slice(1)
273
- const channels = [0, 2, 4].map((offset) => {
274
- const channel = Number.parseInt(value.slice(offset, offset + 2), 16) / 255
275
- return channel <= 0.04045
276
- ? channel / 12.92
277
- : ((channel + 0.055) / 1.055) ** 2.4
278
- })
279
- const luminance =
280
- 0.2126 * (channels[0] ?? 0) +
281
- 0.7152 * (channels[1] ?? 0) +
282
- 0.0722 * (channels[2] ?? 0)
283
- return luminance > 0.36 ? '#0b0b0c' : '#ffffff'
284
- }
285
-
286
- function sectionFrameStyle(
287
- style: StorefrontSectionStyle,
288
- assets: AssetManifest,
289
- ): CSSProperties {
290
- const variables: Record<string, string> = {}
291
-
292
- const backgroundImageSrc = storefrontImageSrc(style.backgroundImage, assets)
293
- if (style.background) {
294
- variables['--background'] = style.background
295
- variables['--muted'] = style.background
296
- variables['--accent'] = style.background
297
- variables['--card'] = lift(style.background)
298
- variables['--secondary'] = lift(style.background)
299
- variables['--card-foreground'] = 'var(--foreground)'
300
- variables['--secondary-foreground'] = 'var(--foreground)'
301
- if (!style.border) variables['--border'] = lift(style.background, '18%')
302
- }
303
- if (backgroundImageSrc) {
304
- variables['--background'] = 'transparent'
305
- variables['--muted'] = 'transparent'
306
- variables['--accent'] = 'transparent'
307
- }
308
- if (style.text) {
309
- variables['--foreground'] = style.text
310
- variables['--accent-foreground'] = style.text
311
- variables['--muted-foreground'] =
312
- `color-mix(in oklab, ${style.text} 68%, transparent)`
313
- }
314
- if (style.accent) {
315
- variables['--primary'] = style.accent
316
- variables['--primary-foreground'] = readableOn(style.accent)
317
- variables['--ring'] = style.accent
318
- }
319
- if (style.border) {
320
- variables['--border'] = style.border
321
- variables['--input'] = style.border
322
- }
323
- if (style.radius) variables['--radius'] = radiusValue(style.radius)
324
- if (style.paddingTop) {
325
- variables['--storefront-section-padding-top'] = SPACE_VALUES[style.paddingTop]
326
- }
327
- if (style.paddingBottom) {
328
- variables['--storefront-section-padding-bottom'] =
329
- SPACE_VALUES[style.paddingBottom]
330
- }
331
- if (style.width) {
332
- variables['--storefront-content-width'] = CONTENT_WIDTH_VALUES[style.width]
333
- }
334
- if (style.fontScale && style.fontScale !== 'md') {
335
- const factor = FONT_SCALE_VALUES[style.fontScale]
336
- for (const [token, rem] of TEXT_STEPS) {
337
- variables[token] = `${(rem * factor).toFixed(4)}rem`
338
- }
339
- }
340
- if (backgroundImageSrc) {
341
- variables.backgroundImage = `url("${backgroundImageSrc}")`
342
- variables.backgroundSize = 'cover'
343
- variables.backgroundPosition = 'center'
344
- }
345
-
346
- return variables as CSSProperties
347
- }
348
-
349
- function sectionFrameClass(
350
- style: StorefrontSectionStyle,
351
- ): string | undefined {
352
- if (style.visibility === 'desktop') return 'hidden md:block'
353
- if (style.visibility === 'mobile') return 'md:hidden'
354
- return undefined
355
- }
356
-
357
- export function storefrontPageCss(
358
- page: StorefrontPage,
359
- styles: readonly StorefrontNamedStyle[],
360
- ): string {
361
- return page.sections
362
- .filter((section) => section.enabled)
363
- .map((section) => {
364
- const css = resolveStorefrontSectionStyle(section, styles).css
365
- return css
366
- ? scopeStorefrontCss(css, storefrontSectionCssScope(section.id))
367
- : ''
368
- })
369
- .filter((css) => css.length > 0)
370
- .join('\n')
371
- }
372
-
373
- function StorefrontCustomStyle({ css }: { css: string }) {
374
- if (css.length === 0) return null
375
- return <style dangerouslySetInnerHTML={{ __html: css }} />
376
- }
377
-
378
- function StorefrontActionLink({
379
- action,
380
- variant = 'default',
381
- }: {
382
- action: StorefrontAction
383
- variant?: 'default' | 'secondary' | 'outline'
384
- }) {
385
- const className = buttonVariants({ variant, size: 'lg' })
386
- const isExternal =
387
- action.href.startsWith('https://') ||
388
- action.href.startsWith('mailto:') ||
389
- action.href.startsWith('tel:')
390
-
391
- const opensNewWindow = action.href.startsWith('https://')
392
- return (
393
- <a
394
- href={action.href}
395
- className={className}
396
- rel={isExternal && opensNewWindow ? 'noreferrer' : undefined}
397
- target={isExternal && opensNewWindow ? '_blank' : undefined}
398
- >
399
- {action.label}
400
- </a>
401
- )
402
- }
403
-
404
- function StorefrontNavigationLink({
405
- item,
406
- }: {
407
- item: StorefrontNavigationItem
408
- }) {
409
- const className =
410
- 'flex min-h-10 items-center px-2 text-sm text-muted-foreground transition-colors [@media(hover:hover)]:hover:text-foreground'
411
- const opensNewWindow = item.external || item.href.startsWith('https://')
412
- const isExternal =
413
- opensNewWindow ||
414
- item.href.startsWith('mailto:') ||
415
- item.href.startsWith('tel:')
416
-
417
- return (
418
- <a
419
- href={item.href}
420
- className={className}
421
- rel={isExternal && opensNewWindow ? 'noreferrer' : undefined}
422
- target={isExternal && opensNewWindow ? '_blank' : undefined}
423
- >
424
- {item.label}
425
- </a>
426
- )
427
- }
428
-
429
- function HeroSectionRenderer({
430
- section,
431
- context,
432
- }: SectionProps<HeroStorefrontSection>) {
433
- const isCentered = section.align === 'center'
434
- const heroImageSrc = storefrontImageSrc(section.image, context.assets)
435
- return (
436
- <section
437
- className={cn(
438
- 'storefront-definition-section relative isolate min-h-[min(72vh,680px)] overflow-hidden',
439
- sectionSurfaceClass(section),
440
- )}
441
- aria-labelledby={`${section.id}-title`}
442
- data-storefront-section={section.id}
443
- >
444
- {heroImageSrc ? (
445
- <>
446
- <img
447
- src={heroImageSrc}
448
- alt={storefrontImageAlt(section.image, section.imageAlt)}
449
- width={1600}
450
- height={900}
451
- fetchPriority="high"
452
- className="absolute inset-0 -z-20 h-full w-full object-cover"
453
- />
454
- <div className="absolute inset-0 -z-10 bg-black/55" aria-hidden />
455
- </>
456
- ) : null}
457
- <div
458
- className={cn(
459
- 'mx-auto flex min-h-[inherit] w-full max-w-(--storefront-content-width) items-center px-4',
460
- isCentered && 'justify-center text-center',
461
- )}
462
- >
463
- <div
464
- className={cn(
465
- 'max-w-2xl',
466
- section.image && 'text-white',
467
- isCentered && 'flex flex-col items-center',
468
- )}
469
- >
470
- {section.eyebrow ? (
471
- <p className="mb-4 text-xs font-semibold uppercase tracking-[0.16em] opacity-75" data-storefront-text="eyebrow">
472
- {section.eyebrow}
473
- </p>
474
- ) : null}
475
- <h1
476
- id={`${section.id}-title`}
477
- data-storefront-text="title"
478
- className="text-balance text-4xl font-semibold leading-[1.02] tracking-[-0.022em] sm:text-5xl lg:text-6xl"
479
- >
480
- {section.title}
481
- </h1>
482
- {section.description ? (
483
- <p className="mt-5 max-w-[65ch] text-pretty text-base leading-relaxed opacity-80 sm:text-lg" data-storefront-text="description">
484
- {section.description}
485
- </p>
486
- ) : null}
487
- {section.primaryAction || section.secondaryAction ? (
488
- <div className="mt-8 flex flex-wrap gap-3">
489
- {section.primaryAction ? (
490
- <StorefrontActionLink action={section.primaryAction} />
491
- ) : null}
492
- {section.secondaryAction ? (
493
- <StorefrontActionLink
494
- action={section.secondaryAction}
495
- variant="outline"
496
- />
497
- ) : null}
498
- </div>
499
- ) : null}
500
- </div>
501
- </div>
502
- </section>
503
- )
504
- }
505
-
506
- function gridColumnsClass(columns: 2 | 3 | 4): string {
507
- if (columns === 2) return 'sm:grid-cols-2'
508
- if (columns === 3) return 'sm:grid-cols-2 lg:grid-cols-3'
509
- return 'sm:grid-cols-2 lg:grid-cols-4'
510
- }
511
-
512
- function SectionHeading({
513
- id,
514
- title,
515
- description,
516
- }: {
517
- id: string
518
- title: string
519
- description?: string
520
- }) {
521
- return (
522
- <header className="mb-8 max-w-2xl">
523
- <h2
524
- id={id}
525
- data-storefront-text="title"
526
- className="text-balance text-3xl font-semibold tracking-[-0.022em]"
527
- >
528
- {title}
529
- </h2>
530
- {description ? (
531
- <p className="mt-3 text-pretty leading-relaxed text-muted-foreground" data-storefront-text="description">
532
- {description}
533
- </p>
534
- ) : null}
535
- </header>
536
- )
537
- }
538
-
539
- function ProductGridSectionRenderer({
540
- section,
541
- context,
542
- }: SectionProps<ProductGridStorefrontSection>) {
543
- const products = (context.data.productsBySectionId?.[section.id] ?? []).slice(
544
- 0,
545
- section.limit,
546
- )
547
- const formatter = new Intl.NumberFormat(context.locale, {
548
- style: 'currency',
549
- currency: context.currency,
550
- })
551
-
552
- return (
553
- <section
554
- className={cn(
555
- 'storefront-definition-section',
556
- sectionSurfaceClass(section),
557
- )}
558
- aria-labelledby={`${section.id}-title`}
559
- data-storefront-section={section.id}
560
- >
561
- <div className="mx-auto w-full max-w-(--storefront-content-width) px-4">
562
- <SectionHeading
563
- id={`${section.id}-title`}
564
- title={section.title}
565
- description={section.description}
566
- />
567
- {products.length > 0 ? (
568
- <div
569
- className={cn(
570
- 'grid grid-cols-2 gap-x-4 gap-y-8',
571
- gridColumnsClass(section.columns),
572
- )}
573
- >
574
- {products.map((product) => (
575
- <a
576
- key={product.id}
577
- href={`/products/${product.slug}`}
578
- className="group rounded-lg focus-visible:ring-2 focus-visible:ring-ring"
579
- >
580
- <div className="aspect-square overflow-hidden rounded-lg bg-muted outline outline-1 -outline-offset-1 outline-black/10">
581
- {product.imageUrl ? (
582
- <img
583
- src={product.imageUrl}
584
- alt={product.name}
585
- width={640}
586
- height={640}
587
- loading="lazy"
588
- className="h-full w-full object-cover transition-transform duration-300 ease-out [@media(hover:hover)]:group-hover:scale-[1.025] motion-reduce:transition-none"
589
- />
590
- ) : (
591
- <div className="flex h-full items-center justify-center px-4 text-center text-xs text-muted-foreground">
592
- Imagen no disponible
593
- </div>
594
- )}
595
- </div>
596
- <div className="pt-3">
597
- {product.brand ? (
598
- <p className="text-xs text-muted-foreground">
599
- {product.brand}
600
- </p>
601
- ) : null}
602
- <h3 className="mt-1 line-clamp-2 text-pretty text-sm font-medium leading-snug">
603
- {product.name}
604
- </h3>
605
- <p className="mt-1 text-sm font-semibold tabular-nums">
606
- {formatter.format(product.price)}
607
- </p>
608
- </div>
609
- </a>
610
- ))}
611
- </div>
612
- ) : (
613
- <p className="border-t py-6 text-sm text-muted-foreground">
614
- Aún no hay productos para mostrar.
615
- </p>
616
- )}
617
- </div>
618
- </section>
619
- )
620
- }
621
-
622
- function CollectionGridSectionRenderer({
623
- section,
624
- context,
625
- }: SectionProps<CollectionGridStorefrontSection>) {
626
- const collections = (
627
- context.data.collectionsBySectionId?.[section.id] ?? []
628
- ).slice(0, section.limit)
629
-
630
- return (
631
- <section
632
- className={cn(
633
- 'storefront-definition-section',
634
- sectionSurfaceClass(section),
635
- )}
636
- aria-labelledby={`${section.id}-title`}
637
- data-storefront-section={section.id}
638
- >
639
- <div className="mx-auto w-full max-w-(--storefront-content-width) px-4">
640
- <SectionHeading
641
- id={`${section.id}-title`}
642
- title={section.title}
643
- description={section.description}
644
- />
645
- {collections.length > 0 ? (
646
- <div className={cn('grid gap-4', gridColumnsClass(section.columns))}>
647
- {collections.map((collection) => (
648
- <a
649
- key={collection.id}
650
- href={`/collections/${collection.slug}`}
651
- className="group relative min-h-72 overflow-hidden rounded-lg bg-muted focus-visible:ring-2 focus-visible:ring-ring"
652
- >
653
- {collection.imageUrl ? (
654
- <img
655
- src={collection.imageUrl}
656
- alt={collection.name}
657
- width={960}
658
- height={720}
659
- loading="lazy"
660
- className="absolute inset-0 h-full w-full object-cover transition-transform duration-500 ease-out [@media(hover:hover)]:group-hover:scale-105 motion-reduce:transition-none"
661
- />
662
- ) : null}
663
- <div className="absolute inset-0 bg-black/55" aria-hidden />
664
- <div className="absolute inset-x-0 bottom-0 p-5 text-white">
665
- <h3 className="text-balance text-xl font-semibold tracking-[-0.012em]">
666
- {collection.name}
667
- </h3>
668
- {collection.description ? (
669
- <p className="mt-1 line-clamp-2 text-pretty text-sm text-white/80">
670
- {collection.description}
671
- </p>
672
- ) : null}
673
- </div>
674
- </a>
675
- ))}
676
- </div>
677
- ) : (
678
- <p className="border-t py-6 text-sm text-muted-foreground">
679
- Aún no hay colecciones para mostrar.
680
- </p>
681
- )}
682
- </div>
683
- </section>
684
- )
685
- }
686
-
687
- function PromoBannerSectionRenderer({
688
- section,
689
- }: SectionProps<PromoBannerStorefrontSection>) {
690
- const isPrimary = section.tone === 'primary'
691
- return (
692
- <section
693
- className={cn(
694
- 'storefront-definition-section',
695
- isPrimary
696
- ? 'bg-primary text-primary-foreground'
697
- : sectionSurfaceClass(section),
698
- )}
699
- aria-labelledby={`${section.id}-title`}
700
- data-storefront-section={section.id}
701
- >
702
- <div className="mx-auto flex w-full max-w-(--storefront-content-width) flex-col gap-6 px-4 sm:flex-row sm:items-end sm:justify-between">
703
- <div className="max-w-3xl">
704
- {section.eyebrow ? (
705
- <p className="mb-3 text-xs font-semibold uppercase tracking-[0.16em] opacity-75" data-storefront-text="eyebrow">
706
- {section.eyebrow}
707
- </p>
708
- ) : null}
709
- <h2
710
- id={`${section.id}-title`}
711
- data-storefront-text="title"
712
- className="text-balance text-3xl font-semibold tracking-[-0.022em]"
713
- >
714
- {section.title}
715
- </h2>
716
- {section.description ? (
717
- <p className="mt-3 max-w-[65ch] text-pretty leading-relaxed opacity-80" data-storefront-text="description">
718
- {section.description}
719
- </p>
720
- ) : null}
721
- </div>
722
- {section.action ? (
723
- <StorefrontActionLink
724
- action={section.action}
725
- variant={isPrimary ? 'secondary' : 'default'}
726
- />
727
- ) : null}
728
- </div>
729
- </section>
730
- )
731
- }
732
-
733
- function ImageTextSectionRenderer({
734
- section,
735
- context,
736
- }: SectionProps<ImageTextStorefrontSection>) {
737
- const imageFirst = section.imagePosition === 'left'
738
- const imageSrc = storefrontImageSrc(section.image, context.assets)
739
- return (
740
- <section
741
- className={cn(
742
- 'storefront-definition-section',
743
- sectionSurfaceClass(section),
744
- )}
745
- aria-labelledby={`${section.id}-title`}
746
- data-storefront-section={section.id}
747
- >
748
- <div className="mx-auto grid w-full max-w-(--storefront-content-width) items-center gap-8 px-4 md:grid-cols-2 md:gap-12">
749
- <img
750
- src={imageSrc}
751
- alt={storefrontImageAlt(section.image, section.imageAlt)}
752
- width={960}
753
- height={720}
754
- loading="lazy"
755
- className={cn(
756
- 'aspect-[4/3] h-full w-full rounded-lg object-cover outline outline-1 -outline-offset-1 outline-black/10',
757
- !imageFirst && 'md:order-2',
758
- )}
759
- />
760
- <div className={cn(!imageFirst && 'md:order-1')}>
761
- {section.eyebrow ? (
762
- <p className="mb-3 text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground" data-storefront-text="eyebrow">
763
- {section.eyebrow}
764
- </p>
765
- ) : null}
766
- <h2
767
- id={`${section.id}-title`}
768
- data-storefront-text="title"
769
- className="text-balance text-3xl font-semibold tracking-[-0.022em]"
770
- >
771
- {section.title}
772
- </h2>
773
- <p className="mt-4 max-w-[65ch] whitespace-pre-line text-pretty leading-relaxed text-muted-foreground" data-storefront-text="body">
774
- {section.body}
775
- </p>
776
- {section.action ? (
777
- <div className="mt-7">
778
- <StorefrontActionLink action={section.action} />
779
- </div>
780
- ) : null}
781
- </div>
782
- </div>
783
- </section>
784
- )
785
- }
786
-
787
- function RichTextSectionRenderer({
788
- section,
789
- }: SectionProps<RichTextStorefrontSection>) {
790
- const isCentered = section.align === 'center'
791
- return (
792
- <section
793
- className={cn(
794
- 'storefront-definition-section',
795
- sectionSurfaceClass(section),
796
- )}
797
- aria-labelledby={section.title ? `${section.id}-title` : undefined}
798
- data-storefront-section={section.id}
799
- >
800
- <div
801
- className={cn(
802
- 'mx-auto w-full max-w-3xl px-4',
803
- isCentered && 'text-center',
804
- )}
805
- >
806
- {section.title ? (
807
- <h2
808
- id={`${section.id}-title`}
809
- data-storefront-text="title"
810
- className="text-balance text-3xl font-semibold tracking-[-0.022em]"
811
- >
812
- {section.title}
813
- </h2>
814
- ) : null}
815
- <div className={cn('space-y-4', section.title && 'mt-5')}>
816
- {section.paragraphs.map((paragraph, index) => (
817
- <p
818
- key={`${section.id}-paragraph-${index}`}
819
- className="whitespace-pre-line text-pretty leading-7 text-muted-foreground"
820
- >
821
- {paragraph}
822
- </p>
823
- ))}
824
- </div>
825
- </div>
826
- </section>
827
- )
828
- }
829
-
830
- function testimonialColumnsClass(columns: 1 | 2 | 3): string {
831
- if (columns === 1) return 'grid-cols-1'
832
- if (columns === 2) return 'md:grid-cols-2'
833
- return 'md:grid-cols-2 lg:grid-cols-3'
834
- }
835
-
836
- function TestimonialsSectionRenderer({
837
- section,
838
- }: SectionProps<TestimonialsStorefrontSection>) {
839
- return (
840
- <section
841
- className={cn(
842
- 'storefront-definition-section',
843
- sectionSurfaceClass(section),
844
- )}
845
- aria-labelledby={`${section.id}-title`}
846
- data-storefront-section={section.id}
847
- >
848
- <div className="mx-auto w-full max-w-(--storefront-content-width) px-4">
849
- <SectionHeading
850
- id={`${section.id}-title`}
851
- title={section.title}
852
- description={section.description}
853
- />
854
- <div
855
- className={cn('grid gap-5', testimonialColumnsClass(section.columns))}
856
- >
857
- {section.blocks.map((item) => (
858
- <figure
859
- key={item.id}
860
- className="flex h-full flex-col justify-between border-t pt-6 text-foreground"
861
- >
862
- <blockquote className="text-pretty text-lg leading-relaxed">
863
- “{item.quote}”
864
- </blockquote>
865
- <figcaption className="mt-8 text-sm">
866
- <span className="font-semibold">{item.author}</span>
867
- {item.role ? (
868
- <span className="mt-1 block text-muted-foreground">
869
- {item.role}
870
- </span>
871
- ) : null}
872
- </figcaption>
873
- </figure>
874
- ))}
875
- </div>
876
- </div>
877
- </section>
878
- )
879
- }
880
-
881
- function NewsletterSectionRenderer({
882
- section,
883
- }: SectionProps<NewsletterStorefrontSection>) {
884
- return (
885
- <section
886
- className={cn(
887
- 'storefront-definition-section',
888
- sectionSurfaceClass(section),
889
- )}
890
- aria-labelledby={`${section.id}-title`}
891
- data-storefront-section={section.id}
892
- >
893
- <div className="mx-auto grid w-full max-w-(--storefront-content-width) gap-7 px-4 md:grid-cols-[1fr_minmax(280px,440px)] md:items-end">
894
- <div className="max-w-2xl">
895
- {section.eyebrow ? (
896
- <p className="mb-3 text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground" data-storefront-text="eyebrow">
897
- {section.eyebrow}
898
- </p>
899
- ) : null}
900
- <h2
901
- id={`${section.id}-title`}
902
- data-storefront-text="title"
903
- className="text-balance text-3xl font-semibold tracking-[-0.022em]"
904
- >
905
- {section.title}
906
- </h2>
907
- {section.description ? (
908
- <p className="mt-3 text-pretty leading-relaxed text-muted-foreground" data-storefront-text="description">
909
- {section.description}
910
- </p>
911
- ) : null}
912
- </div>
913
- <form action={section.formAction} method="post" className="flex gap-2">
914
- <label htmlFor={`${section.id}-email`} className="sr-only">
915
- Correo electrónico
916
- </label>
917
- <input
918
- id={`${section.id}-email`}
919
- name="email"
920
- type="email"
921
- autoComplete="email"
922
- required
923
- placeholder={section.emailPlaceholder}
924
- className="min-h-10 min-w-0 flex-1 rounded-md border border-input bg-background px-3 text-sm text-foreground shadow-sm placeholder:text-muted-foreground"
925
- />
926
- <button
927
- type="submit"
928
- className={buttonVariants({ variant: 'default', size: 'lg' })}
929
- >
930
- {section.buttonLabel}
931
- </button>
932
- </form>
933
- </div>
934
- </section>
935
- )
936
- }
937
-
938
-
939
- function FeatureGridSectionRenderer({
940
- section,
941
- }: SectionProps<FeatureGridStorefrontSection>) {
942
- return (
943
- <section
944
- className={cn(
945
- 'storefront-definition-section',
946
- sectionSurfaceClass(section),
947
- )}
948
- data-storefront-section={section.id}
949
- >
950
- <div className="mx-auto w-full max-w-(--storefront-content-width) px-4">
951
- {section.title ? (
952
- <SectionHeading
953
- id={`${section.id}-title`}
954
- title={section.title}
955
- description={section.description}
956
- />
957
- ) : null}
958
- <div className={cn('grid gap-6', gridColumnsClass(section.columns))}>
959
- {section.blocks.map((block) => (
960
- <div key={block.id}>
961
- <h3
962
- className="text-base font-semibold"
963
- data-storefront-text="title"
964
- >
965
- {block.title}
966
- </h3>
967
- {block.description ? (
968
- <p
969
- className="mt-2 text-pretty text-sm leading-relaxed text-muted-foreground"
970
- data-storefront-text="description"
971
- >
972
- {block.description}
973
- </p>
974
- ) : null}
975
- </div>
976
- ))}
977
- </div>
978
- </div>
979
- </section>
980
- )
981
- }
982
-
983
- function FaqSectionRenderer({ section }: SectionProps<FaqStorefrontSection>) {
984
- return (
985
- <section
986
- className={cn(
987
- 'storefront-definition-section',
988
- sectionSurfaceClass(section),
989
- )}
990
- aria-labelledby={`${section.id}-title`}
991
- data-storefront-section={section.id}
992
- >
993
- <div className="mx-auto w-full max-w-3xl px-4">
994
- <SectionHeading
995
- id={`${section.id}-title`}
996
- title={section.title}
997
- description={section.description}
998
- />
999
- <dl className="divide-y border-t">
1000
- {section.blocks.map((block) => (
1001
- <div key={block.id} className="py-5">
1002
- <dt className="text-base font-semibold">{block.question}</dt>
1003
- <dd className="mt-2 whitespace-pre-line text-pretty leading-relaxed text-muted-foreground">
1004
- {block.answer}
1005
- </dd>
1006
- </div>
1007
- ))}
1008
- </dl>
1009
- </div>
1010
- </section>
1011
- )
1012
- }
1013
-
1014
- function StatsSectionRenderer({
1015
- section,
1016
- }: SectionProps<StatsStorefrontSection>) {
1017
- return (
1018
- <section
1019
- className={cn(
1020
- 'storefront-definition-section',
1021
- sectionSurfaceClass(section),
1022
- )}
1023
- data-storefront-section={section.id}
1024
- >
1025
- <div className="mx-auto w-full max-w-(--storefront-content-width) px-4">
1026
- {section.title ? (
1027
- <SectionHeading id={`${section.id}-title`} title={section.title} />
1028
- ) : null}
1029
- <dl className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
1030
- {section.blocks.map((block) => (
1031
- <div key={block.id}>
1032
- <dt className="text-3xl font-semibold tracking-[-0.02em]">
1033
- {block.value}
1034
- </dt>
1035
- <dd className="mt-1 text-sm text-muted-foreground">
1036
- {block.label}
1037
- </dd>
1038
- </div>
1039
- ))}
1040
- </dl>
1041
- </div>
1042
- </section>
1043
- )
1044
- }
1045
-
1046
- function LogoWallSectionRenderer({
1047
- section,
1048
- context,
1049
- }: SectionProps<LogoWallStorefrontSection>) {
1050
- return (
1051
- <section
1052
- className={cn(
1053
- 'storefront-definition-section',
1054
- sectionSurfaceClass(section),
1055
- )}
1056
- data-storefront-section={section.id}
1057
- >
1058
- <div className="mx-auto w-full max-w-(--storefront-content-width) px-4">
1059
- {section.title ? (
1060
- <SectionHeading id={`${section.id}-title`} title={section.title} />
1061
- ) : null}
1062
- <ul className="flex flex-wrap items-center justify-center gap-x-10 gap-y-6">
1063
- {section.blocks.map((block) => {
1064
- const src = storefrontImageSrc(block.image, context.assets)
1065
- if (!src) return null
1066
- return (
1067
- <li key={block.id}>
1068
- <img
1069
- alt={block.alt}
1070
- className="h-8 w-auto opacity-70 transition-opacity hover:opacity-100"
1071
- loading="lazy"
1072
- src={src}
1073
- />
1074
- </li>
1075
- )
1076
- })}
1077
- </ul>
1078
- </div>
1079
- </section>
1080
- )
1081
- }
1082
-
1083
- function GallerySectionRenderer({
1084
- section,
1085
- context,
1086
- }: SectionProps<GalleryStorefrontSection>) {
1087
- return (
1088
- <section
1089
- className={cn(
1090
- 'storefront-definition-section',
1091
- sectionSurfaceClass(section),
1092
- )}
1093
- data-storefront-section={section.id}
1094
- >
1095
- <div className="mx-auto w-full max-w-(--storefront-content-width) px-4">
1096
- {section.title ? (
1097
- <SectionHeading
1098
- id={`${section.id}-title`}
1099
- title={section.title}
1100
- description={section.description}
1101
- />
1102
- ) : null}
1103
- <div className={cn('grid gap-3', gridColumnsClass(section.columns))}>
1104
- {section.blocks.map((block) => {
1105
- const src = storefrontImageSrc(block.image, context.assets)
1106
- if (!src) return null
1107
- return (
1108
- <figure key={block.id}>
1109
- <img
1110
- alt={block.alt}
1111
- className="aspect-[4/3] w-full rounded-md object-cover"
1112
- loading="lazy"
1113
- src={src}
1114
- />
1115
- {block.caption ? (
1116
- <figcaption className="mt-2 text-xs text-muted-foreground">
1117
- {block.caption}
1118
- </figcaption>
1119
- ) : null}
1120
- </figure>
1121
- )
1122
- })}
1123
- </div>
1124
- </div>
1125
- </section>
1126
- )
1127
- }
1128
-
1129
- function ColumnsSectionRenderer({
1130
- section,
1131
- context,
1132
- }: SectionProps<ColumnsStorefrontSection>) {
1133
- const columns = Math.min(Math.max(section.blocks.length, 2), 4) as 2 | 3 | 4
1134
- return (
1135
- <section
1136
- className={cn(
1137
- 'storefront-definition-section',
1138
- sectionSurfaceClass(section),
1139
- )}
1140
- data-storefront-section={section.id}
1141
- >
1142
- <div className="mx-auto w-full max-w-(--storefront-content-width) px-4">
1143
- {section.title ? (
1144
- <SectionHeading
1145
- id={`${section.id}-title`}
1146
- title={section.title}
1147
- description={section.description}
1148
- />
1149
- ) : null}
1150
- <div className={cn('grid gap-8', gridColumnsClass(columns))}>
1151
- {section.blocks.map((block) => {
1152
- const src = storefrontImageSrc(block.image, context.assets)
1153
- return (
1154
- <div key={block.id}>
1155
- {src ? (
1156
- <img
1157
- alt={block.imageAlt ?? ''}
1158
- className="mb-4 aspect-[4/3] w-full rounded-md object-cover"
1159
- loading="lazy"
1160
- src={src}
1161
- />
1162
- ) : null}
1163
- <h3
1164
- className="text-lg font-semibold"
1165
- data-storefront-text="title"
1166
- >
1167
- {block.title}
1168
- </h3>
1169
- {block.body ? (
1170
- <p className="mt-2 text-pretty leading-relaxed text-muted-foreground">
1171
- {block.body}
1172
- </p>
1173
- ) : null}
1174
- {block.action ? (
1175
- <div className="mt-4">
1176
- <StorefrontActionLink action={block.action} />
1177
- </div>
1178
- ) : null}
1179
- </div>
1180
- )
1181
- })}
1182
- </div>
1183
- </div>
1184
- </section>
1185
- )
1186
- }
1187
-
1188
- function VideoSectionRenderer({
1189
- section,
1190
- context,
1191
- }: SectionProps<VideoStorefrontSection>) {
1192
- const poster = storefrontImageSrc(section.poster, context.assets)
1193
- return (
1194
- <section
1195
- className={cn(
1196
- 'storefront-definition-section',
1197
- sectionSurfaceClass(section),
1198
- )}
1199
- data-storefront-section={section.id}
1200
- >
1201
- <div className="mx-auto w-full max-w-4xl px-4">
1202
- {section.title ? (
1203
- <SectionHeading
1204
- id={`${section.id}-title`}
1205
- title={section.title}
1206
- description={section.description}
1207
- />
1208
- ) : null}
1209
- <div className="overflow-hidden rounded-lg border bg-muted">
1210
- <video
1211
- className="aspect-video w-full"
1212
- controls
1213
- preload="none"
1214
- {...(poster ? { poster } : {})}
1215
- src={section.url}
1216
- >
1217
- {section.posterAlt ?? ''}
1218
- </video>
1219
- </div>
1220
- </div>
1221
- </section>
1222
- )
1223
- }
1224
-
1225
- export const defaultStorefrontSectionRegistry: SectionRegistry = {
1226
- hero: HeroSectionRenderer,
1227
- 'product-grid': ProductGridSectionRenderer,
1228
- 'collection-grid': CollectionGridSectionRenderer,
1229
- 'promo-banner': PromoBannerSectionRenderer,
1230
- 'image-text': ImageTextSectionRenderer,
1231
- 'rich-text': RichTextSectionRenderer,
1232
- testimonials: TestimonialsSectionRenderer,
1233
- newsletter: NewsletterSectionRenderer,
1234
- 'feature-grid': FeatureGridSectionRenderer,
1235
- faq: FaqSectionRenderer,
1236
- stats: StatsSectionRenderer,
1237
- 'logo-wall': LogoWallSectionRenderer,
1238
- gallery: GallerySectionRenderer,
1239
- columns: ColumnsSectionRenderer,
1240
- video: VideoSectionRenderer,
1241
- }
1242
-
1243
- function StorefrontSectionFrame({
1244
- section,
1245
- context,
1246
- children,
1247
- }: {
1248
- section: StorefrontSection
1249
- context: SectionContext
1250
- children: ReactNode
1251
- }) {
1252
- const resolved = resolveStorefrontSectionStyle(section, context.styles)
1253
- const style = sectionFrameStyle(resolved, context.assets)
1254
- const className = sectionFrameClass(resolved)
1255
- const overlay = resolved.backgroundOverlay
1256
- const hasFrame =
1257
- Object.keys(style).length > 0 || className !== undefined || Boolean(overlay)
1258
- if (!hasFrame) return children
1259
-
1260
- return (
1261
- <div
1262
- className={cn(
1263
- 'storefront-definition-frame text-foreground',
1264
- Boolean(resolved.backgroundImage) && 'relative isolate',
1265
- className,
1266
- )}
1267
- style={style}
1268
- data-storefront-section-frame={section.id}
1269
- >
1270
- {overlay ? (
1271
- <div
1272
- aria-hidden
1273
- className="absolute inset-0 -z-10 bg-black"
1274
- style={{ opacity: overlay }}
1275
- />
1276
- ) : null}
1277
- {children}
1278
- </div>
1279
- )
1280
- }
1281
-
1282
- export function renderStorefrontSection(
1283
- section: StorefrontSection,
1284
- registry: SectionRegistry,
1285
- context: SectionContext,
1286
- ): ReactNode {
1287
- return (
1288
- <StorefrontSectionFrame section={section} context={context}>
1289
- {renderStorefrontSectionBody(section, registry, context)}
1290
- </StorefrontSectionFrame>
1291
- )
1292
- }
1293
-
1294
- function renderStorefrontSectionBody(
1295
- section: StorefrontSection,
1296
- registry: SectionRegistry,
1297
- context: SectionContext,
1298
- ): ReactNode {
1299
- if (section.type === 'hero') return registry.hero({ section, context })
1300
- if (section.type === 'product-grid') {
1301
- return registry['product-grid']({ section, context })
1302
- }
1303
- if (section.type === 'collection-grid') {
1304
- return registry['collection-grid']({ section, context })
1305
- }
1306
- if (section.type === 'promo-banner') {
1307
- return registry['promo-banner']({ section, context })
1308
- }
1309
- if (section.type === 'image-text') {
1310
- return registry['image-text']({ section, context })
1311
- }
1312
- if (section.type === 'rich-text') {
1313
- return registry['rich-text']({ section, context })
1314
- }
1315
- if (section.type === 'testimonials') {
1316
- return registry.testimonials({ section, context })
1317
- }
1318
- if (section.type === 'feature-grid') {
1319
- return registry['feature-grid']({ section, context })
1320
- }
1321
- if (section.type === 'faq') {
1322
- return registry.faq({ section, context })
1323
- }
1324
- if (section.type === 'stats') {
1325
- return registry.stats({ section, context })
1326
- }
1327
- if (section.type === 'logo-wall') {
1328
- return registry['logo-wall']({ section, context })
1329
- }
1330
- if (section.type === 'gallery') {
1331
- return registry.gallery({ section, context })
1332
- }
1333
- if (section.type === 'columns') {
1334
- return registry.columns({ section, context })
1335
- }
1336
- if (section.type === 'video') {
1337
- return registry.video({ section, context })
1338
- }
1339
- return registry.newsletter({ section, context })
1340
- }
1341
-
1342
- function StorefrontChrome({
1343
- definition,
1344
- children,
1345
- }: {
1346
- definition: StorefrontDefinition
1347
- children: ReactNode
1348
- }) {
1349
- return (
1350
- <>
1351
- <a
1352
- href="#storefront-definition-main"
1353
- className="sr-only z-100 rounded-md bg-background px-3 py-2 text-sm focus:not-sr-only focus:fixed focus:left-3 focus:top-3"
1354
- >
1355
- Ir al contenido
1356
- </a>
1357
- {definition.site.navigation.items.length > 0 ? (
1358
- <header className="border-b bg-background" data-storefront-chrome="navigation">
1359
- <nav
1360
- className="mx-auto flex min-h-14 w-full max-w-(--storefront-content-width) flex-wrap items-center gap-x-4 px-4"
1361
- aria-label="Navegación principal"
1362
- >
1363
- {definition.site.navigation.items.map((item) => (
1364
- <StorefrontNavigationLink key={item.id} item={item} />
1365
- ))}
1366
- </nav>
1367
- </header>
1368
- ) : null}
1369
- <main id="storefront-definition-main">{children}</main>
1370
- <footer className="border-t bg-muted/40" data-storefront-chrome="footer">
1371
- <div className="mx-auto flex w-full max-w-(--storefront-content-width) flex-col gap-4 px-4 py-8 text-sm text-muted-foreground sm:flex-row sm:items-center sm:justify-between">
1372
- {definition.site.footer.text ? <p>{definition.site.footer.text}</p> : null}
1373
- {definition.site.footer.items.length > 0 ? (
1374
- <nav
1375
- className="flex flex-wrap gap-x-3"
1376
- aria-label="Navegación del pie"
1377
- >
1378
- {definition.site.footer.items.map((item) => (
1379
- <StorefrontNavigationLink key={item.id} item={item} />
1380
- ))}
1381
- </nav>
1382
- ) : null}
1383
- </div>
1384
- </footer>
1385
- </>
1386
- )
1387
- }
1388
-
1389
- export function DefinitionSections({
1390
- definition,
1391
- pageId,
1392
- data = {},
1393
- locale = 'es-PE',
1394
- currency = 'PEN',
1395
- registry = defaultStorefrontSectionRegistry,
1396
- assets = {},
1397
- }: DefinitionSectionsProps) {
1398
- const context: SectionContext = {
1399
- data,
1400
- locale,
1401
- currency,
1402
- assets,
1403
- styles: definition.site.styles,
1404
- }
1405
-
1406
- const page = storefrontPageFor(definition, pageId)
1407
- if (!page) return null
1408
-
1409
- return (
1410
- <>
1411
- <StorefrontCustomStyle
1412
- css={storefrontPageCss(page, definition.site.styles)}
1413
- />
1414
- {page.sections
1415
- .filter((section) => section.enabled)
1416
- .map((section) => (
1417
- <Fragment key={section.id}>
1418
- {renderStorefrontSection(section, registry, context)}
1419
- </Fragment>
1420
- ))}
1421
- </>
1422
- )
1423
- }
1424
-
1425
- export function DefinitionShell({
1426
- definition,
1427
- children,
1428
- showChrome = true,
1429
- className,
1430
- }: DefinitionShellProps) {
1431
- const content = showChrome ? (
1432
- <StorefrontChrome definition={definition}>{children}</StorefrontChrome>
1433
- ) : (
1434
- children
1435
- )
1436
-
1437
- return (
1438
- <div
1439
- className={cn(
1440
- 'storefront-definition min-h-full bg-background text-foreground antialiased',
1441
- className,
1442
- )}
1443
- style={themeStyle(definition.site.theme)}
1444
- data-storefront-definition-version={definition.version}
1445
- data-storefront-definition-revision={definition.revision}
1446
- >
1447
- <StorefrontCustomStyle
1448
- css={scopeStorefrontCss(definition.site.css, STOREFRONT_SITE_CSS_SCOPE)}
1449
- />
1450
- {content}
1451
- </div>
1452
- )
1453
- }
1454
-
1455
- export function DefinitionRenderer({
1456
- definition,
1457
- pageId,
1458
- data,
1459
- locale,
1460
- currency,
1461
- registry,
1462
- showChrome = true,
1463
- className,
1464
- }: DefinitionRendererProps) {
1465
- return (
1466
- <DefinitionShell
1467
- definition={definition}
1468
- showChrome={showChrome}
1469
- className={className}
1470
- >
1471
- <DefinitionSections
1472
- definition={definition}
1473
- pageId={pageId}
1474
- data={data}
1475
- locale={locale}
1476
- currency={currency}
1477
- registry={registry}
1478
- />
1479
- </DefinitionShell>
1480
- )
1481
- }