@mohasinac/appkit 4.32.5 → 4.33.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 (83) hide show
  1. package/dist/_internal/server/features/products/actions.js +29 -3
  2. package/dist/_internal/server/features/products/list-public.d.ts +1 -1
  3. package/dist/_internal/server/features/products/list-public.js +10 -2
  4. package/dist/_internal/server/features/stores/denormalize.d.ts +44 -0
  5. package/dist/_internal/server/features/stores/denormalize.js +58 -0
  6. package/dist/_internal/server/features/stores/index.d.ts +1 -0
  7. package/dist/_internal/server/features/stores/index.js +1 -0
  8. package/dist/_internal/server/jobs/core/jobRunners.js +5 -0
  9. package/dist/_internal/server/jobs/core/storeNameBackfill.d.ts +27 -0
  10. package/dist/_internal/server/jobs/core/storeNameBackfill.js +90 -0
  11. package/dist/_internal/shared/features/products/to-product-item.js +7 -0
  12. package/dist/client.d.ts +1 -1
  13. package/dist/client.js +4 -1
  14. package/dist/features/admin/actions/admin-actions.js +11 -1
  15. package/dist/features/admin/components/AdminSectionsView.js +17 -20
  16. package/dist/features/admin/components/sections/adminSectionsBuildParse.js +31 -14
  17. package/dist/features/admin/components/sections/adminSectionsTypes.d.ts +37 -7
  18. package/dist/features/admin/components/sections/adminSectionsTypes.js +11 -7
  19. package/dist/features/auctions/components/AuctionDetailPageView.js +56 -36
  20. package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +9 -0
  21. package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -2
  22. package/dist/features/blog/components/BlogFeaturedCard.d.ts +7 -1
  23. package/dist/features/blog/components/BlogFeaturedCard.js +3 -3
  24. package/dist/features/cart/repository/cart.repository.d.ts +27 -6
  25. package/dist/features/cart/repository/cart.repository.js +52 -21
  26. package/dist/features/cart/utils/guest-cart.js +5 -0
  27. package/dist/features/cart/utils/pending-ops.d.ts +15 -0
  28. package/dist/features/cart/utils/pending-ops.js +24 -0
  29. package/dist/features/events/components/EventRafflesSection.js +2 -1
  30. package/dist/features/homepage/components/BlogArticlesSection.d.ts +7 -1
  31. package/dist/features/homepage/components/BlogArticlesSection.js +5 -3
  32. package/dist/features/homepage/components/CTABannerSection.d.ts +18 -1
  33. package/dist/features/homepage/components/CTABannerSection.js +22 -4
  34. package/dist/features/homepage/components/CustomerReviewsSection.d.ts +7 -1
  35. package/dist/features/homepage/components/CustomerReviewsSection.js +2 -2
  36. package/dist/features/homepage/components/FAQSection.js +2 -11
  37. package/dist/features/homepage/components/FeaturedAuctionsSection.d.ts +3 -1
  38. package/dist/features/homepage/components/FeaturedAuctionsSection.js +5 -3
  39. package/dist/features/homepage/components/FeaturedBundlesSection.d.ts +7 -1
  40. package/dist/features/homepage/components/FeaturedBundlesSection.js +6 -5
  41. package/dist/features/homepage/components/FeaturedPreOrdersSection.d.ts +3 -1
  42. package/dist/features/homepage/components/FeaturedPreOrdersSection.js +4 -2
  43. package/dist/features/homepage/components/FeaturedProductsSection.d.ts +3 -1
  44. package/dist/features/homepage/components/FeaturedProductsSection.js +5 -3
  45. package/dist/features/homepage/components/HeroCarousel.d.ts +23 -2
  46. package/dist/features/homepage/components/HeroCarousel.js +22 -9
  47. package/dist/features/homepage/components/HomepageCustomerReviewsSection.d.ts +6 -1
  48. package/dist/features/homepage/components/HomepageCustomerReviewsSection.js +11 -3
  49. package/dist/features/homepage/components/MarketplaceHomepageView.d.ts +9 -1
  50. package/dist/features/homepage/components/MarketplaceHomepageView.js +7 -4
  51. package/dist/features/homepage/components/StatsCounterSection.d.ts +6 -1
  52. package/dist/features/homepage/components/StatsCounterSection.js +3 -3
  53. package/dist/features/homepage/constants/faq-category-labels.d.ts +15 -0
  54. package/dist/features/homepage/constants/faq-category-labels.js +23 -0
  55. package/dist/features/homepage/constants/section-copy.d.ts +107 -0
  56. package/dist/features/homepage/constants/section-copy.js +96 -0
  57. package/dist/features/homepage/lib/section-renderer.d.ts +16 -1
  58. package/dist/features/homepage/lib/section-renderer.js +58 -37
  59. package/dist/features/homepage/repository/homepage-sections.repository.d.ts +23 -0
  60. package/dist/features/homepage/repository/homepage-sections.repository.js +40 -0
  61. package/dist/features/homepage/schemas/firestore.d.ts +120 -44
  62. package/dist/features/media/MediaImage.d.ts +10 -1
  63. package/dist/features/media/MediaImage.js +6 -3
  64. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -2
  65. package/dist/features/products/components/CardStoreLine.d.ts +32 -0
  66. package/dist/features/products/components/CardStoreLine.js +14 -0
  67. package/dist/features/products/components/MarketplacePrizeDrawCard.js +2 -1
  68. package/dist/features/products/components/ProductGrid.js +3 -6
  69. package/dist/features/products/schemas/firestore.d.ts +17 -0
  70. package/dist/features/products/types/index.d.ts +2 -0
  71. package/dist/features/seller/actions/seller-actions.d.ts +17 -0
  72. package/dist/features/seller/actions/seller-actions.js +54 -0
  73. package/dist/features/stores/components/StoreAuctionsListing.js +3 -0
  74. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +119 -0
  75. package/dist/seed/carousel-slides-seed-data.js +154 -22
  76. package/dist/seed/carousels-seed-data.js +4 -1
  77. package/dist/seed/homepage-sections-seed-data.js +337 -254
  78. package/dist/seed/products-preorders-seed-data.d.ts +1 -0
  79. package/dist/server.d.ts +1 -0
  80. package/dist/server.js +4 -0
  81. package/dist/styles.css +1 -1
  82. package/dist/tailwind-utilities.css +1 -1
  83. package/package.json +1 -1
@@ -1,7 +1,25 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import Link from "next/link";
3
- import { Div, Heading, Row, Section, Span, Text } from "../../../ui";
3
+ import { Div, Heading, Row, Scrim, Section, Span, Text } from "../../../ui";
4
+ import { MediaImage } from "../../media/MediaImage";
4
5
  import { ROUTES } from "../../../next/routing/route-map";
5
- export function CTABannerSection({ title = "Thousands of collectibles. One marketplace.", subtitle, primaryLabel = "Shop All Products →", primaryHref = String(ROUTES.PUBLIC.PRODUCTS), secondaryLabel = "Browse Auctions →", secondaryHref = String(ROUTES.PUBLIC.AUCTIONS), className = "", }) {
6
- return (_jsx(Section, { tone: "accent-banner", className: `${className}`, paddingY: "y-4xl", paddingX: "x-md", children: _jsxs(Div, { className: "mx-auto max-w-3xl text-left", children: [_jsx(Span, { className: "mb-4 inline-block", size: "2xl", "aria-hidden": "true", children: "\u2728" }), _jsx(Heading, { color: "inverse", level: 2, mdSize: "3xl", lgSize: "4xl", size: "2xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { color: "inverse", className: "mt-3 /80", size: "base", children: subtitle })), _jsxs(Row, { className: "mt-8", align: "center", justify: "start", gap: "md", wrap: true, children: [_jsx(Link, { href: primaryHref, className: "inline-flex items-center rounded-full bg-white px-[var(--appkit-space-8)] py-[var(--appkit-space-3)] text-[length:var(--appkit-text-sm)] font-semibold text-primary-700 shadow-sm transition-all hover:bg-surface-hover hover:shadow-md focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-primary-600", children: primaryLabel }), secondaryHref && (_jsx(Link, { href: secondaryHref, className: "inline-flex items-center rounded-full border-2 border-white/80 px-[var(--appkit-space-8)] py-[var(--appkit-space-3)] text-[length:var(--appkit-text-sm)] font-semibold text-white transition-all hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-primary-600", children: secondaryLabel }))] })] }) }));
6
+ import { SECTION_TITLE, SECTION_COPY } from "../constants/section-copy";
7
+ /**
8
+ * `height` maps onto the existing spacing scale rather than emitting a
9
+ * `min-h-[300px]`-style utility.
10
+ *
11
+ * Two reasons. A class generated only inside appkit and absent from the
12
+ * consumer's `src/**` is purged from the production CSS (Root Cause #3), and
13
+ * `BANNER_HEIGHTS`' raw px values would be exactly that. And padding keeps the
14
+ * banner sized by its content, so a long headline grows the box instead of
15
+ * overflowing a fixed height.
16
+ */
17
+ const HEIGHT_PADDING_Y = {
18
+ sm: "y-2xl",
19
+ md: "y-4xl",
20
+ lg: "y-5xl",
21
+ xl: "y-6xl",
22
+ };
23
+ export function CTABannerSection({ title = SECTION_TITLE.banner, subtitle, description, primaryLabel = SECTION_COPY.bannerPrimaryCta, primaryHref = String(ROUTES.PUBLIC.PRODUCTS), secondaryLabel = SECTION_COPY.bannerSecondaryCta, secondaryHref = String(ROUTES.PUBLIC.AUCTIONS), height = "md", backgroundImage, className = "", }) {
24
+ return (_jsxs(Section, { tone: "accent-banner", className: `relative ${className}`, overflow: "hidden", paddingY: HEIGHT_PADDING_Y[height], paddingX: "x-md", children: [backgroundImage && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "absolute inset-0", children: _jsx(MediaImage, { src: backgroundImage, alt: "", size: "banner", objectFit: "cover" }) }), _jsx(Scrim, { direction: "left-to-right", intensity: "strong", className: "absolute inset-0" })] })), _jsxs(Div, { className: "relative mx-auto max-w-3xl text-left", children: [_jsx(Span, { className: "mb-4 inline-block", size: "2xl", "aria-hidden": "true", children: "\u2728" }), _jsx(Heading, { color: "inverse", level: 2, mdSize: "3xl", lgSize: "4xl", size: "2xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { color: "inverse", className: "mt-3 /80", size: "base", children: subtitle })), description && (_jsx(Text, { color: "inverse", className: "mt-2 /70 max-w-2xl", size: "sm", children: description })), _jsxs(Row, { className: "mt-8", align: "center", justify: "start", gap: "md", wrap: true, children: [_jsx(Link, { href: primaryHref, className: "inline-flex items-center rounded-full bg-white px-[var(--appkit-space-8)] py-[var(--appkit-space-3)] text-[length:var(--appkit-text-sm)] font-semibold text-primary-700 shadow-sm transition-all hover:bg-surface-hover hover:shadow-md focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-primary-600", children: primaryLabel }), secondaryHref && (_jsx(Link, { href: secondaryHref, className: "inline-flex items-center rounded-full border-2 border-white/80 px-[var(--appkit-space-8)] py-[var(--appkit-space-3)] text-[length:var(--appkit-text-sm)] font-semibold text-white transition-all hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-primary-600", children: secondaryLabel }))] })] })] }));
7
25
  }
@@ -12,5 +12,11 @@ export interface CustomerReviewsSectionProps<T = unknown> {
12
12
  autoScroll?: boolean;
13
13
  scrollInterval?: number;
14
14
  loop?: boolean;
15
+ /**
16
+ * Responsive cards-per-view. Defaults to the shared `reviews` preset; the
17
+ * homepage section passes an admin-configured override built from its
18
+ * `itemsPerView` / `mobileItemsPerView` config.
19
+ */
20
+ perView?: Record<string, number>;
15
21
  }
16
- export declare function CustomerReviewsSection<T = unknown>({ title, subtitle, items, renderItem, viewMoreHref, viewMoreLabel, isLoading, keyExtractor, className, autoScroll, scrollInterval, loop, }: CustomerReviewsSectionProps<T>): React.JSX.Element | null;
22
+ export declare function CustomerReviewsSection<T = unknown>({ title, subtitle, items, renderItem, viewMoreHref, viewMoreLabel, isLoading, keyExtractor, className, autoScroll, scrollInterval, loop, perView, }: CustomerReviewsSectionProps<T>): React.JSX.Element | null;
@@ -10,12 +10,12 @@ const __O = {
10
10
  hidden: "overflow-hidden",
11
11
  };
12
12
  // --- Section -----------------------------------------------------------------
13
- export function CustomerReviewsSection({ title, subtitle, items, renderItem, viewMoreHref, viewMoreLabel = "See all reviews", isLoading = false, keyExtractor, className = "", autoScroll = true, scrollInterval = 4500, loop = true, }) {
13
+ export function CustomerReviewsSection({ title, subtitle, items, renderItem, viewMoreHref, viewMoreLabel = "See all reviews", isLoading = false, keyExtractor, className = "", autoScroll = true, scrollInterval = 4500, loop = true, perView, }) {
14
14
  const themed = { textPrimary: THEMED_TEXT_PRIMARY, textSecondary: THEMED_TEXT_SECONDARY };
15
15
  if (isLoading) {
16
16
  return (_jsx(Section, { className: `${__P.p8} ${className}`, surface: "muted", children: _jsxs(Div, { className: "w-full max-w-7xl mx-auto", children: [_jsx(Div, { className: `h-8 ${skeleton.base} mb-8 max-w-xs mx-auto` }), _jsx(Div, { layout: "flex", gap: "6", className: `${__O.hidden}`, children: [...Array(3)].map((_, i) => (_jsx(Div, { className: `${skeleton.card} h-[clamp(180px,26vh,260px)] min-w-[clamp(150px,18vw,260px)] max-w-[clamp(240px,36vw,380px)] flex-none` }, i))) })] }) }));
17
17
  }
18
18
  if (items.length === 0)
19
19
  return null;
20
- return (_jsx(Section, { className: `${className}`, surface: "muted", padding: "y-3xl", children: _jsxs(Div, { className: "w-full max-w-7xl mx-auto md:px-[2rem]", padding: "x-md", children: [_jsxs(Div, { className: "text-left mb-10", children: [_jsx(Heading, { level: 2, className: `${themed.textPrimary} mb-3`, size: "3xl", mdSize: "4xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { className: `${themed.textSecondary}`, size: "base", children: subtitle }))] }), _jsx(HorizontalScroller, { items: items, renderItem: (item) => renderItem(item), perView: CAROUSEL_PER_VIEW.reviews, gap: 24, autoScroll: autoScroll, autoScrollInterval: scrollInterval, pauseOnHover: true, snapToItems: true, showArrows: true, keyExtractor: keyExtractor, loop: loop }), viewMoreHref && (_jsx(Div, { className: "text-left mt-8", children: _jsx(TextLink, { href: viewMoreHref, className: "text-primary hover:text-primary/80", size: "sm", weight: "medium", children: viewMoreLabel }) }))] }) }));
20
+ return (_jsx(Section, { className: `${className}`, surface: "muted", padding: "y-3xl", children: _jsxs(Div, { className: "w-full max-w-7xl mx-auto md:px-[2rem]", padding: "x-md", children: [_jsxs(Div, { className: "text-left mb-10", children: [_jsx(Heading, { level: 2, className: `${themed.textPrimary} mb-3`, size: "3xl", mdSize: "4xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { className: `${themed.textSecondary}`, size: "base", children: subtitle }))] }), _jsx(HorizontalScroller, { items: items, renderItem: (item) => renderItem(item), perView: perView ?? CAROUSEL_PER_VIEW.reviews, gap: 24, autoScroll: autoScroll, autoScrollInterval: scrollInterval, pauseOnHover: true, snapToItems: true, showArrows: true, keyExtractor: keyExtractor, loop: loop }), viewMoreHref && (_jsx(Div, { className: "text-left mt-8", children: _jsx(TextLink, { href: viewMoreHref, className: "text-primary hover:text-primary/80", size: "sm", weight: "medium", children: viewMoreLabel }) }))] }) }));
21
21
  }
@@ -4,19 +4,10 @@ import { useState, useMemo } from "react";
4
4
  import { THEMED_BG_PRIMARY, THEMED_BG_TERTIARY, THEMED_BORDER, THEMED_TEXT_PRIMARY, THEMED_TEXT_SECONDARY, FLEX_BETWEEN } from "../../../_internal/shared/styles/themed";
5
5
  import { Button, Div, Heading, RichText, Section, Span, Stack, Tabs, TabsList, TabsTrigger, Text, TextLink } from "../../../ui";
6
6
  import { ChevronDown } from "lucide-react";
7
+ import { FAQ_CATEGORY_LABELS } from "../constants/faq-category-labels";
7
8
  const __O = {
8
9
  hidden: "overflow-hidden",
9
10
  };
10
- // --- Constants ----------------------------------------------------------------
11
- const CATEGORY_LABELS = {
12
- general: "General",
13
- orders_payment: "Orders & Payment",
14
- shipping_delivery: "Shipping",
15
- returns_refunds: "Returns & Refunds",
16
- product_information: "Products",
17
- account_security: "Account",
18
- technical_support: "Support",
19
- };
20
11
  // --- Helpers -----------------------------------------------------------------
21
12
  function isHtml(s) {
22
13
  return /<[a-z][\s\S]*>/i.test(s);
@@ -37,7 +28,7 @@ export function FAQSection({ title, subtitle, items, tabs: tabsProp, showCategor
37
28
  const cat = item.category ?? "general";
38
29
  if (!seen.has(cat)) {
39
30
  seen.add(cat);
40
- result.push({ value: cat, label: CATEGORY_LABELS[cat] ?? cat });
31
+ result.push({ value: cat, label: FAQ_CATEGORY_LABELS[cat] ?? cat });
41
32
  }
42
33
  }
43
34
  return result;
@@ -9,8 +9,10 @@ export interface FeaturedAuctionsSectionProps {
9
9
  filterByBrand?: string;
10
10
  initialItems?: ProductItem[];
11
11
  rows?: number;
12
+ /** Cap the rendered cards, mirroring FeaturedProductsSection. */
13
+ maxItems?: number;
12
14
  autoScroll?: boolean;
13
15
  scrollInterval?: number;
14
16
  loop?: boolean;
15
17
  }
16
- export declare function FeaturedAuctionsSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, rows, autoScroll, scrollInterval, loop, }: FeaturedAuctionsSectionProps): React.JSX.Element;
18
+ export declare function FeaturedAuctionsSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, rows, maxItems, autoScroll, scrollInterval, loop, }: FeaturedAuctionsSectionProps): React.JSX.Element;
@@ -4,7 +4,9 @@ import { SectionCarousel } from "./SectionCarousel";
4
4
  import { useFeaturedAuctions } from "../hooks/useFeaturedAuctions";
5
5
  import { MarketplaceAuctionCard } from "../../auctions/components/MarketplaceAuctionCard";
6
6
  import { CAROUSEL_PER_VIEW } from "../constants/carousel-per-view";
7
- export function FeaturedAuctionsSection({ title = "Live Auctions", description, viewMoreHref, viewMoreLabel = "View all auctions →", className = "", filterByBrand, initialItems, rows = 1, autoScroll = false, scrollInterval = 5000, loop = true, }) {
8
- const { data: items = [], isLoading } = useFeaturedAuctions({ filterByBrand, initialData: initialItems });
9
- return (_jsx(SectionCarousel, { title: title, description: description, pillLabel: "Live Auctions", headingVariant: "editorial", viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, items: items, isLoading: isLoading, skeletonCount: 4, perView: CAROUSEL_PER_VIEW.standard, gap: 16, rows: Math.min(Math.max(rows, 1), 4), autoScroll: autoScroll, autoScrollInterval: scrollInterval, loop: loop, keyExtractor: (product) => product.id, renderItem: (product) => (_jsx(MarketplaceAuctionCard, { product: product })), className: className }));
7
+ import { SECTION_TITLE, VIEW_MORE_LABEL } from "../constants/section-copy";
8
+ export function FeaturedAuctionsSection({ title = SECTION_TITLE.auctions, description, viewMoreHref, viewMoreLabel = VIEW_MORE_LABEL.auctions, className = "", filterByBrand, initialItems, rows = 1, maxItems, autoScroll = false, scrollInterval = 5000, loop = true, }) {
9
+ const { data: fetched = [], isLoading } = useFeaturedAuctions({ filterByBrand, initialData: initialItems });
10
+ const items = maxItems && maxItems > 0 ? fetched.slice(0, maxItems) : fetched;
11
+ return (_jsx(SectionCarousel, { title: title, description: description, pillLabel: SECTION_TITLE.auctions, headingVariant: "editorial", viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, items: items, isLoading: isLoading, skeletonCount: 4, perView: CAROUSEL_PER_VIEW.standard, gap: 16, rows: Math.min(Math.max(rows, 1), 4), autoScroll: autoScroll, autoScrollInterval: scrollInterval, loop: loop, keyExtractor: (product) => product.id, renderItem: (product) => (_jsx(MarketplaceAuctionCard, { product: product })), className: className }));
10
12
  }
@@ -12,9 +12,15 @@ export interface FeaturedBundlesSectionProps {
12
12
  title?: string;
13
13
  description?: string;
14
14
  initialItems?: CategoryDocument[];
15
+ /** Cap the rendered bundles. Items are supplied pre-fetched, so this slices. */
16
+ maxItems?: number;
17
+ /** Show the "N% off" badge on each cover. Defaults to `true`. */
18
+ showSavingsBadge?: boolean;
19
+ /** "View all" link label. */
20
+ viewMoreLabel?: string;
15
21
  className?: string;
16
22
  onBuyNow?: (input: {
17
23
  bundleSlug: string;
18
24
  }) => Promise<unknown>;
19
25
  }
20
- export declare function FeaturedBundlesSection({ title, description, initialItems, className, onBuyNow, }: FeaturedBundlesSectionProps): React.JSX.Element | null;
26
+ export declare function FeaturedBundlesSection({ title, description, initialItems, maxItems, showSavingsBadge, viewMoreLabel, className, onBuyNow, }: FeaturedBundlesSectionProps): React.JSX.Element | null;
@@ -24,20 +24,21 @@ const __O = {
24
24
  hidden: "overflow-hidden",
25
25
  };
26
26
  const PLACEHOLDER_EMOJI = "📦";
27
- export function FeaturedBundlesSection({ title = BUNDLE_COPY.featuredDefaultTitle, description, initialItems = [], className = "", onBuyNow, }) {
27
+ export function FeaturedBundlesSection({ title = BUNDLE_COPY.featuredDefaultTitle, description, initialItems = [], maxItems, showSavingsBadge = true, viewMoreLabel = BUNDLE_COPY.featured.viewAll, className = "", onBuyNow, }) {
28
28
  const { hand } = useHandMode();
29
- const items = initialItems.filter((c) => c.categoryType === "bundle");
29
+ const bundles = initialItems.filter((c) => c.categoryType === "bundle");
30
+ const items = maxItems && maxItems > 0 ? bundles.slice(0, maxItems) : bundles;
30
31
  if (items.length === 0)
31
32
  return null;
32
- return (_jsx(Section, { className: `${className}`, padding: "y-2xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", wrap: true, reverse: hand === "left", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "2xl", weight: "semibold", color: "primary", children: title }), description && (_jsx(Text, { size: "sm", color: "muted", children: description }))] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLES), className: "text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-primary)] hover:underline", children: BUNDLE_COPY.featured.viewAll })] }), _jsx(Grid, { cols: "cards", gap: "3", children: items.map((bundle) => (_jsx(FeaturedBundleCard, { bundle: bundle, onBuyNow: onBuyNow }, bundle.id))) })] }) }));
33
+ return (_jsx(Section, { className: `${className}`, padding: "y-2xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", wrap: true, reverse: hand === "left", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "2xl", weight: "semibold", color: "primary", children: title }), description && (_jsx(Text, { size: "sm", color: "muted", children: description }))] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLES), className: "text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-primary)] hover:underline", children: viewMoreLabel })] }), _jsx(Grid, { cols: "cards", gap: "3", children: items.map((bundle) => (_jsx(FeaturedBundleCard, { bundle: bundle, showSavingsBadge: showSavingsBadge, onBuyNow: onBuyNow }, bundle.id))) })] }) }));
33
34
  }
34
- function FeaturedBundleCard({ bundle, onBuyNow }) {
35
+ function FeaturedBundleCard({ bundle, showSavingsBadge = true, onBuyNow, }) {
35
36
  const stock = bundle.bundleStockStatus ?? "in_stock";
36
37
  const memberCount = bundle.bundleProductIds?.length ?? 0;
37
38
  const cover = bundle.display?.coverImage;
38
39
  const href = String(ROUTES.PUBLIC.BUNDLE_DETAIL?.(bundle.slug) ?? "#");
39
40
  const discount = computeBundleDiscount(bundle.bundlePrice, bundle.bundleOriginalTotal);
40
- return (_jsxs(Stack, { gap: "none", surface: "default", rounded: "xl", border: "default", children: [_jsxs(Link, { href: href, className: "group block p-[var(--appkit-space-3)] hover:no-underline", children: [_jsxs(Div, { className: `relative mb-2 aspect-square ${__O.hidden}`, rounded: "lg", surface: "subtle", children: [cover ? (_jsx(MediaImage, { src: cover, alt: bundle.name, size: "card", className: "transition-transform group-hover:scale-105" })) : (_jsx(Row, { textSize: "3xl", className: "h-full w-full", align: "center", justify: "center", children: PLACEHOLDER_EMOJI })), discount && (_jsx(Badge, { variant: "success", className: "absolute left-2 top-2", children: BUNDLE_COPY.detail.discountBadge(discount.percent) }))] }), _jsx(Text, { className: "line-clamp-2", size: "sm", weight: "semibold", children: bundle.name }), _jsxs(Row, { gap: "sm", align: "center", className: "mt-1", wrap: true, children: [_jsx(Text, { size: "sm", weight: "bold", children: bundle.bundlePrice
41
+ return (_jsxs(Stack, { gap: "none", surface: "default", rounded: "xl", border: "default", children: [_jsxs(Link, { href: href, className: "group block p-[var(--appkit-space-3)] hover:no-underline", children: [_jsxs(Div, { className: `relative mb-2 aspect-square ${__O.hidden}`, rounded: "lg", surface: "subtle", children: [cover ? (_jsx(MediaImage, { src: cover, alt: bundle.name, size: "card", className: "transition-transform group-hover:scale-105" })) : (_jsx(Row, { textSize: "3xl", className: "h-full w-full", align: "center", justify: "center", children: PLACEHOLDER_EMOJI })), showSavingsBadge && discount && (_jsx(Badge, { variant: "success", className: "absolute left-2 top-2", children: BUNDLE_COPY.detail.discountBadge(discount.percent) }))] }), _jsx(Text, { className: "line-clamp-2", size: "sm", weight: "semibold", children: bundle.name }), _jsxs(Row, { gap: "sm", align: "center", className: "mt-1", wrap: true, children: [_jsx(Text, { size: "sm", weight: "bold", children: bundle.bundlePrice
41
42
  ? formatCurrency(bundle.bundlePrice, "INR")
42
43
  : BUNDLE_COPY.featured.priceFallback }), discount && (_jsx(Text, { size: "xs", color: "muted", className: "line-through", children: formatCurrency(discount.originalTotal, "INR") })), _jsxs(Text, { size: "xs", color: "muted", children: ["\u00B7 ", BUNDLE_COPY.featured.itemCount(memberCount)] }), stock !== "in_stock" && (_jsx(Badge, { variant: BUNDLE_STOCK_VARIANT[stock], children: BUNDLE_COPY.stockBadge.listVariantOutOfStock }))] })] }), onBuyNow && (_jsx(Div, { border: "subtle", className: `border-t ${__P.p3}`, padding: "t-xs", children: _jsx(BundleBuyNowCta, { bundleSlug: bundle.slug, outOfStock: stock === "out_of_stock", onBuyNow: onBuyNow, compact: true }) }))] }));
43
44
  }
@@ -9,8 +9,10 @@ export interface FeaturedPreOrdersSectionProps {
9
9
  filterByBrand?: string;
10
10
  initialItems?: ProductItem[];
11
11
  rows?: number;
12
+ /** Cap the rendered cards, mirroring FeaturedProductsSection. */
13
+ maxItems?: number;
12
14
  autoScroll?: boolean;
13
15
  scrollInterval?: number;
14
16
  loop?: boolean;
15
17
  }
16
- export declare function FeaturedPreOrdersSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, rows, autoScroll, scrollInterval, loop, }: FeaturedPreOrdersSectionProps): React.JSX.Element;
18
+ export declare function FeaturedPreOrdersSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, rows, maxItems, autoScroll, scrollInterval, loop, }: FeaturedPreOrdersSectionProps): React.JSX.Element;
@@ -4,7 +4,9 @@ import { SectionCarousel } from "./SectionCarousel";
4
4
  import { useFeaturedPreOrders } from "../hooks/useFeaturedPreOrders";
5
5
  import { MarketplacePreorderCard } from "../../pre-orders/components/MarketplacePreorderCard";
6
6
  import { CAROUSEL_PER_VIEW } from "../constants/carousel-per-view";
7
- export function FeaturedPreOrdersSection({ title = "Reserve Before It Ships", description, viewMoreHref, viewMoreLabel = "View all pre-orders →", className = "", filterByBrand, initialItems, rows = 1, autoScroll = false, scrollInterval = 5000, loop = true, }) {
8
- const { data: items = [], isLoading } = useFeaturedPreOrders({ filterByBrand, initialData: initialItems });
7
+ import { SECTION_TITLE, VIEW_MORE_LABEL } from "../constants/section-copy";
8
+ export function FeaturedPreOrdersSection({ title = SECTION_TITLE.preOrders, description, viewMoreHref, viewMoreLabel = VIEW_MORE_LABEL.preOrders, className = "", filterByBrand, initialItems, rows = 1, maxItems, autoScroll = false, scrollInterval = 5000, loop = true, }) {
9
+ const { data: fetched = [], isLoading } = useFeaturedPreOrders({ filterByBrand, initialData: initialItems });
10
+ const items = maxItems && maxItems > 0 ? fetched.slice(0, maxItems) : fetched;
9
11
  return (_jsx(SectionCarousel, { title: title, description: description, pillLabel: "Pre-Order Incoming", headingVariant: "editorial", viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, items: items, isLoading: isLoading, skeletonCount: 4, perView: CAROUSEL_PER_VIEW.standard, gap: 16, rows: Math.min(Math.max(rows, 1), 4), autoScroll: autoScroll, autoScrollInterval: scrollInterval, loop: loop, keyExtractor: (product) => product.id, renderItem: (product) => (_jsx(MarketplacePreorderCard, { product: product })), className: className }));
10
12
  }
@@ -8,9 +8,11 @@ export interface FeaturedProductsSectionProps {
8
8
  filterByBrand?: string;
9
9
  initialItems?: ProductItem[];
10
10
  rows?: number;
11
+ /** Cap the rendered cards. Was declared but never destructured — so every
12
+ * `maxItems` an admin configured was silently ignored. */
11
13
  maxItems?: number;
12
14
  autoScroll?: boolean;
13
15
  scrollInterval?: number;
14
16
  loop?: boolean;
15
17
  }
16
- export declare function FeaturedProductsSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, rows, autoScroll, scrollInterval, loop, }: FeaturedProductsSectionProps): import("react").JSX.Element;
18
+ export declare function FeaturedProductsSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, rows, maxItems, autoScroll, scrollInterval, loop, }: FeaturedProductsSectionProps): import("react").JSX.Element;
@@ -5,13 +5,15 @@ import { useFeaturedProducts } from "../hooks/useFeaturedProducts";
5
5
  import { InteractiveProductCard } from "../../products/components/InteractiveProductCard";
6
6
  import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
7
7
  import { CAROUSEL_PER_VIEW } from "../constants/carousel-per-view";
8
- export function FeaturedProductsSection({ title = "Featured Products", description, viewMoreHref, viewMoreLabel = "View all products →", className = "", filterByBrand, initialItems, rows = 1, autoScroll = false, scrollInterval = 5000, loop = true, }) {
8
+ import { SECTION_TITLE, VIEW_MORE_LABEL } from "../constants/section-copy";
9
+ export function FeaturedProductsSection({ title = SECTION_TITLE.products, description, viewMoreHref, viewMoreLabel = VIEW_MORE_LABEL.products, className = "", filterByBrand, initialItems, rows = 1, maxItems, autoScroll = false, scrollInterval = 5000, loop = true, }) {
9
10
  const { data, isLoading } = useFeaturedProducts({
10
11
  filterByBrand,
11
12
  initialData: initialItems?.length
12
13
  ? { items: initialItems, total: initialItems.length, page: 1, pageSize: initialItems.length, totalPages: 1, hasMore: false }
13
14
  : undefined,
14
15
  });
15
- const items = data?.items ?? [];
16
- return (_jsx(SectionCarousel, { title: title, description: description, pillLabel: "Featured Products", headingVariant: "editorial", viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, items: items, isLoading: isLoading, skeletonCount: 4, perView: CAROUSEL_PER_VIEW.standard, gap: 16, rows: Math.min(Math.max(rows, 1), 4), autoScroll: autoScroll, autoScrollInterval: scrollInterval, loop: loop, keyExtractor: (product) => product.id, renderItem: (product) => (_jsx(InteractiveProductCard, { product: product, href: pluginFor(product.listingType ?? "standard").detailRoute(product.slug ?? product.id ?? "") })), className: className }));
16
+ const fetched = data?.items ?? [];
17
+ const items = maxItems && maxItems > 0 ? fetched.slice(0, maxItems) : fetched;
18
+ return (_jsx(SectionCarousel, { title: title, description: description, pillLabel: SECTION_TITLE.products, headingVariant: "editorial", viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, items: items, isLoading: isLoading, skeletonCount: 4, perView: CAROUSEL_PER_VIEW.standard, gap: 16, rows: Math.min(Math.max(rows, 1), 4), autoScroll: autoScroll, autoScrollInterval: scrollInterval, loop: loop, keyExtractor: (product) => product.id, renderItem: (product) => (_jsx(InteractiveProductCard, { product: product, href: pluginFor(product.listingType ?? "standard").detailRoute(product.slug ?? product.id ?? "") })), className: className }));
17
19
  }
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import type { CarouselSlide } from "../types/index";
2
+ import type { CarouselSlide, CarouselSlideHeight } from "../types/index";
3
3
  export interface HeroCarouselProps {
4
4
  initialSlides?: CarouselSlide[];
5
5
  /**
@@ -8,5 +8,26 @@ export interface HeroCarouselProps {
8
8
  * Falls back to `window.location.href` assignment when omitted.
9
9
  */
10
10
  push?: (href: string) => void;
11
+ /**
12
+ * Everything below comes from `CarouselSectionConfig`, which the section
13
+ * renderer used to drop entirely — it rendered `<HeroCarousel initialSlides />`
14
+ * and nothing else, so every one of these admin controls was inert.
15
+ */
16
+ /** Fallback slide height when a slide carries no `settings.height`. */
17
+ height?: CarouselSlideHeight;
18
+ /** Fallback autoplay delay when a slide carries no `settings.autoplayDelayMs`. */
19
+ defaultAutoplayDelayMs?: number;
20
+ /** Pause autoplay while the pointer is over the carousel. Defaults to `true`. */
21
+ pauseOnHover?: boolean;
22
+ /** Render the slide-position dots. Defaults to `true`. */
23
+ showDots?: boolean;
24
+ /** Render the prev/next arrows. Defaults to `true`. */
25
+ showArrows?: boolean;
26
+ /**
27
+ * Accessible name for the carousel region. Deliberately NOT rendered as a
28
+ * visible heading — the config field is a label for the admin list, and
29
+ * printing it above the slides would add hero chrome nobody asked for.
30
+ */
31
+ title?: string;
11
32
  }
12
- export declare function HeroCarousel({ initialSlides, push }?: HeroCarouselProps): React.JSX.Element;
33
+ export declare function HeroCarousel({ initialSlides, push, height, defaultAutoplayDelayMs, pauseOnHover, showDots, showArrows, title, }?: HeroCarouselProps): React.JSX.Element;
@@ -86,7 +86,7 @@ function SlideBackground({ bg, legacy, mobileLegacy, isMobile, priority, }) {
86
86
  if (effectiveBg.type === "video") {
87
87
  return (_jsxs(_Fragment, { children: [_jsx(MediaVideo, { src: src, thumbnailUrl: effectiveBg.thumbnail ?? legacy?.thumbnail, alt: "", autoPlayMuted: true, loop: true, controls: false }), effectiveBg.dimOverlay?.enabled && (_jsx(Div, { className: POSITION_FILL, style: { backgroundColor: `rgba(0,0,0,${(effectiveBg.dimOverlay.opacity ?? 0) / 100})` } }))] }));
88
88
  }
89
- return (_jsxs(_Fragment, { children: [_jsx(MediaImage, { src: src, alt: "", size: "hero", priority: priority }), effectiveBg.dimOverlay?.enabled && (_jsx(Div, { className: POSITION_FILL, style: { backgroundColor: `rgba(0,0,0,${(effectiveBg.dimOverlay.opacity ?? 0) / 100})` } }))] }));
89
+ return (_jsxs(_Fragment, { children: [_jsx(MediaImage, { src: src, alt: "", size: "hero", priority: priority, fetchPriority: priority ? "high" : "low" }), effectiveBg.dimOverlay?.enabled && (_jsx(Div, { className: POSITION_FILL, style: { backgroundColor: `rgba(0,0,0,${(effectiveBg.dimOverlay.opacity ?? 0) / 100})` } }))] }));
90
90
  }
91
91
  /** Renders the background of a card (image, video, color, gradient). */
92
92
  function CardBackground({ bg }) {
@@ -117,14 +117,17 @@ function CarouselCardRenderer({ card, isMobile, push, }) {
117
117
  gridRow: String(gridPos.gridRow),
118
118
  gridColumn: String(gridPos.gridColumn),
119
119
  minHeight: isMobile ? 100 : 140,
120
- }, children: [_jsx(CardBackground, { bg: card.background }), !card.isButtonOnly && (_jsxs(_Fragment, { children: [_jsx(Scrim, { direction: "bottom-up", intensity: "strong", className: POSITION_FILL }), _jsxs(Stack, { justify: "end", className: `${POSITION_FILL} md:p-[1.5rem] ${textAlignClass}`, padding: "xs", children: [card.content?.eyebrow && (_jsx(Text, { color: "inverse", shadow: "sm", className: "text-[10px] !/70 mb-0.5 tracking-wider", mdSize: "xs", transform: "uppercase", children: card.content.eyebrow })), card.content?.subtitle && (_jsx(Text, { color: "inverse", shadow: "sm", className: "hidden md:block !/90 mb-0.5 md:mb-2", mdSize: "sm", size: "xs", children: card.content.subtitle })), card.content?.title && (_jsx(Heading, { level: 2, className: `text-[11px] mb-0.5 md:mb-3`, truncate: 2, weight: "bold", shadow: "md", mdSize: "2xl", lgSize: "3xl", style: { color: card.content.textColor ?? "white" }, children: card.content.title })), card.content?.description && (_jsx(Text, { color: "inverse", shadow: "sm", className: `text-[10px] md:text-[0.875rem] !/80 mb-1 md:mb-4`, truncate: true, children: card.content.description })), (card.buttons?.length ?? 0) > 0 && (_jsx(Row, { wrap: true, gap: "xs", className: "md:gap-[var(--appkit-space-2)]", children: (card.buttons ?? []).map((btn, i) => {
120
+ }, children: [_jsx(CardBackground, { bg: card.background }), !card.isButtonOnly && (_jsxs(_Fragment, { children: [_jsx(Scrim, { direction: "bottom-up", intensity: "strong", className: POSITION_FILL }), _jsxs(Stack, { justify: "end", className: `${POSITION_FILL} md:p-[1.5rem] ${textAlignClass}`, padding: "xs", children: [card.content?.eyebrow && (_jsx(Text, { color: "inverse", shadow: "sm", className: "text-[10px] !/70 mb-0.5 tracking-wider", mdSize: "xs", transform: "uppercase", children: card.content.eyebrow })), card.content?.subtitle && (_jsx(Text, { color: "inverse", shadow: "sm", className: "hidden md:block !/90 mb-0.5 md:mb-2", mdSize: "sm", size: "xs", children: card.content.subtitle })), card.content?.title && (
121
+ // level={3}: a card sits INSIDE a slide, so it is subordinate to
122
+ // that slide's own h2 overlay title rather than a sibling of it.
123
+ _jsx(Heading, { level: 3, className: `text-[11px] mb-0.5 md:mb-3`, truncate: 2, weight: "bold", shadow: "md", mdSize: "2xl", lgSize: "3xl", style: { color: card.content.textColor ?? "white" }, children: card.content.title })), card.content?.description && (_jsx(Text, { color: "inverse", shadow: "sm", className: `text-[10px] md:text-[0.875rem] !/80 mb-1 md:mb-4`, truncate: true, children: card.content.description })), (card.buttons?.length ?? 0) > 0 && (_jsx(Row, { wrap: true, gap: "xs", className: "md:gap-[var(--appkit-space-2)]", children: (card.buttons ?? []).map((btn, i) => {
121
124
  const variant = btn.variant === "link" || btn.variant === "ghost"
122
125
  ? "ghost"
123
126
  : btn.variant;
124
127
  return (_jsx(Button, { variant: variant, size: "sm", onClick: makeButtonClickHandler(btn.href, btn.openInNewTab, push), children: btn.text }, btn.id ?? i));
125
128
  }) }))] })] })), card.isButtonOnly && card.buttons?.[0] && (_jsx(Button, { variant: "ghost", rounded: "none", className: `${POSITION_FILL} ${FLEX_CENTER} font-[600] text-white hover:bg-[rgba(0,0,0,0.2)] transition-colors p-[0]`, onClick: makeButtonClickHandler(card.buttons[0].href, card.buttons[0].openInNewTab, push), children: _jsx(Span, { size: "lg", mdSize: "2xl", children: card.buttons[0].text }) }))] }));
126
129
  }
127
- export function HeroCarousel({ initialSlides, push } = {}) {
130
+ export function HeroCarousel({ initialSlides, push, height, defaultAutoplayDelayMs, pauseOnHover = true, showDots = true, showArrows = true, title, } = {}) {
128
131
  const { hand } = useHandMode();
129
132
  const [currentSlide, setCurrentSlide] = useState(0);
130
133
  const [isPaused, setIsPaused] = useState(false);
@@ -139,8 +142,9 @@ export function HeroCarousel({ initialSlides, push } = {}) {
139
142
  ?.sort((a, b) => a.order - b.order)
140
143
  ?.slice(0, 5) || [];
141
144
  const currentSlideData = slides[currentSlide];
142
- const autoplayDelay = currentSlideData?.settings?.autoplayDelayMs ?? 4000;
143
- const heightClass = getSlideHeightClass(currentSlideData?.settings?.height);
145
+ // Per-slide setting wins, then the section-level config, then the built-in.
146
+ const autoplayDelay = currentSlideData?.settings?.autoplayDelayMs ?? defaultAutoplayDelayMs ?? 4000;
147
+ const heightClass = getSlideHeightClass(currentSlideData?.settings?.height ?? height);
144
148
  const goToSlide = useCallback((index) => {
145
149
  const el = slidesRef.current;
146
150
  if (!el)
@@ -184,16 +188,25 @@ export function HeroCarousel({ initialSlides, push } = {}) {
184
188
  if (!slides || slides.length === 0) {
185
189
  return (_jsxs(Div, { gap: "4", layout: "flex-col", align: "start", justify: "center", className: "relative w-full min-h-[260px] md:min-h-[60vh] bg-[image:var(--appkit-gradient-section-cool)] px-[var(--appkit-space-6)]", children: [_jsx(Heading, { level: 2, className: "text-primary", size: "2xl", weight: "bold", align: "start", children: "Coming Soon" }), _jsx(Text, { variant: "secondary", className: "max-w-md", align: "start", children: "Amazing deals are on their way. Stay tuned!" })] }));
186
190
  }
187
- return (_jsxs(Section, { ref: sectionRef, className: `relative w-full ${heightClass} overflow-hidden`, "aria-roledescription": "carousel", "aria-label": "Hero carousel", onKeyDown: handleKeyDown, onMouseEnter: () => setIsPaused(true), onMouseLeave: () => setIsPaused(false), onFocus: () => setIsPaused(true), onBlur: () => setIsPaused(false), tabIndex: 0, children: [_jsx(Div, { "aria-live": "polite", "aria-atomic": "true", className: "sr-only", children: `Slide ${currentSlide + 1} of ${slides.length}` }), _jsx(Div, { className: POSITION_FILL, children: _jsx(Div, { layout: "flex", ref: slidesRef, onScroll: handleSlidesScroll, className: "h-full overflow-x-auto scroll-smooth snap-x snap-mandatory [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", style: { gap: 0 }, children: slides.map((slide, slideIndex) => {
191
+ return (_jsxs(Section, { ref: sectionRef, className: `relative w-full ${heightClass} overflow-hidden`, "aria-roledescription": "carousel", "aria-label": title?.trim() || "Hero carousel", onKeyDown: handleKeyDown,
192
+ // Hover-pause is configurable; focus-pause is NOT. WCAG 2.2.2 requires a
193
+ // mechanism to pause moving content, and keyboard users have no hover.
194
+ onMouseEnter: pauseOnHover ? () => setIsPaused(true) : undefined, onMouseLeave: pauseOnHover ? () => setIsPaused(false) : undefined, onFocus: () => setIsPaused(true), onBlur: () => setIsPaused(false), tabIndex: 0, children: [_jsx(Div, { "aria-live": "polite", "aria-atomic": "true", className: "sr-only", children: `Slide ${currentSlide + 1} of ${slides.length}` }), _jsx(Div, { className: POSITION_FILL, children: _jsx(Div, { layout: "flex", ref: slidesRef, onScroll: handleSlidesScroll, className: "h-full overflow-x-auto scroll-smooth snap-x snap-mandatory [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", style: { gap: 0 }, children: slides.map((slide, slideIndex) => {
188
195
  const slideHeightClass = getSlideHeightClass(slide.settings?.height);
189
196
  const hasCards = slide.cards && slide.cards.length > 0;
190
- return (_jsxs(Div, { className: `snap-start flex-none w-full relative self-stretch bg-neutral-900 ${slideHeightClass}`, children: [_jsx(Div, { className: POSITION_FILL, children: _jsx(SlideBackground, { bg: slide.background, legacy: slide.media, mobileLegacy: slide.mobileMedia, isMobile: isMobile, priority: slideIndex === 0 }) }), slide.overlay && (_jsxs(Div, { layout: "flex-col", align: "start", justify: "center", className: `${POSITION_FILL} text-left md:px-[4rem] lg:px-[8rem]`, padding: "x-lg", children: [slide.overlay.subtitle && (_jsx(Text, { color: "inverse", shadow: "sm", className: "stagger-1 !/80 mb-1 md:mb-2 tracking-widest", mdSize: "sm", size: "xs", transform: "uppercase", children: slide.overlay.subtitle })), slide.overlay.title && (_jsx(Heading, { color: "inverse", level: 1, className: "stagger-2 font-display ! mb-2 md:mb-4 break-words", size: "4xl", shadow: "2xl", mdSize: "6xl", lgSize: "8xl", children: slide.overlay.title })), slide.overlay.description && (_jsx(Text, { color: "inverse", shadow: "sm", className: "stagger-3 !/90 mb-4 md:mb-8 max-w-2xl", mdSize: "lg", lgSize: "xl", size: "sm", children: slide.overlay.description })), slide.overlay.button && (_jsx(Div, { className: "stagger-4", children: _jsx(Button, { variant: slide.overlay.button.variant, size: "sm", onClick: makeButtonClickHandler(slide.overlay.button.link, slide.overlay.button.openInNewTab, push), children: slide.overlay.button.text }) }))] })), hasCards && (_jsx(Div, { className: `${POSITION_FILL} grid gap-[var(--appkit-space-2)] md:gap-[var(--appkit-space-4)] md:p-[var(--appkit-space-8)]`, padding: "md", style: {
197
+ return (_jsxs(Div, { className: `snap-start flex-none w-full relative self-stretch bg-neutral-900 ${slideHeightClass}`, children: [_jsx(Div, { className: POSITION_FILL, children: _jsx(SlideBackground, { bg: slide.background, legacy: slide.media, mobileLegacy: slide.mobileMedia, isMobile: isMobile, priority: slideIndex === 0 }) }), slide.overlay && (_jsxs(Div, { layout: "flex-col", align: "start", justify: "center", className: `${POSITION_FILL} text-left md:px-[4rem] lg:px-[8rem]`, padding: "x-lg", children: [slide.overlay.subtitle && (_jsx(Text, { color: "inverse", shadow: "sm", className: "stagger-1 !/80 mb-1 md:mb-2 tracking-widest", mdSize: "sm", size: "xs", transform: "uppercase", children: slide.overlay.subtitle })), slide.overlay.title && (
198
+ // level={2}, NOT 1. The welcome section above this
199
+ // carousel owns the page's single <h1>; a carousel that
200
+ // emits one <h1> per slide gives the document as many
201
+ // h1s as it has slides. `size` is independent of `level`,
202
+ // so this changes the outline and not one pixel.
203
+ _jsx(Heading, { color: "inverse", level: 2, className: "stagger-2 font-display ! mb-2 md:mb-4 break-words", size: "4xl", shadow: "2xl", mdSize: "6xl", lgSize: "8xl", children: slide.overlay.title })), slide.overlay.description && (_jsx(Text, { color: "inverse", shadow: "sm", className: "stagger-3 !/90 mb-4 md:mb-8 max-w-2xl", mdSize: "lg", lgSize: "xl", size: "sm", children: slide.overlay.description })), slide.overlay.button && (_jsx(Div, { className: "stagger-4", children: _jsx(Button, { variant: slide.overlay.button.variant, size: "sm", onClick: makeButtonClickHandler(slide.overlay.button.link, slide.overlay.button.openInNewTab, push), children: slide.overlay.button.text }) }))] })), hasCards && (_jsx(Div, { className: `${POSITION_FILL} grid gap-[var(--appkit-space-2)] md:gap-[var(--appkit-space-4)] md:p-[var(--appkit-space-8)]`, padding: "md", style: {
191
204
  gridTemplateRows: "repeat(2, 1fr)",
192
205
  gridTemplateColumns: isMobile ? "1fr" : "repeat(3, 1fr)",
193
206
  alignContent: "center",
194
207
  alignItems: "stretch",
195
208
  }, children: slide.cards.slice(0, 6).map((card) => (_jsx(CarouselCardRenderer, { card: card, isMobile: isMobile, push: push }, card.id))) }))] }, slide.id));
196
- }) }) }), slides.length > 1 && (_jsx(Row, { gap: "sm", className: "absolute bottom-4 left-1/2 -translate-x-1/2 z-10", children: slides.map((_, index) => (_jsx(Button, { variant: "ghost", className: `relative overflow-hidden rounded-full transition-all duration-500 p-[var(--appkit-space-0)] !min-h-0 ${index === currentSlide
209
+ }) }) }), showDots && slides.length > 1 && (_jsx(Row, { gap: "sm", className: "absolute bottom-4 left-1/2 -translate-x-1/2 z-10", children: slides.map((_, index) => (_jsx(Button, { variant: "ghost", className: `relative overflow-hidden rounded-full transition-all duration-500 p-[var(--appkit-space-0)] !min-h-0 ${index === currentSlide
197
210
  ? HERO_CAROUSEL_DOT_ACTIVE
198
- : `${HERO_CAROUSEL_DOT_INACTIVE} hover:bg-white/75`}`, onClick: () => goToSlide(index), "aria-label": `Go to slide ${index + 1}`, children: index === currentSlide && (_jsx(Span, { className: "absolute inset-y-0 left-0 bg-[rgba(0,0,0,0.2)] animate-[progress-fill_4s_linear_forwards]", rounded: "full", "aria-hidden": "true" })) }, index))) })), slides.length > 1 && (_jsxs(Row, { gap: "sm", className: `absolute bottom-4 ${hand === "left" ? "left-4" : "right-4"} z-20`, children: [_jsx(Button, { variant: "ghost", className: `p-[0] ${HERO_CAROUSEL_ARROW}`, onClick: goPrev, "aria-label": "Previous slide", children: _jsx("svg", { className: "w-7 h-7 shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M15 19l-7-7 7-7" }) }) }), _jsx(Button, { variant: "ghost", className: `p-[0] ${HERO_CAROUSEL_ARROW}`, onClick: goNext, "aria-label": "Next slide", children: _jsx("svg", { className: "w-7 h-7 shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M9 5l7 7-7 7" }) }) })] }))] }));
211
+ : `${HERO_CAROUSEL_DOT_INACTIVE} hover:bg-white/75`}`, onClick: () => goToSlide(index), "aria-label": `Go to slide ${index + 1}`, children: index === currentSlide && (_jsx(Span, { className: "absolute inset-y-0 left-0 bg-[rgba(0,0,0,0.2)] animate-[progress-fill_4s_linear_forwards]", rounded: "full", "aria-hidden": "true" })) }, index))) })), showArrows && slides.length > 1 && (_jsxs(Row, { gap: "sm", className: `absolute bottom-4 ${hand === "left" ? "left-4" : "right-4"} z-20`, children: [_jsx(Button, { variant: "ghost", className: `p-[0] ${HERO_CAROUSEL_ARROW}`, onClick: goPrev, "aria-label": "Previous slide", children: _jsx("svg", { className: "w-7 h-7 shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M15 19l-7-7 7-7" }) }) }), _jsx(Button, { variant: "ghost", className: `p-[0] ${HERO_CAROUSEL_ARROW}`, onClick: goNext, "aria-label": "Next slide", children: _jsx("svg", { className: "w-7 h-7 shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M9 5l7 7-7 7" }) }) })] }))] }));
199
212
  }
@@ -4,9 +4,14 @@ export interface HomepageCustomerReviewsSectionProps {
4
4
  subtitle?: string;
5
5
  viewMoreHref?: string;
6
6
  viewMoreLabel?: string;
7
+ /** Cap the rendered reviews. */
8
+ maxReviews?: number;
9
+ /** Cards visible per view on desktop / mobile. */
10
+ itemsPerView?: number;
11
+ mobileItemsPerView?: number;
7
12
  className?: string;
8
13
  autoScroll?: boolean;
9
14
  scrollInterval?: number;
10
15
  loop?: boolean;
11
16
  }
12
- export declare function HomepageCustomerReviewsSection({ title, subtitle, viewMoreHref, viewMoreLabel, className, autoScroll, scrollInterval, loop, }: HomepageCustomerReviewsSectionProps): React.JSX.Element;
17
+ export declare function HomepageCustomerReviewsSection({ title, subtitle, viewMoreHref, viewMoreLabel, maxReviews, itemsPerView, mobileItemsPerView, className, autoScroll, scrollInterval, loop, }: HomepageCustomerReviewsSectionProps): React.JSX.Element;
@@ -3,7 +3,15 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { CustomerReviewsSection } from "./CustomerReviewsSection";
4
4
  import { useHomepageReviews } from "../hooks/useHomepageReviews";
5
5
  import { ReviewCard } from "../../reviews/components";
6
- export function HomepageCustomerReviewsSection({ title = "What Our Customers Say", subtitle, viewMoreHref, viewMoreLabel = "See all reviews →", className = "", autoScroll = true, scrollInterval = 4500, loop = true, }) {
7
- const { data: reviews = [], isLoading } = useHomepageReviews();
8
- return (_jsx(CustomerReviewsSection, { title: title, subtitle: subtitle, items: reviews, renderItem: (review) => (_jsx(ReviewCard, { review: review, className: "h-full shadow-sm border-[var(--appkit-color-border)]" })), viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, isLoading: isLoading, keyExtractor: (review) => review.id, className: className, autoScroll: autoScroll, scrollInterval: scrollInterval, loop: loop }));
6
+ import { SECTION_TITLE, VIEW_MORE_LABEL } from "../constants/section-copy";
7
+ export function HomepageCustomerReviewsSection({ title = SECTION_TITLE.reviews, subtitle, viewMoreHref, viewMoreLabel = VIEW_MORE_LABEL.reviews, maxReviews, itemsPerView, mobileItemsPerView, className = "", autoScroll = true, scrollInterval = 4500, loop = true, }) {
8
+ const { data: fetched = [], isLoading } = useHomepageReviews();
9
+ const reviews = maxReviews && maxReviews > 0 ? fetched.slice(0, maxReviews) : fetched;
10
+ return (_jsx(CustomerReviewsSection, { title: title, subtitle: subtitle, items: reviews, renderItem: (review) => (_jsx(ReviewCard, { review: review, className: "h-full shadow-sm border-[var(--appkit-color-border)]" })), viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, isLoading: isLoading, keyExtractor: (review) => review.id, className: className, autoScroll: autoScroll, scrollInterval: scrollInterval, loop: loop,
11
+ // Only override the shared preset when the admin actually set one of the
12
+ // two counts — otherwise a partial override would silently flatten the
13
+ // preset's sm/lg/2xl steps down to two breakpoints.
14
+ perView: itemsPerView || mobileItemsPerView
15
+ ? { base: mobileItemsPerView ?? 1, md: itemsPerView ?? 3 }
16
+ : undefined }));
9
17
  }
@@ -5,5 +5,13 @@ export interface MarketplaceHomepageViewProps {
5
5
  newsletterFormSlot?: React.ReactNode;
6
6
  /** Callback when user dismisses announcement banner — wire to a server action to persist. */
7
7
  onBannerDismiss?: (hash: string) => void;
8
+ /**
9
+ * Consumer's site identity. Pass this from whatever single source already
10
+ * owns it (this app uses the same `appkit.config.js` block that backs
11
+ * `SITE_IDENTITY` in the root layout). Without it the sections fall back to
12
+ * brand-neutral copy — which is correct for a library, and is why the
13
+ * renderer no longer hardcodes "LIR" or "LetItRip".
14
+ */
15
+ brand?: import("../lib/section-renderer").HomepageBrand;
8
16
  }
9
- export declare function MarketplaceHomepageView({ adSlots, newsletterFormSlot, onBannerDismiss, }?: MarketplaceHomepageViewProps): Promise<React.JSX.Element>;
17
+ export declare function MarketplaceHomepageView({ adSlots, newsletterFormSlot, onBannerDismiss, brand, }?: MarketplaceHomepageViewProps): Promise<React.JSX.Element>;
@@ -6,6 +6,7 @@ import { carouselRepository, faqsRepository, siteSettingsRepository } from "../.
6
6
  import { safeRead } from "../../../errors/safe-read";
7
7
  import { fetchLiveStats } from "../lib/live-stats";
8
8
  import { renderSection, AnnouncementBar } from "../lib/section-renderer";
9
+ import { SECTION_COPY } from "../constants/section-copy";
9
10
  import { homepageSectionsRepository } from "../repository/homepage-sections.repository";
10
11
  import { isListingTypeEnabled } from "../../../_internal/shared/listing-types/feature-flags";
11
12
  /**
@@ -38,7 +39,7 @@ function toCarouselSlides(docs) {
38
39
  updatedAt: updatedAt instanceof Date ? updatedAt.toISOString() : undefined,
39
40
  }));
40
41
  }
41
- export async function MarketplaceHomepageView({ adSlots, newsletterFormSlot, onBannerDismiss, } = {}) {
42
+ export async function MarketplaceHomepageView({ adSlots, newsletterFormSlot, onBannerDismiss, brand, } = {}) {
42
43
  // Every read on this page is a RAIL: the homepage must still render when one
43
44
  // fails, but a failure has to be distinguishable from an empty catalogue —
44
45
  // that is exactly Root Cause #59, and safeRead is what records it.
@@ -48,8 +49,10 @@ export async function MarketplaceHomepageView({ adSlots, newsletterFormSlot, onB
48
49
  const siteSettings = await safeRead(() => siteSettingsRepository.getSingleton(), {
49
50
  route: "/", key: "homepage.siteSettings", fallback: null,
50
51
  });
51
- const announcementMessage = siteSettings?.announcementBar?.message?.trim() ||
52
- "🎉 Up to 15% Off on Pokémon TCG this week — Use code SAVE15";
52
+ // The fallback used to be a hardcoded seasonal Pokémon TCG discount, which
53
+ // any install leaving the bar unconfigured would have published as live
54
+ // promotional copy for a promotion that does not exist.
55
+ const announcementMessage = siteSettings?.announcementBar?.message?.trim() || SECTION_COPY.announcementFallback;
53
56
  const showAnnouncement = siteSettings?.announcementBar?.enabled ?? true;
54
57
  const [allSections, rawFaqItems] = await Promise.all([
55
58
  safeRead(() => homepageSectionsRepository.getEnabledSections(), {
@@ -172,5 +175,5 @@ export async function MarketplaceHomepageView({ adSlots, newsletterFormSlot, onB
172
175
  : undefined,
173
176
  };
174
177
  const carouselSlides = toCarouselSlides(slides);
175
- return (_jsx(Main, { children: _jsxs(Div, { className: "relative", children: [showAnnouncement ? (_jsx(AnnouncementBar, { overlay: true, message: announcementMessage, link: siteSettings?.announcementBar?.link, onDismiss: onBannerDismiss })) : null, orderedSections.map((section) => renderSection(section, adSlots, newsletterFormSlot ?? null, faqItems, carouselSlides, liveStats, sectionData))] }) }));
178
+ return (_jsx(Main, { children: _jsxs(Div, { className: "relative", children: [showAnnouncement ? (_jsx(AnnouncementBar, { overlay: true, message: announcementMessage, link: siteSettings?.announcementBar?.link, onDismiss: onBannerDismiss })) : null, orderedSections.map((section) => renderSection(section, adSlots, newsletterFormSlot ?? null, faqItems, carouselSlides, liveStats, sectionData, brand))] }) }));
176
179
  }
@@ -9,6 +9,11 @@ export interface StatItem {
9
9
  }
10
10
  export interface StatsCounterSectionProps {
11
11
  stats: StatItem[];
12
+ /**
13
+ * Optional heading above the tiles. The section config has always carried a
14
+ * `title` and this component had nowhere to put it, so it never rendered.
15
+ */
16
+ title?: string;
12
17
  className?: string;
13
18
  }
14
- export declare function StatsCounterSection({ stats, className, }: StatsCounterSectionProps): import("react").JSX.Element;
19
+ export declare function StatsCounterSection({ stats, title, className, }: StatsCounterSectionProps): import("react").JSX.Element;
@@ -12,10 +12,10 @@ function StatCard({ stat, visible, delayClass, isLast, }) {
12
12
  delayClass,
13
13
  visible ? "translate-y-0 opacity-100" : "translate-y-6 opacity-0",
14
14
  !isLast ? "border-r border-[var(--appkit-color-border)]" : "",
15
- ].join(" "), children: [stat.renderIcon && (_jsx(Row, { className: "mb-4 h-14 w-14 bg-primary-50 dark:bg-primary-950/40 border border-[var(--appkit-color-primary-100)] dark:border-[var(--appkit-color-primary-900)]/30", align: "center", justify: "center", rounded: "2xl", children: stat.renderIcon({ className: "w-7 h-7" }) })), _jsx(Heading, { level: 2, variant: "none", gradient: "brand", className: "mb-1 font-display font-black", mdSize: "5xl", size: "4xl", children: stat.value }), _jsx(Text, { variant: "none", className: "uppercase tracking-widest", color: "muted", size: "xs", weight: "medium", children: stat.label })] }));
15
+ ].join(" "), children: [stat.renderIcon && (_jsx(Row, { className: "mb-4 h-14 w-14 bg-primary-50 dark:bg-primary-950/40 border border-[var(--appkit-color-primary-100)] dark:border-[var(--appkit-color-primary-900)]/30", align: "center", justify: "center", rounded: "2xl", children: stat.renderIcon({ className: "w-7 h-7" }) })), _jsx(Heading, { level: 3, variant: "none", gradient: "brand", className: "mb-1 font-display font-black", mdSize: "5xl", size: "4xl", children: stat.value }), _jsx(Text, { variant: "none", className: "uppercase tracking-widest", color: "muted", size: "xs", weight: "medium", children: stat.label })] }));
16
16
  }
17
17
  // --- Sectmon -----------------------------------------------------------------
18
- export function StatsCounterSection({ stats, className = "", }) {
18
+ export function StatsCounterSection({ stats, title, className = "", }) {
19
19
  const sectionRef = useRef(null);
20
20
  const [visible, setVisible] = useState(false);
21
21
  useEffect(() => {
@@ -31,5 +31,5 @@ export function StatsCounterSection({ stats, className = "", }) {
31
31
  observer.observe(el);
32
32
  return () => observer.disconnect();
33
33
  }, []);
34
- return (_jsx(Section, { border: "subtle", paddingY: "y-2-5xl", ref: sectionRef, className: `border-y ${className}`, paddingX: "x-md", surface: "default", children: _jsx(Div, { className: "mx-auto max-w-5xl", children: _jsx(Grid, { cols: "statTiles", gap: "none", children: stats.map((stat, i) => (_jsx(StatCard, { stat: stat, visible: visible, delayClass: STAT_DELAYS[Math.min(i, STAT_DELAYS.length - 1)], isLast: i === stats.length - 1 }, stat.key))) }) }) }));
34
+ return (_jsx(Section, { border: "subtle", paddingY: "y-2-5xl", ref: sectionRef, className: `border-y ${className}`, paddingX: "x-md", surface: "default", children: _jsxs(Div, { className: "mx-auto max-w-5xl", children: [title && (_jsx(Heading, { level: 2, size: "2xl", weight: "semibold", className: "mb-6 text-left", children: title })), _jsx(Grid, { cols: "statTiles", gap: "none", children: stats.map((stat, i) => (_jsx(StatCard, { stat: stat, visible: visible, delayClass: STAT_DELAYS[Math.min(i, STAT_DELAYS.length - 1)], isLast: i === stats.length - 1 }, stat.key))) })] }) }));
35
35
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * FAQ category → display label.
3
+ *
4
+ * Single owner. This map previously existed twice, byte-identical: once inline
5
+ * in `lib/section-renderer.tsx` and once as `CATEGORY_LABELS` in
6
+ * `components/FAQSection.tsx`. Adding a category meant two commits or a silent
7
+ * disagreement between the homepage strip's tabs and the section's own — the
8
+ * Duplication Framework's "bug-fix multiplier" trigger.
9
+ *
10
+ * Keyed loosely (`Record<string, string>`) rather than on `FAQCategoryKey`
11
+ * on purpose: `FAQCategoryKey` (7 values) and `FAQCategory` (8, including
12
+ * `scam_awareness`) have already drifted apart, and an unknown key here falls
13
+ * back to rendering the raw key rather than crashing a tab bar.
14
+ */
15
+ export declare const FAQ_CATEGORY_LABELS: Record<string, string>;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * FAQ category → display label.
3
+ *
4
+ * Single owner. This map previously existed twice, byte-identical: once inline
5
+ * in `lib/section-renderer.tsx` and once as `CATEGORY_LABELS` in
6
+ * `components/FAQSection.tsx`. Adding a category meant two commits or a silent
7
+ * disagreement between the homepage strip's tabs and the section's own — the
8
+ * Duplication Framework's "bug-fix multiplier" trigger.
9
+ *
10
+ * Keyed loosely (`Record<string, string>`) rather than on `FAQCategoryKey`
11
+ * on purpose: `FAQCategoryKey` (7 values) and `FAQCategory` (8, including
12
+ * `scam_awareness`) have already drifted apart, and an unknown key here falls
13
+ * back to rendering the raw key rather than crashing a tab bar.
14
+ */
15
+ export const FAQ_CATEGORY_LABELS = {
16
+ general: "General",
17
+ orders_payment: "Orders & Payment",
18
+ shipping_delivery: "Shipping",
19
+ returns_refunds: "Returns & Refunds",
20
+ product_information: "Products",
21
+ account_security: "Account",
22
+ technical_support: "Support",
23
+ };