@mohasinac/appkit 4.32.6 → 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 (82) 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/homepage/components/BlogArticlesSection.d.ts +7 -1
  30. package/dist/features/homepage/components/BlogArticlesSection.js +5 -3
  31. package/dist/features/homepage/components/CTABannerSection.d.ts +18 -1
  32. package/dist/features/homepage/components/CTABannerSection.js +22 -4
  33. package/dist/features/homepage/components/CustomerReviewsSection.d.ts +7 -1
  34. package/dist/features/homepage/components/CustomerReviewsSection.js +2 -2
  35. package/dist/features/homepage/components/FAQSection.js +2 -11
  36. package/dist/features/homepage/components/FeaturedAuctionsSection.d.ts +3 -1
  37. package/dist/features/homepage/components/FeaturedAuctionsSection.js +5 -3
  38. package/dist/features/homepage/components/FeaturedBundlesSection.d.ts +7 -1
  39. package/dist/features/homepage/components/FeaturedBundlesSection.js +6 -5
  40. package/dist/features/homepage/components/FeaturedPreOrdersSection.d.ts +3 -1
  41. package/dist/features/homepage/components/FeaturedPreOrdersSection.js +4 -2
  42. package/dist/features/homepage/components/FeaturedProductsSection.d.ts +3 -1
  43. package/dist/features/homepage/components/FeaturedProductsSection.js +5 -3
  44. package/dist/features/homepage/components/HeroCarousel.d.ts +23 -2
  45. package/dist/features/homepage/components/HeroCarousel.js +22 -9
  46. package/dist/features/homepage/components/HomepageCustomerReviewsSection.d.ts +6 -1
  47. package/dist/features/homepage/components/HomepageCustomerReviewsSection.js +11 -3
  48. package/dist/features/homepage/components/MarketplaceHomepageView.d.ts +9 -1
  49. package/dist/features/homepage/components/MarketplaceHomepageView.js +7 -4
  50. package/dist/features/homepage/components/StatsCounterSection.d.ts +6 -1
  51. package/dist/features/homepage/components/StatsCounterSection.js +3 -3
  52. package/dist/features/homepage/constants/faq-category-labels.d.ts +15 -0
  53. package/dist/features/homepage/constants/faq-category-labels.js +23 -0
  54. package/dist/features/homepage/constants/section-copy.d.ts +107 -0
  55. package/dist/features/homepage/constants/section-copy.js +96 -0
  56. package/dist/features/homepage/lib/section-renderer.d.ts +16 -1
  57. package/dist/features/homepage/lib/section-renderer.js +58 -37
  58. package/dist/features/homepage/repository/homepage-sections.repository.d.ts +23 -0
  59. package/dist/features/homepage/repository/homepage-sections.repository.js +40 -0
  60. package/dist/features/homepage/schemas/firestore.d.ts +120 -44
  61. package/dist/features/media/MediaImage.d.ts +10 -1
  62. package/dist/features/media/MediaImage.js +6 -3
  63. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -2
  64. package/dist/features/products/components/CardStoreLine.d.ts +32 -0
  65. package/dist/features/products/components/CardStoreLine.js +14 -0
  66. package/dist/features/products/components/MarketplacePrizeDrawCard.js +2 -1
  67. package/dist/features/products/components/ProductGrid.js +3 -6
  68. package/dist/features/products/schemas/firestore.d.ts +17 -0
  69. package/dist/features/products/types/index.d.ts +2 -0
  70. package/dist/features/seller/actions/seller-actions.d.ts +17 -0
  71. package/dist/features/seller/actions/seller-actions.js +54 -0
  72. package/dist/features/stores/components/StoreAuctionsListing.js +3 -0
  73. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +119 -0
  74. package/dist/seed/carousel-slides-seed-data.js +154 -22
  75. package/dist/seed/carousels-seed-data.js +4 -1
  76. package/dist/seed/homepage-sections-seed-data.js +337 -254
  77. package/dist/seed/products-preorders-seed-data.d.ts +1 -0
  78. package/dist/server.d.ts +1 -0
  79. package/dist/server.js +4 -0
  80. package/dist/styles.css +1 -1
  81. package/dist/tailwind-utilities.css +1 -1
  82. package/package.json +1 -1
@@ -152,19 +152,27 @@ export declare const carouselSlideQueryHelpers: {
152
152
  export declare function createCarouselId(title: string): string;
153
153
  export declare function isValidGridPosition(gridRow: number, gridCol: number): boolean;
154
154
  export declare function canActivateSlide(currentActiveCount: number): boolean;
155
- /** Config for the hero carousel section — references the carouselSlides collection. */
155
+ /**
156
+ * Config for the hero carousel section — references the carouselSlides collection.
157
+ *
158
+ * Every field here reaches `HeroCarousel` as a prop. It did not used to: the
159
+ * section renderer emitted `<HeroCarousel initialSlides={slides} />` and dropped
160
+ * the whole config, so all six controls were inert in the admin UI.
161
+ *
162
+ * `carouselId` was REMOVED. Nothing ever read it — SSR calls
163
+ * `carouselRepository.getActiveSlides()` (all active, capped at 5) and the
164
+ * client refetch goes through `useHeroCarousel`, and neither accepts an id.
165
+ * Honouring it means changing the repository, `GET /api/homepage/carousel` and
166
+ * the hook; leaving it in the schema advertised a capability that did not exist.
167
+ */
156
168
  export interface CarouselSectionConfig {
169
+ /** Accessible name for the carousel region. Not rendered as visible copy. */
157
170
  title?: string;
158
171
  height?: CarouselSlideHeight;
159
172
  defaultAutoplayDelayMs?: number;
160
173
  pauseOnHover?: boolean;
161
174
  showDots?: boolean;
162
175
  showArrows?: boolean;
163
- /**
164
- * ID of a named CarouselDocument (EX2).
165
- * When absent, the section loads all active slides (pre-EX2 default hero behavior).
166
- */
167
- carouselId?: string;
168
176
  }
169
177
  export interface WelcomeSectionConfig {
170
178
  h1: string;
@@ -173,8 +181,11 @@ export interface WelcomeSectionConfig {
173
181
  showCTA: boolean;
174
182
  ctaText?: string;
175
183
  ctaLink?: string;
176
- /** Optional section background — reuses the carousel's background schema for consistency. Video is ignored at this scope (color/gradient/image only); see Section's `background` prop. */
177
- background?: CarouselBackground;
184
+ /** Small eyebrow pill above the headline. Defaults to a generic label. */
185
+ pillLabel?: string;
186
+ /** Secondary CTA beside the primary one. */
187
+ secondaryCtaText?: string;
188
+ secondaryCtaLink?: string;
178
189
  }
179
190
  export type TrustIndicator = {
180
191
  id: string;
@@ -193,11 +204,14 @@ export interface SectionCTA {
193
204
  }
194
205
  export interface CategoriesSectionConfig {
195
206
  title: string;
196
- maxCategories: 4;
207
+ /** Cards to render. Was pinned to the literal `4`; widened so the cap is real. */
208
+ maxCategories?: number;
197
209
  autoScroll: boolean;
198
210
  scrollInterval: number;
199
211
  /** Circular auto-rotate at the carousel edges. Default: true. */
200
212
  loop?: boolean;
213
+ /** Label for the "view all" link. Defaults to a generic one. */
214
+ viewMoreLabel?: string;
201
215
  /** Optional CTA button rendered below the scroller. */
202
216
  cta?: SectionCTA;
203
217
  /** Optional client-side filter chips above the scroller. */
@@ -206,8 +220,6 @@ export interface CategoriesSectionConfig {
206
220
  rootOnly?: boolean;
207
221
  rootCategoryId?: string;
208
222
  };
209
- /** Optional section background — reuses the carousel's background schema for consistency. */
210
- background?: CarouselBackground;
211
223
  }
212
224
  export interface BrandsSectionConfig {
213
225
  title: string;
@@ -217,6 +229,8 @@ export interface BrandsSectionConfig {
217
229
  scrollInterval: number;
218
230
  /** Circular auto-rotate at the carousel edges. Default: true. */
219
231
  loop?: boolean;
232
+ /** Label for the "view all" link. Defaults to a generic one. */
233
+ viewMoreLabel?: string;
220
234
  /** Optional CTA button rendered below the scroller. */
221
235
  cta?: SectionCTA;
222
236
  /** Optional client-side filter chips above the scroller. */
@@ -229,6 +243,7 @@ export type SectionPagination = "load-more" | "arrows" | "auto-scroll";
229
243
  export interface ProductsSectionConfig {
230
244
  title: string;
231
245
  subtitle?: string;
246
+ /** @deprecated Legacy name the admin builder writes; prefer `maxItems`. */
232
247
  maxProducts?: number;
233
248
  /** Number of display rows (1–4). Each row shows itemsPerRow cards. Default: 1. */
234
249
  rows?: number;
@@ -240,6 +255,8 @@ export interface ProductsSectionConfig {
240
255
  pagination?: SectionPagination;
241
256
  autoScroll: boolean;
242
257
  scrollInterval: number;
258
+ /** Label for the "view all" link. Defaults to a generic one. */
259
+ viewMoreLabel?: string;
243
260
  filterByBrand?: string;
244
261
  sortBy?: "latest" | "oldest" | "priceLow" | "priceHigh" | "featured" | "onSale" | "popular";
245
262
  filterByCategory?: string;
@@ -288,32 +305,47 @@ export interface StatsSectionConfig {
288
305
  suffix?: string;
289
306
  }>;
290
307
  }
308
+ /**
309
+ * The count fields here were declared as LITERAL types (`maxAuctions: 18`,
310
+ * `rows: 2`, …), which made them unconfigurable by construction — an admin
311
+ * setting 12 was a type error, and the pre-order config already contradicted
312
+ * its own literals in the seed. They are plain optional numbers now that the
313
+ * renderer actually forwards them.
314
+ */
291
315
  export interface AuctionsSectionConfig {
292
316
  title: string;
293
317
  subtitle?: string;
294
- maxAuctions: 18;
295
- rows: 2;
296
- itemsPerRow: 3;
297
- mobileItemsPerRow: 1;
318
+ maxAuctions?: number;
319
+ rows?: number;
320
+ itemsPerRow?: number;
321
+ mobileItemsPerRow?: number;
298
322
  autoScroll: boolean;
299
323
  scrollInterval: number;
324
+ /** Label for the "view all" link. Defaults to a generic one. */
325
+ viewMoreLabel?: string;
300
326
  filterByBrand?: string;
301
327
  sortBy?: "latest" | "oldest" | "priceLow" | "priceHigh" | "featured" | "onSale" | "popular";
302
328
  filterByCategory?: string;
303
329
  maxCount?: 5 | 10 | 20;
304
330
  loop?: boolean;
305
331
  }
332
+ /**
333
+ * `source` and `placeId` were REMOVED. They implied this section could render
334
+ * Google reviews, which it never could — `HomepageCustomerReviewsSection` only
335
+ * ever calls `useHomepageReviews()`. The `google-reviews` section type is the
336
+ * real surface for that, and it has its own config with a required `placeId`.
337
+ */
306
338
  export interface ReviewsSectionConfig {
307
339
  title: string;
308
- maxReviews: 18;
309
- itemsPerView: 3;
310
- mobileItemsPerView: 1;
340
+ maxReviews?: number;
341
+ itemsPerView?: number;
342
+ mobileItemsPerView?: number;
311
343
  autoScroll: boolean;
312
344
  scrollInterval: number;
313
345
  /** Circular auto-rotate at the carousel edges. Default: true. */
314
346
  loop?: boolean;
315
- source?: "platform" | "google";
316
- placeId?: string;
347
+ /** Label for the "view all" link. Defaults to a generic one. */
348
+ viewMoreLabel?: string;
317
349
  }
318
350
  export interface WhatsAppCommunitySectionConfig {
319
351
  title: string;
@@ -324,9 +356,30 @@ export interface WhatsAppCommunitySectionConfig {
324
356
  buttonText: string;
325
357
  testimonial?: string;
326
358
  }
359
+ export interface FeatureItem {
360
+ key: string;
361
+ title: string;
362
+ description: string;
363
+ }
364
+ /**
365
+ * `features: string[]` could never reach `SecurityHighlightsSection`, which
366
+ * needs `{key, title, description}` cards — so the renderer silently fell back
367
+ * to `DEFAULT_SECURITY_ITEMS` and the configured list was dead.
368
+ *
369
+ * `items` is the real shape. `features` is kept readable for back-compat with
370
+ * documents written before this change; when only `features` is present each
371
+ * string becomes a title-only card.
372
+ */
327
373
  export interface FeaturesSectionConfig {
328
374
  title: string;
329
- features: string[];
375
+ items?: FeatureItem[];
376
+ /** @deprecated Use `items`. Title-only cards. */
377
+ features?: string[];
378
+ /** Small eyebrow pill above the heading. */
379
+ pillLabel?: string;
380
+ /** "Learn more" link below the cards. */
381
+ learnMoreLabel?: string;
382
+ learnMoreLink?: string;
330
383
  }
331
384
  export type FAQCategoryKey = "general" | "orders_payment" | "shipping_delivery" | "returns_refunds" | "product_information" | "account_security" | "technical_support";
332
385
  export interface FAQSectionConfig {
@@ -345,19 +398,28 @@ export interface FAQSectionConfig {
345
398
  linkToFullPage: boolean;
346
399
  /** Which FAQ categories to pull items from for this section. */
347
400
  categories: FAQCategoryKey[];
401
+ /** Label for the "view all FAQs" link. Defaults to a generic one. */
402
+ viewMoreLabel?: string;
348
403
  }
349
404
  export interface BlogArticlesSectionConfig {
350
405
  title: string;
351
- maxArticles: 4;
352
- showReadTime: boolean;
353
- showAuthor: boolean;
354
- showThumbnails: boolean;
406
+ /** Was the literal `4`, so the cap could not be changed. */
407
+ maxArticles?: number;
408
+ showReadTime?: boolean;
409
+ showAuthor?: boolean;
410
+ showThumbnails?: boolean;
411
+ /** Label for the "view all" link. Defaults to a generic one. */
412
+ viewMoreLabel?: string;
355
413
  }
414
+ /**
415
+ * `placeholder` and `buttonText` were REMOVED. The form itself is supplied by
416
+ * the consumer through `MarketplaceHomepageViewProps.newsletterFormSlot` — the
417
+ * section only renders the surrounding band — so appkit has no element to apply
418
+ * them to. They were controls that could never affect anything.
419
+ */
356
420
  export interface NewsletterSectionConfig {
357
421
  title: string;
358
422
  description: string;
359
- placeholder: string;
360
- buttonText: string;
361
423
  privacyText: string;
362
424
  privacyLink: string;
363
425
  }
@@ -367,12 +429,12 @@ export interface StoresSectionConfig {
367
429
  maxStores: number;
368
430
  autoScroll: boolean;
369
431
  scrollInterval: number;
432
+ /** Label for the "view all" link. Defaults to a generic one. */
433
+ viewMoreLabel?: string;
370
434
  sortBy?: "latest" | "oldest" | "priceLow" | "priceHigh" | "featured" | "onSale" | "popular";
371
435
  filterByCategory?: string;
372
436
  maxCount?: 5 | 10 | 20;
373
437
  loop?: boolean;
374
- /** Optional section background — reuses the carousel's background schema for consistency. */
375
- background?: CarouselBackground;
376
438
  }
377
439
  export interface EventsSectionConfig {
378
440
  title: string;
@@ -380,12 +442,12 @@ export interface EventsSectionConfig {
380
442
  maxEvents: number;
381
443
  autoScroll: boolean;
382
444
  scrollInterval: number;
445
+ /** Label for the "view all" link. Defaults to a generic one. */
446
+ viewMoreLabel?: string;
383
447
  sortBy?: "latest" | "oldest" | "priceLow" | "priceHigh" | "featured" | "onSale" | "popular";
384
448
  filterByCategory?: string;
385
449
  maxCount?: 5 | 10 | 20;
386
450
  loop?: boolean;
387
- /** Optional section background — reuses the carousel's background schema for consistency. */
388
- background?: CarouselBackground;
389
451
  }
390
452
  export type SocialPlatform = "instagram" | "facebook" | "tiktok" | "deviantart" | "youtube";
391
453
  export type SocialPostType = "all" | "images" | "videos" | "reels";
@@ -446,23 +508,35 @@ export type SocialPost = {
446
508
  export interface PreOrdersSectionConfig {
447
509
  title: string;
448
510
  subtitle?: string;
449
- maxItems: number;
450
- rows: 2;
451
- itemsPerRow: 3;
452
- mobileItemsPerRow: 1;
511
+ maxItems?: number;
512
+ rows?: number;
513
+ itemsPerRow?: number;
514
+ mobileItemsPerRow?: number;
453
515
  autoScroll: boolean;
454
516
  scrollInterval: number;
517
+ /** Label for the "view all" link. Defaults to a generic one. */
518
+ viewMoreLabel?: string;
455
519
  filterByBrand?: string;
456
520
  sortBy?: "latest" | "oldest" | "priceLow" | "priceHigh" | "featured" | "onSale" | "popular";
457
521
  filterByCategory?: string;
458
522
  maxCount?: 5 | 10 | 20;
459
523
  loop?: boolean;
460
524
  }
525
+ /**
526
+ * Four fields were REMOVED here:
527
+ *
528
+ * - `backgroundColor` / `gradient`. The band's copy is fixed inverse (white)
529
+ * ink, so an arbitrary admin-chosen fill has no safe outcome — that is
530
+ * Root Cause #67 exactly. `backgroundImage` survives because it renders
531
+ * under a scrim, which guarantees the contrast. The themed `accent-banner`
532
+ * tone already gives a brand gradient that follows the active theme.
533
+ * - `clickable` / `clickLink`. A link wrapping the whole banner would nest the
534
+ * two CTA buttons inside an anchor, which is an a11y violation and
535
+ * ambiguous besides. The buttons are the click target.
536
+ */
461
537
  export interface BannerSectionConfig {
462
538
  height: "sm" | "md" | "lg" | "xl";
463
539
  backgroundImage?: string;
464
- backgroundColor?: string;
465
- gradient?: string;
466
540
  content: {
467
541
  title: string;
468
542
  subtitle?: string;
@@ -473,8 +547,6 @@ export interface BannerSectionConfig {
473
547
  link: string;
474
548
  variant: "primary" | "secondary" | "outline";
475
549
  }>;
476
- clickable: boolean;
477
- clickLink?: string;
478
550
  }
479
551
  export type CustomCardsCard = {
480
552
  id: string;
@@ -524,18 +596,22 @@ export interface CollectionCardsSectionConfig {
524
596
  maxItems?: number;
525
597
  showCollectionTabs?: boolean;
526
598
  cta?: SectionCTA;
527
- /** Optional section background — reuses the carousel's background schema for consistency. */
528
- background?: CarouselBackground;
529
599
  }
530
600
  export declare const COLLECTION_CARDS_MAX_ENTRIES: 3;
601
+ /**
602
+ * `storeId`, `categorySlug` and `sortBy` were REMOVED. Bundles arrive
603
+ * pre-fetched from `listFeaturedBundles(8)` in `MarketplaceHomepageView`, which
604
+ * takes no scope or sort argument — honouring them means changing that fetch,
605
+ * and a per-section fetch would breach the SSR round-trip budget (Rule #6).
606
+ * `maxItems` survives because slicing an already-fetched list is real.
607
+ */
531
608
  export interface FeaturedBundlesSectionConfig {
532
609
  title?: string;
533
610
  subtitle?: string;
534
611
  maxItems?: number;
535
- storeId?: string;
536
- categorySlug?: string;
537
- sortBy?: "newest" | "savings-desc" | "price-asc";
538
612
  showSavingsBadge?: boolean;
613
+ /** Label for the "view all" link. Defaults to a generic one. */
614
+ viewMoreLabel?: string;
539
615
  }
540
616
  export interface PrizeDrawsSectionConfig {
541
617
  title?: string;
@@ -15,6 +15,15 @@ export interface MediaImageProps {
15
15
  priority?: boolean;
16
16
  /** Override browser loading strategy when needed (`eager` for LCP images). */
17
17
  loading?: "lazy" | "eager";
18
+ /**
19
+ * Explicit `fetchpriority` hint. Defaults to what `priority` implies.
20
+ *
21
+ * Set `"low"` on eagerly-loaded images that are NOT the LCP candidate — a
22
+ * carousel's off-screen slides are the motivating case. All five hero banners
23
+ * otherwise race each other at the browser's default priority and delay the
24
+ * one slide the visitor can actually see.
25
+ */
26
+ fetchPriority?: "high" | "low" | "auto";
18
27
  /** CSS object-fit applied to the underlying img element. Defaults to `'cover'`. */
19
28
  objectFit?: "cover" | "contain";
20
29
  /**
@@ -66,5 +75,5 @@ export interface MediaImageProps {
66
75
  media?: string;
67
76
  }>;
68
77
  }
69
- export declare function MediaImage({ src, alt, size, priority, loading, objectFit, fallback, className, rounded, shadow, style, sources, onError, }: MediaImageProps): import("react").JSX.Element;
78
+ export declare function MediaImage({ src, alt, size, priority, loading, fetchPriority, objectFit, fallback, className, rounded, shadow, style, sources, onError, }: MediaImageProps): import("react").JSX.Element;
70
79
  export default MediaImage;
@@ -48,7 +48,7 @@ function resolveSrcSet(input) {
48
48
  .filter(Boolean)
49
49
  .join(", ");
50
50
  }
51
- export function MediaImage({ src, alt, size = "card", priority = false, loading, objectFit = "cover", fallback, className, rounded, shadow, style, sources, onError, }) {
51
+ export function MediaImage({ src, alt, size = "card", priority = false, loading, fetchPriority, objectFit = "cover", fallback, className, rounded, shadow, style, sources, onError, }) {
52
52
  const [hasError, setHasError] = useState(false);
53
53
  const [isLoaded, setIsLoaded] = useState(false);
54
54
  const [retryCount, setRetryCount] = useState(0);
@@ -98,15 +98,18 @@ export function MediaImage({ src, alt, size = "card", priority = false, loading,
98
98
  const requestSrc = retryCount > 0
99
99
  ? `${resolvedSrc}${resolvedSrc.includes("?") ? "&" : "?"}retry=${retryCount}`
100
100
  : resolvedSrc;
101
+ // An explicit hint always wins; otherwise mirror what `priority` already
102
+ // implies, so existing call sites keep their current behaviour exactly.
103
+ const resolvedFetchPriority = fetchPriority ?? (priority ? "high" : undefined);
101
104
  // Art-directed `<picture>` path — bypasses Next.js `<Image>` because the
102
105
  // browser must pick the matching `<source>` itself. Loading + decoding hints
103
106
  // come from the consumer's preset.
104
107
  if (sources && sources.length > 0) {
105
- return (_jsxs(Div, { className: `relative w-full h-full overflow-hidden${wrapperExtra ? ` ${wrapperExtra}` : ""}`, style: style, children: [!isLoaded && (_jsx(Div, { className: "absolute inset-0 animate-pulse", surface: "subtle", "aria-hidden": "true" })), _jsxs("picture", { children: [sources.map((source, index) => (_jsx("source", { srcSet: resolveSrcSet(source.srcSet), type: source.type, media: source.media }, `${source.media ?? ""}-${source.type ?? ""}-${index}`))), _jsx("img", { src: requestSrc, alt: alt, loading: loading ?? (priority ? "eager" : "lazy"), decoding: "async",
108
+ return (_jsxs(Div, { className: `relative w-full h-full overflow-hidden${wrapperExtra ? ` ${wrapperExtra}` : ""}`, style: style, children: [!isLoaded && (_jsx(Div, { className: "absolute inset-0 animate-pulse", surface: "subtle", "aria-hidden": "true" })), _jsxs("picture", { children: [sources.map((source, index) => (_jsx("source", { srcSet: resolveSrcSet(source.srcSet), type: source.type, media: source.media }, `${source.media ?? ""}-${source.type ?? ""}-${index}`))), _jsx("img", { src: requestSrc, alt: alt, loading: loading ?? (priority ? "eager" : "lazy"), fetchPriority: resolvedFetchPriority, decoding: "async",
106
109
  // catalogued primitive for media (variant catalogue allows the inner
107
110
  // <img> here). Object-fit + className come from typed props.
108
111
  className: `absolute inset-0 w-full h-full ${fitClass}`, onLoad: () => setIsLoaded(true), onError: handleError })] })] }));
109
112
  }
110
- return (_jsxs(Div, { className: `relative w-full h-full overflow-hidden${wrapperExtra ? ` ${wrapperExtra}` : ""}`, style: style, children: [!isLoaded && (_jsx(Div, { className: "absolute inset-0 animate-pulse", surface: "subtle", "aria-hidden": "true" })), _jsx(Image, { src: requestSrc, alt: alt, fill: true, priority: priority, loading: loading ?? (priority ? "eager" : "lazy"), className: fitClass, sizes: SIZE_HINTS[size], onLoad: () => setIsLoaded(true), onError: handleError, unoptimized: isSvg })] }));
113
+ return (_jsxs(Div, { className: `relative w-full h-full overflow-hidden${wrapperExtra ? ` ${wrapperExtra}` : ""}`, style: style, children: [!isLoaded && (_jsx(Div, { className: "absolute inset-0 animate-pulse", surface: "subtle", "aria-hidden": "true" })), _jsx(Image, { src: requestSrc, alt: alt, fill: true, priority: priority, fetchPriority: resolvedFetchPriority, loading: loading ?? (priority ? "eager" : "lazy"), className: fitClass, sizes: SIZE_HINTS[size], onLoad: () => setIsLoaded(true), onError: handleError, unoptimized: isSvg })] }));
111
114
  }
112
115
  export default MediaImage;
@@ -18,6 +18,7 @@ import { BaseListingCard, Button, RichText, Row, Span, Stack, Text, TextLink } f
18
18
  import { normalizeRichTextHtml } from "../../../utils/string.formatter";
19
19
  import { PreorderBadge } from "./PreorderCard";
20
20
  import { WishlistHeartButton } from "../../wishlist/components/WishlistHeartButton";
21
+ import { CardStoreLine } from "../../products/components/CardStoreLine";
21
22
  const DEFAULT_LABELS = {
22
23
  preOrderBadge: "Pre-order",
23
24
  featuredBadge: "Featured",
@@ -65,7 +66,7 @@ export function MarketplacePreorderCard({ product, className = "", variant = "gr
65
66
  onSelect(product.id, !isSelected);
66
67
  }, className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" }))] }), _jsx(BaseListingCard.Info, { variant: variant, children: variant === "list" ? (
67
68
  /* ── Compact list layout ── */
68
- _jsxs(_Fragment, { children: [_jsxs(Row, { className: "min-w-0", align: "start", justify: "between", gap: "sm", children: [_jsx(TextLink, { href: detailHref, className: "min-w-0 flex-1", children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), wishlistActions && (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist }))] }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Text, { className: "text-primary", size: "sm", weight: "bold", children: formatCurrency(product.price, getDefaultCurrency()) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "self-start mt-0.5", onClick: handleNavigate, children: mergedLabels.reserveNow })] })) : (
69
+ _jsxs(_Fragment, { children: [_jsxs(Row, { className: "min-w-0", align: "start", justify: "between", gap: "sm", children: [_jsx(TextLink, { href: detailHref, className: "min-w-0 flex-1", children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), wishlistActions && (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist }))] }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Text, { className: "text-primary", size: "sm", weight: "bold", children: formatCurrency(product.price, getDefaultCurrency()) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "self-start mt-0.5", onClick: handleNavigate, children: mergedLabels.reserveNow })] })) : (
69
70
  /* ── Full grid layout ── */
70
- _jsxs(_Fragment, { children: [_jsx(TextLink, { href: detailHref, children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), product.description ? (_jsx(RichText, { html: normalizeRichTextHtml(product.description), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "line-clamp-2 text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)]" })) : null, _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: formatCurrency(product.price, getDefaultCurrency()) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsxs(Row, { justify: "between", className: "mt-auto pt-[var(--appkit-space-2)]", gap: "sm", children: [_jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "flex-1", onClick: handleNavigate, children: mergedLabels.reserveNow }), onAddToCart ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", textSize: "xs", className: "flex-1", onClick: (e) => { e.stopPropagation(); onAddToCart(product); }, children: mergedLabels.addToCart })) : null, wishlistActions ? (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist })) : null] })] })) })] }));
71
+ _jsxs(_Fragment, { children: [_jsx(TextLink, { href: detailHref, children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), product.description ? (_jsx(RichText, { html: normalizeRichTextHtml(product.description), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "line-clamp-2 text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)]" })) : null, _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: formatCurrency(product.price, getDefaultCurrency()) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsxs(Row, { justify: "between", className: "mt-auto pt-[var(--appkit-space-2)]", gap: "sm", children: [_jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "flex-1", onClick: handleNavigate, children: mergedLabels.reserveNow }), onAddToCart ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", textSize: "xs", className: "flex-1", onClick: (e) => { e.stopPropagation(); onAddToCart(product); }, children: mergedLabels.addToCart })) : null, wishlistActions ? (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist })) : null] })] })) })] }));
71
72
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * CardStoreLine — the "by <seller>" line on a listing card.
3
+ *
4
+ * One implementation because there are six card variants that need it and only
5
+ * ONE of them rendered it: `ProductCard` had this block inline while
6
+ * `ProductListRow`, `MarketplacePreorderCard`, `MarketplacePrizeDrawCard`,
7
+ * `PreorderCard` and `MarketplaceAuctionCard` showed no seller at all, even
8
+ * when the data was right there on the item. Six copies of a three-line block
9
+ * is six chances for the fallback rule and the muted tone to drift.
10
+ *
11
+ * Deliberately NOT a link. A listing card is itself wrapped in a `<Link>` to the
12
+ * product, and nesting an anchor inside an anchor is invalid HTML that browsers
13
+ * resolve by splitting the outer link — which is how a card loses its own click
14
+ * target (the shape of Root Cause #43). The seller is reachable from the product
15
+ * detail page, which does link it.
16
+ */
17
+ import React from "react";
18
+ export interface CardStoreLineProps {
19
+ storeName?: string;
20
+ /**
21
+ * Present when the listing has a seller at all, whether or not its display
22
+ * name was denormalized onto the document.
23
+ *
24
+ * This is the distinction the old inline block could not make: it called
25
+ * `safeDisplayName(storeName, "")`, so a missing name produced `""` and the
26
+ * line rendered nothing — indistinguishable from "this listing has no store".
27
+ * With the id in hand, a listing that HAS a seller says so.
28
+ */
29
+ storeId?: string;
30
+ className?: string;
31
+ }
32
+ export declare function CardStoreLine({ storeName, storeId, className }: CardStoreLineProps): React.JSX.Element | null;
@@ -0,0 +1,14 @@
1
+ "use client";
2
+ import { jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Text } from "../../../ui/components/Typography";
4
+ import { safeDisplayName } from "../../../security/pii-redact";
5
+ export function CardStoreLine({ storeName, storeId, className }) {
6
+ if (!storeId && !storeName)
7
+ return null;
8
+ // `safeDisplayName` also guards the encrypted-value case: a PII-ciphertext
9
+ // string reaching a card must render as a label, never as `enc:v1:…`.
10
+ // `storeId` IS the store slug in this codebase, so it is a readable last
11
+ // resort rather than an opaque key.
12
+ const seller = safeDisplayName(storeName, storeId ?? "Seller");
13
+ return (_jsxs(Text, { className: className ?? "mt-0.5 text-[11px]", color: "faint", children: ["by ", seller] }));
14
+ }
@@ -10,6 +10,7 @@ import { getDefaultCurrency } from "../../../core/baseline-resolver";
10
10
  const CLS_PRIZE_PILL = "inline-flex items-center rounded-full bg-fuchsia-600 px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-white";
11
11
  import { BaseListingCard, Button, Div, Row, Span, Stack, Text, TextLink } from "../../../ui";
12
12
  import { MediaImage } from "../../media/MediaImage";
13
+ import { CardStoreLine } from "./CardStoreLine";
13
14
  const DEFAULT_LABELS = {
14
15
  prizeDrawBadge: "Prize draw",
15
16
  pendingBadge: "Reveal pending",
@@ -98,5 +99,5 @@ export function MarketplacePrizeDrawCard({ product, className = "", variant = "g
98
99
  onSelect(product.id, !isSelected);
99
100
  }, className: selectable || isSelected
100
101
  ? "opacity-100"
101
- : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" }))] }), _jsxs(BaseListingCard.Info, { variant: variant, children: [_jsx(TextLink, { href: String(detailHref), children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsxs(Text, { size: "sm", weight: "semibold", color: "primary", children: [formatCurrency(pricePerEntry, getDefaultCurrency()), " ", _jsx(Span, { size: "xs", weight: "normal", className: "text-[var(--appkit-color-text-muted)]", children: mergedLabels.pricePerEntryLabel })] }), max > 0 ? (_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: mergedLabels.entriesRemainingLabel(remaining, max) })) : null] }), countdown ? (_jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: [status === "pending" ? "Opens in" : "Closes in", " ", countdown] })) : null, _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "mt-2 w-full", onClick: handleNavigate, disabled: status === "closed" || remaining === 0, children: mergedLabels.enterDraw })] })] }));
102
+ : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" }))] }), _jsxs(BaseListingCard.Info, { variant: variant, children: [_jsx(TextLink, { href: String(detailHref), children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsxs(Text, { size: "sm", weight: "semibold", color: "primary", children: [formatCurrency(pricePerEntry, getDefaultCurrency()), " ", _jsx(Span, { size: "xs", weight: "normal", className: "text-[var(--appkit-color-text-muted)]", children: mergedLabels.pricePerEntryLabel })] }), max > 0 ? (_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: mergedLabels.entriesRemainingLabel(remaining, max) })) : null] }), countdown ? (_jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: [status === "pending" ? "Opens in" : "Closes in", " ", countdown] })) : null, _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "mt-2 w-full", onClick: handleNavigate, disabled: status === "closed" || remaining === 0, children: mergedLabels.enterDraw })] })] }));
102
103
  }
@@ -8,8 +8,8 @@ import { BaseListingCard, Button, Div, Grid, PriceDisplay, Row, Span, Stack, Tex
8
8
  import { MediaImage } from "../../media/MediaImage";
9
9
  import { formatCurrency } from "../../../utils/number.formatter";
10
10
  import { getDefaultCurrency } from "../../../core/baseline-resolver";
11
- import { safeDisplayName } from "../../../security";
12
11
  import { useLongPress } from "../../../react/hooks/useLongPress";
12
+ import { CardStoreLine } from "./CardStoreLine";
13
13
  import { FeatureBadgeList } from "./FeatureBadge";
14
14
  import { useProductFeatures } from "./ProductFeaturesContext";
15
15
  import { PRODUCT_FEATURE_CARD_MAX_VISIBLE } from "../constants/product-features.constants";
@@ -97,10 +97,7 @@ export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlis
97
97
  },
98
98
  }
99
99
  : {}), children: [_jsx("svg", { className: "h-2.5 w-2.5 flex-shrink-0", fill: "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" }) }), product.brand] }))] }));
100
- })(), (() => {
101
- const seller = safeDisplayName(product.storeName, "");
102
- return seller ? (_jsxs(Text, { className: "mt-0.5 text-[11px]", color: "faint", children: ["by ", seller] })) : null;
103
- })(), product.rating !== undefined && (_jsxs(Row, { className: "mt-1", gap: "xs", children: [_jsx(Span, { className: CLS_STAR, children: "\u2605" }), _jsxs(Span, { className: "text-[11px]", color: "muted", children: [product.rating.toFixed(1), product.reviewCount ? ` (${product.reviewCount})` : ""] })] })), _jsxs(Div, { className: "mt-auto", padding: "t-xs", children: [_jsx(PriceDisplay, { amount: product.price, originalAmount: product.originalPrice, currency: getDefaultCurrency(), variant: "compact" }), (() => {
100
+ })(), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), product.rating !== undefined && (_jsxs(Row, { className: "mt-1", gap: "xs", children: [_jsx(Span, { className: CLS_STAR, children: "\u2605" }), _jsxs(Span, { className: "text-[11px]", color: "muted", children: [product.rating.toFixed(1), product.reviewCount ? ` (${product.reviewCount})` : ""] })] })), _jsxs(Div, { className: "mt-auto", padding: "t-xs", children: [_jsx(PriceDisplay, { amount: product.price, originalAmount: product.originalPrice, currency: getDefaultCurrency(), variant: "compact" }), (() => {
104
101
  const stock = product.stockCount ?? product.stockQuantity ?? product.availableQuantity;
105
102
  if (stock === undefined || !listingPlugin.showsStockQuantity)
106
103
  return null;
@@ -136,7 +133,7 @@ function ProductListRow({ product, onClick, onAddToWishlist, isWishlisted, }) {
136
133
  onClick ? "cursor-pointer" : "",
137
134
  ]
138
135
  .filter(Boolean)
139
- .join(" "), children: [_jsx(Div, { className: `flex-shrink-0 w-16 h-16 sm:w-20 sm:h-20 ${__O.hidden} bg-[var(--appkit-color-surface)]`, rounded: "lg", children: product.mainImage ? (_jsx(MediaImage, { src: product.mainImage, alt: product.title, size: "thumbnail" })) : (_jsx(Div, { className: "w-full h-full bg-neutral-200" })) }), _jsxs(Stack, { className: "flex-1 min-w-0 gap-[2px]", children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }), (product.categoryName || product.brand) && (_jsx(Span, { className: "text-[11px] truncate", color: "faint", children: [product.categoryName, product.brand].filter(Boolean).join(" · ") })), _jsxs(Row, { className: "mt-0.5", align: "center", gap: "sm", wrap: true, children: [_jsx(Span, { size: "sm", weight: "semibold", className: "text-primary", children: formatCurrency(product.price, getDefaultCurrency()) }), discount && (_jsxs(Span, { weight: "bold", className: CLS_DISCOUNT_TEXT_BARE, children: ["-", discount, "%"] })), product.rating !== undefined && (_jsxs(Span, { layout: "flex", gap: "2xs", className: "text-[11px]", color: "faint", children: [_jsx(Span, { className: CLS_STAR_BARE, children: "\u2605" }), product.rating.toFixed(1)] })), (() => {
136
+ .join(" "), children: [_jsx(Div, { className: `flex-shrink-0 w-16 h-16 sm:w-20 sm:h-20 ${__O.hidden} bg-[var(--appkit-color-surface)]`, rounded: "lg", children: product.mainImage ? (_jsx(MediaImage, { src: product.mainImage, alt: product.title, size: "thumbnail" })) : (_jsx(Div, { className: "w-full h-full bg-neutral-200" })) }), _jsxs(Stack, { className: "flex-1 min-w-0 gap-[2px]", children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }), (product.categoryName || product.brand) && (_jsx(Span, { className: "text-[11px] truncate", color: "faint", children: [product.categoryName, product.brand].filter(Boolean).join(" · ") })), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId, className: "text-[11px]" }), _jsxs(Row, { className: "mt-0.5", align: "center", gap: "sm", wrap: true, children: [_jsx(Span, { size: "sm", weight: "semibold", className: "text-primary", children: formatCurrency(product.price, getDefaultCurrency()) }), discount && (_jsxs(Span, { weight: "bold", className: CLS_DISCOUNT_TEXT_BARE, children: ["-", discount, "%"] })), product.rating !== undefined && (_jsxs(Span, { layout: "flex", gap: "2xs", className: "text-[11px]", color: "faint", children: [_jsx(Span, { className: CLS_STAR_BARE, children: "\u2605" }), product.rating.toFixed(1)] })), (() => {
140
137
  const stock = product.stockCount ?? product.stockQuantity ?? product.availableQuantity;
141
138
  if (stock === undefined)
142
139
  return null;
@@ -155,7 +155,24 @@ export interface ProductDocument extends BaseDocument {
155
155
  video?: ProductVideoField;
156
156
  status: ProductStatus;
157
157
  storeId: string;
158
+ /**
159
+ * Denormalized seller display name — what every listing card renders as
160
+ * "by …", and the only place a card can get it: the public browse query
161
+ * never touches `storeRepository`.
162
+ *
163
+ * Optional for back-compat with documents written before the create paths
164
+ * populated it, NOT because it is optional in practice. `resolveStoreFields`
165
+ * fills it on every create, and a store rename re-syncs it through the
166
+ * `storeNameBackfill` job — that job is the mirror-guard, and removing it
167
+ * turns this into a field that silently goes stale (Root Cause #42).
168
+ */
158
169
  storeName?: string;
170
+ /**
171
+ * Denormalized `storeSlug`, so a card or a detail page can build a
172
+ * `/stores/{slug}` link without a second read. Written alongside `storeName`
173
+ * and re-synced by the same job.
174
+ */
175
+ storeSlug?: string;
159
176
  /** W1-34 — denormalized seller rating (0–5) shown on product detail store card. */
160
177
  storeRating?: number;
161
178
  /** W1-34 — denormalized seller total review count shown alongside the rating. */
@@ -98,6 +98,8 @@ export interface ProductItem {
98
98
  brands?: string[];
99
99
  storeId?: string;
100
100
  storeName?: string;
101
+ /** Denormalized store slug — lets a card name its seller with no second read. */
102
+ storeSlug?: string;
101
103
  specifications?: {
102
104
  name: string;
103
105
  value: string;
@@ -113,6 +113,23 @@ export declare function updatePayoutSettings(userId: string, input: UpdatePayout
113
113
  }>;
114
114
  export declare function requestPayout(userId: string, userName: string, userEmail: string, input: RequestPayoutInput): Promise<unknown>;
115
115
  export declare function bulkSellerOrder(userId: string, userRole: string, userDisplayName: string, userEmail: string, orderIds: string[]): Promise<BulkSellerOrderResult>;
116
+ /**
117
+ * The real "create a listing" path — every seller `new` page under `/store`
118
+ * routes here.
119
+ *
120
+ * The store fields are resolved from the OWNER, never taken from the draft.
121
+ * `SellerProductDraft` has no `storeId`, `storeName` or `storeSlug` field at
122
+ * all, so this used to write `sellerId`/`sellerName`/`sellerEmail` and nothing
123
+ * else about the store — leaving every seller-created listing with no `storeId`
124
+ * whatsoever. That is not just a blank "by …" line on the card: `storeId` is
125
+ * what the store page queries on, what `splitCartIntoOrderGroups` groups on,
126
+ * and what a payout hangs off. Seed data set all three by hand, so a seeded
127
+ * environment looked entirely correct.
128
+ *
129
+ * Resolving server-side rather than accepting them from the client is also the
130
+ * only safe shape — a caller-supplied `storeId` would let a seller file a
131
+ * listing under someone else's store.
132
+ */
116
133
  export declare function createSellerProduct(userId: string, userName: string, userEmail: string, input: Record<string, JsonValue>): Promise<void>;
117
134
  export declare function getSellerStore(userId: string): Promise<StoreDocument | null>;
118
135
  export declare function getSellerShipping(userId: string): Promise<import("../../auth").SellerShippingConfig | null>;