@mohasinac/appkit 4.12.0 → 4.12.2

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 (68) hide show
  1. package/dist/_internal/server/features/auctions/service.d.ts +7 -1
  2. package/dist/_internal/server/features/auctions/service.js +13 -5
  3. package/dist/_internal/server/features/checkout/actions.d.ts +0 -15
  4. package/dist/_internal/server/features/checkout/actions.js +7 -3
  5. package/dist/_internal/server/features/orders/actions.js +5 -3
  6. package/dist/_internal/server/functions/firestore.d.ts +1 -2
  7. package/dist/_internal/server/functions/firestore.js +1 -9
  8. package/dist/_internal/server/jobs/core/index.d.ts +0 -1
  9. package/dist/_internal/server/jobs/core/index.js +0 -1
  10. package/dist/_internal/server/jobs/core/offerExpiry.js +33 -1
  11. package/dist/_internal/server/jobs/handlers/index.d.ts +1 -2
  12. package/dist/_internal/server/jobs/handlers/index.js +1 -2
  13. package/dist/_internal/shared/features/auctions/config.d.ts +34 -0
  14. package/dist/_internal/shared/features/auctions/config.js +47 -3
  15. package/dist/_internal/shared/listing-types/auction/config.js +1 -1
  16. package/dist/_internal/shared/listing-types/digital-code/config.js +1 -1
  17. package/dist/_internal/shared/listing-types/live/config.js +1 -1
  18. package/dist/_internal/shared/listing-types/pre-order/config.js +1 -1
  19. package/dist/features/account/components/NotificationBell.js +1 -1
  20. package/dist/features/admin/components/AdminNotificationsView.js +4 -1
  21. package/dist/features/admin/components/AdminOrderEditorView.d.ts +7 -1
  22. package/dist/features/admin/components/AdminOrderEditorView.js +16 -4
  23. package/dist/features/admin/components/AdminOrdersView.js +2 -1
  24. package/dist/features/auctions/actions/bid-actions.js +71 -9
  25. package/dist/features/auctions/components/AuctionDetailPageView.js +8 -2
  26. package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -3
  27. package/dist/features/auctions/components/PlaceBidFormClient.js +89 -14
  28. package/dist/features/auctions/schemas/bid-input.d.ts +19 -6
  29. package/dist/features/auctions/schemas/bid-input.js +19 -7
  30. package/dist/features/auth/repository/user.repository.js +6 -0
  31. package/dist/features/faq/components/FAQHelpfulButtons.js +2 -2
  32. package/dist/features/filters/FilterFacetSection.js +1 -1
  33. package/dist/features/homepage/components/WhatsAppCommunitySection.js +1 -1
  34. package/dist/features/layout/BackToTop.js +1 -1
  35. package/dist/features/layout/BottomActions.js +1 -1
  36. package/dist/features/layout/FooterLayout.js +2 -2
  37. package/dist/features/layout/TitleBarLayout.js +1 -1
  38. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -3
  39. package/dist/features/products/components/MarketplaceBundleCard.js +2 -2
  40. package/dist/features/products/components/MarketplacePrizeDrawCard.js +2 -2
  41. package/dist/features/products/components/PrizeDrawCollage.js +1 -1
  42. package/dist/features/products/components/PrizeDrawItemsEditor.js +1 -1
  43. package/dist/features/products/components/ProductDetailPageView.js +1 -1
  44. package/dist/features/products/components/ProductGrid.js +3 -3
  45. package/dist/features/products/repository/products.repository.d.ts +0 -4
  46. package/dist/features/products/repository/products.repository.js +0 -15
  47. package/dist/features/promotions/repository/coupons.repository.d.ts +12 -0
  48. package/dist/features/promotions/repository/coupons.repository.js +12 -0
  49. package/dist/features/seller/components/SellerSidebar.js +1 -1
  50. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +279 -0
  51. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +1 -1
  52. package/dist/jobs.d.ts +1 -1
  53. package/dist/jobs.js +1 -1
  54. package/dist/seed/homepage-sections-seed-data.js +5 -1
  55. package/dist/seed/site-settings-seed-data.js +18 -0
  56. package/dist/styles.css +55 -1
  57. package/dist/tailwind-utilities.css +1 -1
  58. package/dist/tokens/color-pairs.js +1 -0
  59. package/dist/tokens/tokens.css +33 -0
  60. package/dist/ui/components/Button.style.css +21 -0
  61. package/dist/ui/components/ImageLightbox.js +7 -4
  62. package/dist/ui/components/surface-tokens.d.ts +7 -0
  63. package/dist/ui/components/surface-tokens.js +7 -0
  64. package/package.json +1 -1
  65. package/dist/_internal/server/jobs/core/onBidPlaced.d.ts +0 -15
  66. package/dist/_internal/server/jobs/core/onBidPlaced.js +0 -63
  67. package/dist/_internal/server/jobs/handlers/onBidPlaced.d.ts +0 -3
  68. package/dist/_internal/server/jobs/handlers/onBidPlaced.js +0 -9
@@ -17,7 +17,9 @@ import { storeRepository } from "../../stores/repository/store.repository";
17
17
  import { siteSettingsRepository } from "../../../repositories";
18
18
  import { getAdminRealtimeDb } from "../../../providers/db-firebase";
19
19
  import { maskPublicBid } from "../../../security";
20
- import { resolveMinBidIncrement } from "../../../_internal/shared/features/auctions/config";
20
+ import { resolveMinBid, resolveMinBidIncrement } from "../../../_internal/shared/features/auctions/config";
21
+ import { sendNotification } from "../../admin/actions/notification-actions";
22
+ import { BID_MESSAGES } from "../../../_internal/server/jobs/handlers/messages";
21
23
  import { ERROR_MESSAGES, AuthorizationError, ValidationError, NotFoundError, } from "../../../errors";
22
24
  import { BID_ERROR_CODES } from "../../../errors/error-codes";
23
25
  import { increment } from "../../../contracts/field-ops";
@@ -54,22 +56,38 @@ export async function placeBid(userId, userEmail, input) {
54
56
  throw new AuthorizationError(ERROR_MESSAGES.BID.OWN_AUCTION);
55
57
  }
56
58
  }
57
- const baseBid = (product.currentBid ?? 0) > 0
58
- ? product.currentBid
59
+ // An auction with no bids yet prices off its starting bid, and that starting
60
+ // bid is itself acceptable — see ResolveMinBidOptions.hasBids.
61
+ const hasBids = (product.currentBid ?? 0) > 0 || (product.bidCount ?? 0) > 0;
62
+ const baseBid = hasBids
63
+ ? (product.currentBid ?? product.startingBid ?? product.price)
59
64
  : (product.startingBid ?? product.price);
60
65
  const settings = await siteSettingsRepository.getSingleton();
61
- const minIncrement = resolveMinBidIncrement(baseBid, settings.auctionConfig?.bidIncrementTiers ?? [], product.minBidIncrement);
66
+ const tiers = settings.auctionConfig?.bidIncrementTiers ?? [];
67
+ const minIncrement = resolveMinBidIncrement(baseBid, tiers, product.minBidIncrement);
68
+ // Identical derivation to the bid form's, via the one shared helper — the
69
+ // form seeds `resolveMinBid(...)` and this rejects below it, so the two can
70
+ // never disagree about what the auction will accept.
71
+ const minAcceptableBid = resolveMinBid(baseBid, tiers, product.minBidIncrement, { hasBids });
62
72
  // Proxy-bid semantics (eBay style): the buyer's `bidAmount` is treated as
63
73
  // their **maximum** they're willing to pay; the visible price only steps up
64
74
  // in `minIncrement` units as needed to stay ahead of the previous high bid.
65
75
  // If `autoMaxBid` is supplied explicitly it takes precedence over bidAmount
66
76
  // as the cap (lets clients separate desired-step from secret-cap).
67
77
  const newCap = Math.max(bidAmount, autoMaxBid ?? bidAmount);
68
- if (newCap <= baseBid) {
69
- throw new ValidationError(ERROR_MESSAGES.BID.BID_TOO_LOW, { code: BID_ERROR_CODES.TOO_LOW });
70
- }
71
- if (newCap < baseBid + minIncrement) {
72
- throw new ValidationError(ERROR_MESSAGES.BID.INCREMENT_TOO_LOW, { code: BID_ERROR_CODES.INCREMENT_VIOLATED });
78
+ if (newCap < minAcceptableBid) {
79
+ // Both rejections stay distinguishable: "below the standing price" and
80
+ // "above it but short of the increment" are different mistakes and the
81
+ // form maps their codes to different copy. With no bids yet only the
82
+ // former is reachable, since the minimum IS the starting bid.
83
+ const isIncrementViolation = hasBids && newCap > baseBid;
84
+ throw new ValidationError(isIncrementViolation
85
+ ? ERROR_MESSAGES.BID.INCREMENT_TOO_LOW
86
+ : ERROR_MESSAGES.BID.BID_TOO_LOW, {
87
+ code: isIncrementViolation
88
+ ? BID_ERROR_CODES.INCREMENT_VIOLATED
89
+ : BID_ERROR_CODES.TOO_LOW,
90
+ });
73
91
  }
74
92
  const previousWinner = await bidRepository
75
93
  .getWinningBid(productId)
@@ -89,6 +107,14 @@ export async function placeBid(userId, userEmail, input) {
89
107
  newBidWins = true;
90
108
  visibleBid = prevVisible;
91
109
  }
110
+ else if (!hasBids) {
111
+ // Opening bid: the price displays at the seller's starting bid, not at
112
+ // `startingBid + increment`, because there is nobody to outpace yet. Under
113
+ // proxy semantics the submission is a maximum, so without this an opening
114
+ // max of ₹500 on a ₹100 auction would be recorded at ₹200 for no reason.
115
+ newBidWins = true;
116
+ visibleBid = baseBid;
117
+ }
92
118
  else if (!previousWinner || newCap > prevCap) {
93
119
  newBidWins = true;
94
120
  const target = Math.max(prevCap + minIncrement, baseBid + minIncrement);
@@ -162,6 +188,41 @@ export async function placeBid(userId, userEmail, input) {
162
188
  productId,
163
189
  });
164
190
  }
191
+ // Outbid notification.
192
+ //
193
+ // This used to live in the `onBidPlaced` Firestore trigger, which derived the
194
+ // outbid user by re-reading `getWinningBid()` AFTER the fact. That read raced
195
+ // the batch above (the bid doc is created before the batch commits, so the
196
+ // trigger could observe either side of it) and so notified the wrong user, or
197
+ // nobody, non-deterministically. Here the outcome is already known exactly:
198
+ // `previousWinner` plus `newBidWins` say who lost the lead and who kept it.
199
+ //
200
+ // Best-effort, like the RTDB mirror above — a failed notification must never
201
+ // fail a bid that has already been committed.
202
+ const outbidUserId = newBidWins && previousWinner && previousWinner.data.userId !== userId
203
+ ? previousWinner.data.userId
204
+ : null;
205
+ if (outbidUserId) {
206
+ try {
207
+ await sendNotification({
208
+ userId: outbidUserId,
209
+ type: "bid_outbid",
210
+ priority: "high",
211
+ title: BID_MESSAGES.OUTBID_TITLE,
212
+ message: BID_MESSAGES.OUTBID_MESSAGE(product.title, product.currency || getDefaultCurrency(), finalVisibleForRtdb),
213
+ relatedId: productId,
214
+ relatedType: "product",
215
+ });
216
+ }
217
+ catch (notifyErr) {
218
+ void normalizeError(notifyErr);
219
+ serverLogger.warn("placeBid: outbid notification failed", {
220
+ error: notifyErr,
221
+ productId,
222
+ outbidUserId,
223
+ });
224
+ }
225
+ }
165
226
  serverLogger.info("placeBid", {
166
227
  bidId: bid.id,
167
228
  productId,
@@ -169,6 +230,7 @@ export async function placeBid(userId, userEmail, input) {
169
230
  bidAmount: visibleBid,
170
231
  maxProxyBid: newCap,
171
232
  winning: newBidWins,
233
+ outbidUserId,
172
234
  });
173
235
  return { bid };
174
236
  }
@@ -15,7 +15,7 @@ import { formatCurrency } from "../../../utils/number.formatter";
15
15
  import { normalizeRichTextHtml } from "../../../utils/string.formatter";
16
16
  import { safeDisplayName } from "../../../security";
17
17
  import { getSiteSettingsGlobal } from "../../admin/utils/getSiteSettingsGlobal";
18
- import { resolveMinBidIncrement, } from "../../../_internal/shared/features/auctions/config";
18
+ import { resolveMinBid, resolveMinBidIncrement, } from "../../../_internal/shared/features/auctions/config";
19
19
  import { Button, Container, CountdownDisplay, Div, Heading, Input, Main, Nav, RichText, Row, Section, Span, Stack, Text, Ul, Li, Dl, Dt, Dd, } from "../../../ui";
20
20
  import { AuctionDetailView } from "../../products/components/AuctionDetailView";
21
21
  import { ProductTabsShell } from "../../products/components/ProductTabsShell";
@@ -133,6 +133,12 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
133
133
  ]);
134
134
  const bidIncrementTiers = siteSettings.auctionConfig?.bidIncrementTiers ?? [];
135
135
  const initialMinBidIncrement = resolveMinBidIncrement(currentBid, bidIncrementTiers, minBidIncrementOverride);
136
+ // Mirrors placeBid's own hasBids — with no bids the starting bid is itself
137
+ // the minimum, so this preview must not advertise startingBid + increment.
138
+ const initialHasBids = bidCount > 0 || currentBid > startingBid;
139
+ const initialMinBid = resolveMinBid(currentBid, bidIncrementTiers, minBidIncrementOverride, {
140
+ hasBids: initialHasBids,
141
+ });
136
142
  return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: String(p.id ?? p.slug ?? ""), productType: "auction", snapshot: {
137
143
  title: typeof p.title === "string" ? p.title : undefined,
138
144
  thumb: typeof p.mainImage === "string"
@@ -145,7 +151,7 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
145
151
  storeName: typeof p.storeName === "string" ? p.storeName : undefined,
146
152
  } }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.AUCTIONS), className: CLS_BREADCRUMB_LINK, children: "Auctions" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(AuctionDetailView, { renderGallery: () => (_jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title })), renderInfo: () => renderAuctionInfoPanel({ productId: String(product.id), title, currentBid, currency, bidCount, isEnded, endDate, buyNowPrice, featured, freeShipping, condition, category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref }), renderBidForm: () => onPlaceBid ? (_jsx(Stack, { id: "auction-bid-form", gap: "3", children: _jsxs(Stack, { className: `${__P.p5}`, border: "subtle", gap: "3", rounded: "xl", surface: "muted", children: [_jsxs(Row, { justify: "between", align: "baseline", children: [_jsx(LiveBidPrice, { productId: String(product.id), currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "xl", badgeSize: "xs" }), !isEnded && (_jsx(LiveMinIncrement, { productId: String(product.id), currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, tiers: bidIncrementTiers, minBidIncrementOverride: minBidIncrementOverride }))] }), !isEnded && endDate && (_jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Ends in ", _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" })] })), _jsx(PlaceBidModalButton, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, tiers: bidIncrementTiers, minBidIncrementOverride: minBidIncrementOverride, currency: currency, isEnded: isEnded, auctionEndDate: endDate, buyNowPrice: buyNowPrice, bidsHaveStarted: bidsHaveStarted, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid, onBuyNow: onBuyNow, triggerClassName: "w-full" })] }) })) : (
147
153
  /* Read-only bid panel — shown when no bid action is wired (preview/demo) */
148
- _jsxs(Stack, { className: `${__P.p5}`, border: "subtle", gap: "md", rounded: "xl", surface: "muted", children: [_jsx(Stack, { gap: "xs", children: _jsxs(Text, { size: "xs", color: "muted", children: ["Starting bid: ", formatCurrency(startingBid, currency), !isEnded && _jsxs(_Fragment, { children: [" \u00B7 min increment ", formatCurrency(initialMinBidIncrement, currency)] })] }) }), _jsxs(Stack, { gap: "sm", children: [_jsx(Input, { type: "number", placeholder: `At least ${formatCurrency(currentBid + initialMinBidIncrement, currency)}`, min: currentBid + initialMinBidIncrement, "aria-label": "Your bid amount", disabled: isEnded }), !isEnded && endDate && (_jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Ends in ", _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" })] })), _jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isEnded, children: isEnded ? "Auction Ended" : "Place Bid" }), buyNowPrice !== null && !isEnded && !bidsHaveStarted && (_jsxs(Button, { variant: "secondary", size: "md", className: "w-full", disabled: true, children: ["Buy Now \u2014 ", formatCurrency(buyNowPrice, currency)] }))] }), tags.length > 0 && (_jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: tag }, tag))) }) }))] })), renderMobileBidForm: () => !isEnded && onPlaceBid ? (_jsxs(Stack, { className: `lg:hidden ${__P.p4}`, border: "subtle", gap: "3", rounded: "xl", surface: "muted", children: [_jsx(LiveBidPrice, { productId: String(product.id), currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "base", badgeSize: "xs" }), endDate && (_jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Ends in ", _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" })] })), _jsx(PlaceBidModalButton, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, tiers: bidIncrementTiers, minBidIncrementOverride: minBidIncrementOverride, currency: currency, isEnded: isEnded, auctionEndDate: endDate, buyNowPrice: buyNowPrice, bidsHaveStarted: bidsHaveStarted, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid, onBuyNow: onBuyNow, triggerClassName: "w-full" })] })) : !isEnded ? (_jsxs(Div, { className: `${__P.p4} lg:hidden`, border: "subtle", rounded: "xl", surface: "muted", children: [_jsxs(Row, { align: "center", gap: "sm", className: "mb-3", children: [_jsx(Span, { size: "base", weight: "bold", className: "text-primary-600 dark:text-primary-400", children: formatCurrency(currentBid, currency) }), _jsxs(Span, { size: "xs", color: "muted", children: [bidCount, " bids"] })] }), endDate && (_jsxs(Text, { align: "center", className: "mb-3", size: "xs", color: "muted", children: ["Ends in ", _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" })] })), _jsx(Button, { variant: "primary", size: "md", className: "w-full", children: "Place Bid" })] })) : null, renderSublistingSection: sublistingCategoryId
154
+ _jsxs(Stack, { className: `${__P.p5}`, border: "subtle", gap: "md", rounded: "xl", surface: "muted", children: [_jsx(Stack, { gap: "xs", children: _jsxs(Text, { size: "xs", color: "muted", children: ["Starting bid: ", formatCurrency(startingBid, currency), !isEnded && _jsxs(_Fragment, { children: [" \u00B7 min increment ", formatCurrency(initialMinBidIncrement, currency)] })] }) }), _jsxs(Stack, { gap: "sm", children: [_jsx(Input, { type: "number", placeholder: `At least ${formatCurrency(initialMinBid, currency)}`, min: initialMinBid, "aria-label": "Your bid amount", disabled: isEnded }), !isEnded && endDate && (_jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Ends in ", _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" })] })), _jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isEnded, children: isEnded ? "Auction Ended" : "Place Bid" }), buyNowPrice !== null && !isEnded && !bidsHaveStarted && (_jsxs(Button, { variant: "secondary", size: "md", className: "w-full", disabled: true, children: ["Buy Now \u2014 ", formatCurrency(buyNowPrice, currency)] }))] }), tags.length > 0 && (_jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: tag }, tag))) }) }))] })), renderMobileBidForm: () => !isEnded && onPlaceBid ? (_jsxs(Stack, { className: `lg:hidden ${__P.p4}`, border: "subtle", gap: "3", rounded: "xl", surface: "muted", children: [_jsx(LiveBidPrice, { productId: String(product.id), currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "base", badgeSize: "xs" }), endDate && (_jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Ends in ", _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" })] })), _jsx(PlaceBidModalButton, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, tiers: bidIncrementTiers, minBidIncrementOverride: minBidIncrementOverride, currency: currency, isEnded: isEnded, auctionEndDate: endDate, buyNowPrice: buyNowPrice, bidsHaveStarted: bidsHaveStarted, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid, onBuyNow: onBuyNow, triggerClassName: "w-full" })] })) : !isEnded ? (_jsxs(Div, { className: `${__P.p4} lg:hidden`, border: "subtle", rounded: "xl", surface: "muted", children: [_jsxs(Row, { align: "center", gap: "sm", className: "mb-3", children: [_jsx(Span, { size: "base", weight: "bold", className: "text-primary-600 dark:text-primary-400", children: formatCurrency(currentBid, currency) }), _jsxs(Span, { size: "xs", color: "muted", children: [bidCount, " bids"] })] }), endDate && (_jsxs(Text, { align: "center", className: "mb-3", size: "xs", color: "muted", children: ["Ends in ", _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" })] })), _jsx(Button, { variant: "primary", size: "md", className: "w-full", children: "Place Bid" })] })) : null, renderSublistingSection: sublistingCategoryId
149
155
  ? () => (_jsx(SublistingCarouselSection, { sublistingCategoryId: sublistingCategoryId, currentListingId: String(product.id) }))
150
156
  : undefined, renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, customTabs: customSections.map((s) => ({
151
157
  id: s.id,
@@ -13,9 +13,9 @@ import { formatCurrency } from "../../../utils";
13
13
  import { getDefaultCurrency } from "../../../core/baseline-resolver";
14
14
  import { BaseListingCard, Button, Caption, Div, Row, Span, Stack, Text, TextLink } from "../../../ui";
15
15
  const CLS_STAR_ICON = "h-5 w-5 fill-warning text-warning";
16
- const CLS_LIVE_BADGE = "rounded-full bg-error-surface px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-white";
17
- const CLS_ENDING_BADGE = "rounded-full bg-warning-surface px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-white";
18
- const CLS_RESERVE_BADGE = "inline-flex items-center gap-[var(--appkit-space-1)] rounded-full bg-warning-surface px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-white";
16
+ const CLS_LIVE_BADGE = "rounded-full bg-error-solid px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-error-on-solid";
17
+ const CLS_ENDING_BADGE = "rounded-full bg-warning-solid px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-warning-on-solid";
18
+ const CLS_RESERVE_BADGE = "inline-flex items-center gap-[var(--appkit-space-1)] rounded-full bg-warning-solid px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-warning-on-solid";
19
19
  const CLS_HEART_ON = "fill-error text-error";
20
20
  const CLS_HEART_OFF = "text-zinc-400";
21
21
  function renderAuctionCardHero(props) {
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useState, useTransition } from "react";
3
+ import { useEffect, useRef, useState, useTransition } from "react";
4
4
  import { useRouter } from "next/navigation";
5
5
  import { formatCurrency } from "../../../utils/number.formatter";
6
6
  import { isAuthError } from "../../../utils/auth-error";
@@ -10,7 +10,7 @@ import { FieldInput } from "../../../ui/forms/FieldInput";
10
10
  import { applyZodIssues } from "../../../ui/forms/FormShell";
11
11
  import { placeBidSchema } from "../schemas/bid-input";
12
12
  import { useLiveAuctionBid } from "../hooks/useLiveAuctionBid";
13
- import { resolveMinBidIncrement, } from "../../../_internal/shared/features/auctions/config";
13
+ import { BID_PRESET_MULTIPLIERS, resolveMinBid, resolveMinBidIncrement, } from "../../../_internal/shared/features/auctions/config";
14
14
  import { normalizeError } from "../../../errors/normalize";
15
15
  const __P = {
16
16
  p5: "p-[var(--appkit-space-5)]",
@@ -32,19 +32,73 @@ export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, start
32
32
  // Tiered, floor-raising: resolves fresh whenever the live current bid
33
33
  // crosses a tier boundary while this modal is open.
34
34
  const minBidIncrement = resolveMinBidIncrement(currentBid, tiers, minBidIncrementOverride);
35
- const minBid = currentBid + minBidIncrement;
35
+ // Mirrors `placeBid`'s own `hasBids`. The `currentBid` prop already falls
36
+ // back to `startingBid` when the auction has no bids, so it can't be tested
37
+ // for `> 0` the way the server tests the raw document field — `bidCount`, or
38
+ // a price that has moved past the opening one, is the equivalent signal.
39
+ const hasBids = bidCount > 0 || currentBid > startingBid;
40
+ // Same helper `placeBid` uses server-side, so the seeded amount below can
41
+ // never be one the server rejects.
42
+ const minBid = resolveMinBid(currentBid, tiers, minBidIncrementOverride, { hasBids });
36
43
  const [bidAmount, setBidAmount] = useState(String(minBid));
37
- const [stepMul, setStepMul] = useState(1);
44
+ const [stepMul, setStepMul] = useState(BID_PRESET_MULTIPLIERS[0]);
38
45
  const [isPending, startTransition] = useTransition();
39
46
  const [isBuyNowPending, startBuyNowTransition] = useTransition();
40
47
  const [success, setSuccess] = useState(false);
41
48
  const [showLoginModal, setShowLoginModal] = useState(false);
49
+ /** Last amount the form itself wrote — see `seedAmount` below. */
50
+ const lastSeeded = useRef(minBid);
51
+ /**
52
+ * The bid a given preset multiplier resolves to at the live price.
53
+ *
54
+ * Anchored on `minBid` rather than `currentBid` so the first preset is always
55
+ * exactly the minimum acceptable bid. With bids present that is identical to
56
+ * the old `currentBid + increment * n`; with none it makes the opening preset
57
+ * the starting bid itself instead of an amount the seller never asked for.
58
+ */
59
+ function presetAmount(n) {
60
+ return minBid + minBidIncrement * (n - 1);
61
+ }
62
+ /**
63
+ * Write an amount the FORM chose (not the buyer). Tracking these separately
64
+ * from buyer keystrokes is what lets the live-price effect below tell "this
65
+ * value is still ours to update" from "the buyer typed this".
66
+ */
67
+ function seedAmount(value) {
68
+ lastSeeded.current = value;
69
+ setBidAmount(String(value));
70
+ }
42
71
  function applyPreset(n) {
43
72
  setStepMul(n);
44
- setBidAmount(String(currentBid + minBidIncrement * n));
73
+ seedAmount(presetAmount(n));
45
74
  }
75
+ // Re-seed the amount whenever the live price moves under a preset selection.
76
+ //
77
+ // `bidAmount` was previously only ever written on mount or on click, but
78
+ // `currentBid` keeps updating from the `/auction-bids/{id}` SSE channel while
79
+ // this form is open. A buyer who opened the modal, sat on it while another
80
+ // bidder moved the price, then hit "Place Bid" was submitting an amount now
81
+ // below the minimum — a guaranteed BID_AMOUNT_TOO_LOW rejection, with the
82
+ // preset buttons above already relabelled to the new tier so the number in
83
+ // the field visibly disagreed with the button that was highlighted.
84
+ //
85
+ // Custom is deliberately left alone: a typed amount is the buyer's own, and
86
+ // the schema + `min` attribute already surface it if the price outran them.
87
+ useEffect(() => {
88
+ if (stepMul === "custom")
89
+ return;
90
+ const next = presetAmount(stepMul);
91
+ if (String(next) === bidAmount)
92
+ return;
93
+ // Only follow the live price, never fight the buyer's own edits.
94
+ if (bidAmount !== String(lastSeeded.current))
95
+ return;
96
+ seedAmount(next);
97
+ // presetAmount is derived from currentBid/minBidIncrement, both in deps.
98
+ // eslint-disable-next-line react-hooks/exhaustive-deps
99
+ }, [currentBid, minBidIncrement, stepMul]);
46
100
  const buyNowAvailable = buyNowPrice !== null && !isEnded && !bidsHaveStarted && !!onBuyNow;
47
- const schema = placeBidSchema(minBid, minBidIncrement);
101
+ const schema = placeBidSchema(minBid, formatCurrency(minBid, currency));
48
102
  async function submitBid(amount, setFieldError) {
49
103
  try {
50
104
  const result = await onPlaceBid({ productId, bidAmount: amount });
@@ -54,8 +108,15 @@ export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, start
54
108
  return;
55
109
  }
56
110
  setSuccess(true);
57
- setBidAmount(String(amount + minBidIncrement));
58
- setStepMul(1);
111
+ // Provisional next-bid seed, corrected by the live-price effect above as
112
+ // soon as the SSE tick for this bid lands. It is only provisional because
113
+ // under proxy-bid semantics `placeBid` treats the submission as a MAXIMUM
114
+ // — the resulting visible price is usually lower than `amount`, and if the
115
+ // bid lost to a higher standing proxy cap it is higher. The old
116
+ // `amount + increment` was therefore rarely the next valid bid; taking the
117
+ // max against the live price keeps this from ever seeding below minimum.
118
+ setStepMul(BID_PRESET_MULTIPLIERS[0]);
119
+ seedAmount(resolveMinBid(Math.max(currentBid, amount), tiers, minBidIncrementOverride));
59
120
  // The bid history list and any duplicated static current-bid/bid-count
60
121
  // text elsewhere on this SSR'd page (info panel, page-level summary
61
122
  // cards) are frozen server props — refresh so they reflect this bid
@@ -114,16 +175,30 @@ export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, start
114
175
  }
115
176
  });
116
177
  }
117
- return (_jsxs(Stack, { className: `${__P.p5}`, border: "subtle", gap: "md", rounded: "xl", surface: "muted", children: [_jsxs(Stack, { gap: "xs", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { size: "xs", color: "muted", children: "Current bid" }), _jsx(Text, { size: "xs", color: "muted", children: "Starting bid" })] }), _jsxs(Row, { justify: "between", align: "baseline", children: [_jsx(Span, { size: "xl", weight: "bold", className: "text-primary-600 dark:text-primary-400", children: formatCurrency(currentBid, currency) }), _jsx(Span, { size: "sm", color: "muted", children: formatCurrency(startingBid, currency) })] }), _jsxs(Text, { size: "xs", color: "faint", children: [bidCount, " ", bidCount === 1 ? "bid" : "bids", " \u00B7 min increment", " ", formatCurrency(minBidIncrement, currency)] })] }), _jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to place a bid. Please log in or create an account to continue." }), _jsx(Form, { onSubmit: (e) => {
178
+ return (_jsxs(Stack, { className: `${__P.p5}`, border: "subtle", gap: "md", rounded: "xl", surface: "muted", children: [_jsxs(Stack, { gap: "xs", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { size: "xs", color: "muted", children: hasBids ? "Current bid" : "Starting bid" }), _jsx(Text, { size: "xs", color: "muted", children: hasBids ? "Starting bid" : "Minimum bid" })] }), _jsxs(Row, { justify: "between", align: "baseline", children: [_jsx(Span, { size: "xl", weight: "bold", className: "text-primary-600 dark:text-primary-400", children: formatCurrency(currentBid, currency) }), _jsx(Span, { size: "sm", color: "muted", children: formatCurrency(hasBids ? startingBid : minBid, currency) })] }), _jsxs(Text, { size: "xs", color: "faint", children: [bidCount, " ", bidCount === 1 ? "bid" : "bids", " \u00B7 min increment", " ", formatCurrency(minBidIncrement, currency)] })] }), _jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to place a bid. Please log in or create an account to continue." }), _jsx(Form, { onSubmit: (e) => {
118
179
  // handler wired through render-prop below — keeps setFieldError in scope
119
180
  e.preventDefault();
120
- }, children: ({ setFieldError, clearErrors }) => (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { gap: "xs", wrap: true, role: "radiogroup", "aria-label": "Bid amount preset", children: [[1, 5, 10].map((n) => (_jsxs(Button, { type: "button", variant: stepMul === n ? "primary" : "secondary", size: "sm", role: "radio", "aria-checked": stepMul === n, disabled: isEnded || isPending, onClick: () => {
121
- clearErrors();
122
- applyPreset(n);
123
- }, children: ["+", formatCurrency(minBidIncrement * n, currency)] }, n))), _jsx(Button, { type: "button", variant: stepMul === "custom" ? "primary" : "ghost", size: "sm", role: "radio", "aria-checked": stepMul === "custom", disabled: isEnded || isPending, onClick: () => setStepMul("custom"), children: "Custom" })] }), _jsx(FieldInput, { name: "bidAmount", type: "number", value: bidAmount, readOnly: stepMul !== "custom", onChange: (value) => {
181
+ }, children: ({ setFieldError, clearErrors }) => (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { gap: "xs", wrap: true, role: "radiogroup", "aria-label": "Bid amount preset", children: [BID_PRESET_MULTIPLIERS.map((n) => {
182
+ const amount = presetAmount(n);
183
+ const delta = amount - currentBid;
184
+ // `delta` is 0 only for the opening preset on a bid-free
185
+ // auction, where the minimum IS the starting bid — "+₹0" would
186
+ // read as a broken button, so name what it actually is.
187
+ const stepLabel = delta > 0 ? `+${formatCurrency(delta, currency)}` : "Minimum";
188
+ return (_jsx(Button, { type: "button", variant: stepMul === n ? "primary" : "secondary", size: "sm", role: "radio", "aria-checked": stepMul === n, disabled: isEnded || isPending, "aria-label": delta > 0
189
+ ? `Bid ${formatCurrency(amount, currency)} — ${formatCurrency(delta, currency)} above the current bid`
190
+ : `Bid ${formatCurrency(amount, currency)} — the minimum accepted bid`, onClick: () => {
191
+ clearErrors();
192
+ applyPreset(n);
193
+ }, children: _jsxs(Stack, { gap: "none", align: "center", children: [_jsx(Span, { size: "xs", weight: "semibold", children: stepLabel }), _jsx(Span, { size: "xs", color: "muted", children: formatCurrency(amount, currency) })] }) }, n));
194
+ }), _jsx(Button, { type: "button", variant: stepMul === "custom" ? "primary" : "ghost", size: "sm", role: "radio", "aria-checked": stepMul === "custom", disabled: isEnded || isPending, onClick: () => setStepMul("custom"), children: "Custom" })] }), _jsx(FieldInput, { name: "bidAmount", type: "number", value: bidAmount, readOnly: stepMul !== "custom", onChange: (value) => {
124
195
  setBidAmount(value);
125
196
  clearErrors();
126
- }, placeholder: `At least ${formatCurrency(minBid, currency)}`, min: minBid, "aria-label": "Your bid amount", disabled: isEnded || isPending }), stepMul === "custom" && (_jsxs(Text, { size: "xs", color: "muted", "aria-live": "polite", children: ["Any amount \u2265 ", formatCurrency(minBid, currency), " is accepted."] })), success && (_jsx(Text, { className: "text-success", size: "xs", children: "\u2713 Bid placed successfully!" })), !isEnded && auctionEndDate && (_jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Ends in ", _jsx(CountdownDisplay, { targetDate: auctionEndDate, format: "auto", expiredLabel: "Ended" })] })), _jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isEnded || isPending, isLoading: isPending, type: "button", onClick: () => {
197
+ }, placeholder: `At least ${formatCurrency(minBid, currency)}`, min: minBid, "aria-label": "Your bid amount", disabled: isEnded || isPending }), _jsx(Text, { size: "xs", color: "muted", "aria-live": "polite", children: stepMul === "custom"
198
+ ? `Any amount from ${formatCurrency(minBid, currency)} up is accepted — it need not be an exact multiple of the increment.`
199
+ : hasBids
200
+ ? `Minimum next bid ${formatCurrency(minBid, currency)} (current bid + ${formatCurrency(minBidIncrement, currency)} increment).`
201
+ : `Minimum opening bid ${formatCurrency(minBid, currency)} — the seller's starting bid. The ${formatCurrency(minBidIncrement, currency)} increment applies from the second bid on.` }), success && (_jsx(Text, { className: "text-success", size: "xs", children: "\u2713 Bid placed successfully!" })), !isEnded && auctionEndDate && (_jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Ends in ", _jsx(CountdownDisplay, { targetDate: auctionEndDate, format: "auto", expiredLabel: "Ended" })] })), _jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isEnded || isPending, isLoading: isPending, type: "button", onClick: () => {
127
202
  clearErrors();
128
203
  const parsed = schema.safeParse({ bidAmount });
129
204
  if (!parsed.success)
@@ -1,14 +1,27 @@
1
1
  import { z } from "zod";
2
2
  /**
3
3
  * Client-side schema for the place-bid form. Used by PlaceBidFormClient via
4
- * <Form> + FieldInput. Schema-driven so all messages are localised by a single
5
- * Zod custom-message map.
4
+ * <Form> + FieldInput.
6
5
  *
7
- * `minBid` and `minBidIncrement` are derived from the auction state and passed
8
- * to `placeBidSchema(minBid, minBidIncrement)` at render time.
6
+ * `minBid` is the auction's live minimum acceptable next bid, resolved by
7
+ * `resolveMinBid()` (`_internal/shared/features/auctions/config`) the same
8
+ * helper `placeBid` uses server-side, so this form can never seed or accept an
9
+ * amount the server will then reject.
10
+ *
11
+ * `minBidLabel` is the currency-formatted rendering of `minBid` (e.g. "₹1,100")
12
+ * — the schema takes it pre-formatted rather than importing a formatter so the
13
+ * inline error reads in the buyer's currency instead of a bare number.
14
+ *
15
+ * Deliberately NOT enforced here: that the bid is an exact multiple of the
16
+ * increment. `placeBid` only requires `amount >= currentBid + increment`, so
17
+ * adding a multiple-of rule client-side would reject bids the server accepts.
18
+ * A refinement claiming to do that used to exist but was a no-op — its
19
+ * predicate was `(v - minBid) % inc === 0 || (v - minBid) >= 0`, and the
20
+ * right-hand disjunct is always true once the `v >= minBid` check above has
21
+ * passed, so it could never fail.
9
22
  */
10
- export declare const placeBidSchema: (minBid: number, minBidIncrement: number) => z.ZodObject<{
11
- bidAmount: z.ZodEffects<z.ZodEffects<z.ZodEffects<z.ZodNumber, number, number>, number, number>, number, number>;
23
+ export declare const placeBidSchema: (minBid: number, minBidLabel: string) => z.ZodObject<{
24
+ bidAmount: z.ZodEffects<z.ZodEffects<z.ZodNumber, number, number>, number, number>;
12
25
  }, "strip", z.ZodTypeAny, {
13
26
  bidAmount: number;
14
27
  }, {
@@ -1,16 +1,28 @@
1
1
  import { z } from "zod";
2
2
  /**
3
3
  * Client-side schema for the place-bid form. Used by PlaceBidFormClient via
4
- * <Form> + FieldInput. Schema-driven so all messages are localised by a single
5
- * Zod custom-message map.
4
+ * <Form> + FieldInput.
6
5
  *
7
- * `minBid` and `minBidIncrement` are derived from the auction state and passed
8
- * to `placeBidSchema(minBid, minBidIncrement)` at render time.
6
+ * `minBid` is the auction's live minimum acceptable next bid, resolved by
7
+ * `resolveMinBid()` (`_internal/shared/features/auctions/config`) the same
8
+ * helper `placeBid` uses server-side, so this form can never seed or accept an
9
+ * amount the server will then reject.
10
+ *
11
+ * `minBidLabel` is the currency-formatted rendering of `minBid` (e.g. "₹1,100")
12
+ * — the schema takes it pre-formatted rather than importing a formatter so the
13
+ * inline error reads in the buyer's currency instead of a bare number.
14
+ *
15
+ * Deliberately NOT enforced here: that the bid is an exact multiple of the
16
+ * increment. `placeBid` only requires `amount >= currentBid + increment`, so
17
+ * adding a multiple-of rule client-side would reject bids the server accepts.
18
+ * A refinement claiming to do that used to exist but was a no-op — its
19
+ * predicate was `(v - minBid) % inc === 0 || (v - minBid) >= 0`, and the
20
+ * right-hand disjunct is always true once the `v >= minBid` check above has
21
+ * passed, so it could never fail.
9
22
  */
10
- export const placeBidSchema = (minBid, minBidIncrement) => z.object({
23
+ export const placeBidSchema = (minBid, minBidLabel) => z.object({
11
24
  bidAmount: z.coerce
12
25
  .number({ required_error: "Enter a bid amount" })
13
26
  .refine((v) => Number.isFinite(v) && v > 0, "Enter a valid bid amount")
14
- .refine((v) => v >= minBid, `Bid must be at least ${minBid}`)
15
- .refine((v) => (v - minBid) % minBidIncrement === 0 || (v - minBid) >= 0, `Bid must respect the minimum increment of ${minBidIncrement}`),
27
+ .refine((v) => v >= minBid, `Bid must be at least ${minBidLabel}`),
16
28
  });
@@ -257,6 +257,12 @@ UserRepository.SIEVE_FIELDS = {
257
257
  emailVerified: { canFilter: true, canSort: false },
258
258
  disabled: { canFilter: true, canSort: true },
259
259
  storeStatus: { canFilter: true, canSort: false },
260
+ // AdminTeamView's permission-group filter emits `permissionGroup==X` and
261
+ // /api/admin/team merges it into `role==employee,permissionGroup==X`. Without
262
+ // this entry Sieve (throwExceptions:false) dropped the clause silently and the
263
+ // filter matched every employee. The composite indexes it needs are already
264
+ // declared and deployed — (permissionGroup, role, createdAt|displayName).
265
+ permissionGroup: { canFilter: true, canSort: false },
260
266
  createdAt: { canFilter: true, canSort: true, parseValue: parseSieveDateValue },
261
267
  updatedAt: { canFilter: true, canSort: true, parseValue: parseSieveDateValue },
262
268
  };
@@ -41,11 +41,11 @@ export function FAQHelpfulButtons({ faqId, initialHelpful, initialNotHelpful, on
41
41
  return (_jsxs(Div, { className: `${__P.p6}`, rounded: "lg", surface: "subtle", children: [_jsx(Text, { className: "mb-3", color: "muted", size: "sm", children: userVote
42
42
  ? (labels?.thanksForFeedback ?? "Thanks for your feedback")
43
43
  : (labels?.wasThisHelpful ?? "Was this helpful?") }), _jsxs(Row, { gap: "3", children: [_jsxs(Button, { variant: "ghost", onClick: () => handleVote(true), disabled: isPending || userVote !== null, className: `flex-1 gap-[var(--appkit-space-2)] rounded-lg p-[var(--appkit-space-4)] transition-all ${userVote === "helpful"
44
- ? "bg-success text-white"
44
+ ? "bg-success-solid text-success-on-solid"
45
45
  : userVote
46
46
  ? "cursor-not-allowed bg-zinc-200 text-[var(--appkit-color-text-muted)] opacity-50 dark:bg-slate-600 text-[var(--appkit-color-text-muted)]"
47
47
  : "bg-[var(--appkit-color-surface)] text-zinc-800 hover:bg-green-50 bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text)] dark:hover:bg-green-900/20"}`, children: [_jsx(Span, { size: "sm", children: labels?.yes ?? "Yes" }), _jsxs(Span, { size: "sm", color: "muted", children: ["(", helpful, ")"] })] }), _jsxs(Button, { variant: "ghost", onClick: () => handleVote(false), disabled: isPending || userVote !== null, className: `flex-1 gap-[var(--appkit-space-2)] rounded-lg p-[var(--appkit-space-4)] transition-all ${userVote === "not-helpful"
48
- ? "bg-error text-white"
48
+ ? "bg-error-solid text-error-on-solid"
49
49
  : userVote
50
50
  ? "cursor-not-allowed bg-zinc-200 text-[var(--appkit-color-text-muted)] opacity-50 dark:bg-slate-600 text-[var(--appkit-color-text-muted)]"
51
51
  : "bg-[var(--appkit-color-surface)] text-zinc-800 hover:bg-red-50 bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text)] dark:hover:bg-red-900/20"}`, children: [_jsx(Span, { size: "sm", children: labels?.no ?? "No" }), _jsxs(Span, { size: "sm", color: "muted", children: ["(", notHelpful, ")"] })] })] })] }));
@@ -7,7 +7,7 @@ const __O = {
7
7
  yAuto: "overflow-y-auto",
8
8
  };
9
9
  const CLS_OPTION_SELECTED = "border-success bg-success-surface text-success dark:border-success/60 dark:bg-success-surface dark:text-success";
10
- const CLS_OPTION_DOT = "inline-flex h-4 w-4 items-center justify-center rounded-full bg-success-surface text-[10px] text-white";
10
+ const CLS_OPTION_DOT = "inline-flex h-4 w-4 items-center justify-center rounded-full bg-success-solid text-[10px] text-success-on-solid";
11
11
  const CLS_CLEAR_BTN = "w-full py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] text-zinc-400 transition-colors hover:text-error";
12
12
  const CLS_BADGE_COUNT = "inline-flex items-center justify-center w-5 h-5 rounded-full bg-success-surface dark:bg-success-surface text-success dark:text-success ring-1 ring-success/20";
13
13
  const CLS_CLEAR_ICON = "inline-flex items-center justify-center w-5 h-5 p-[var(--appkit-space-0)] text-[var(--appkit-color-text-muted)] hover:text-error dark:hover:text-error hover:bg-[var(--appkit-color-surface)] dark:hover:bg-slate-700 transition-colors rounded-full";
@@ -16,5 +16,5 @@ export function WhatsAppCommunitySection({ title, descriptionHtml, memberCount,
16
16
  if (groupLink)
17
17
  window.open(groupLink, "_blank");
18
18
  };
19
- return (_jsx(Section, { paddingX: "x-md-2xl", className: className, surface: "subtle", paddingY: "y-4xl", children: _jsx(Div, { className: "w-full max-w-7xl mx-auto", children: _jsxs(Section, { color: "inverse", tone: "accent-banner", rounded: "2xl", shadow: "lg", className: "relative overflow-hidden", children: [_jsx(Div, { className: "absolute inset-0 opacity-[0.06] pointer-events-none bg-[repeating-linear-gradient(45deg,white_0,white_1px,transparent_0,transparent_50%)] bg-[size:10px_10px]", "aria-hidden": true }), _jsxs(Div, { className: "relative z-10 px-[1.5rem] sm:px-[2.5rem] sm:py-[3rem]", padding: "y-2xl", children: [_jsxs(Row, { align: "center", justify: "between", gap: "md", wrap: true, className: "mb-8", reverse: hand === "left", children: [_jsxs(Row, { align: "center", gap: "3", children: [_jsx(Row, { className: "w-12 h-12 bg-[var(--appkit-color-whatsapp)] flex-shrink-0", align: "center", justify: "center", rounded: "xl", shadow: "lg", children: _jsx("svg", { className: "w-7 h-7 text-white", fill: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z" }) }) }), _jsx(Span, { color: "inverse", size: "sm", weight: "medium", className: "/80 tracking-wide", transform: "uppercase", children: "WhatsApp Community" })] }), memberCount && (_jsxs(Row, { paddingY: "y-2xs-tall", className: "backdrop-blur-sm", surface: "default", padding: "x-md", align: "center", gap: "sm", rounded: "full", children: [_jsx(Users, { className: "w-4 h-4 text-white/80" }), _jsxs(Span, { color: "inverse", size: "sm", weight: "semibold", children: [memberCount.toLocaleString(getDefaultLocale()), "+ ", memberCountLabel] })] }))] }), _jsx(Heading, { color: "inverse", level: 2, className: "mb-3 leading-tight", smSize: "3xl", size: "2xl", weight: "bold", children: title }), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none prose-invert", className: "text-white/80 text-[length:var(--appkit-text-base)] mb-6 max-w-2xl leading-relaxed" })), benefits.length > 0 && (_jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-1 sm:grid-cols-2 mb-8 max-w-2xl", children: benefits.slice(0, 4).map((benefit, i) => (_jsxs(Row, { align: "start", gap: "2.5", children: [_jsx(Span, { layout: "flex-center", className: "mt-0.5 flex-shrink-0 w-5 h-5 bg-[rgba(255,255,255,0.2)]", rounded: "full", children: _jsx("svg", { className: "w-3 h-3 text-white", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: _jsx("path", { fillRule: "evenodd", d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", clipRule: "evenodd" }) }) }), _jsx(Span, { color: "inverse", size: "sm", className: "/90 leading-snug", children: benefit })] }, i))) })), testimonial && (_jsx(Blockquote, { paddingX: "x-md", className: "border-l-2 border-white/30 mb-8 max-w-xl", children: _jsxs(Text, { color: "inverse", variant: "none", className: "/75 italic leading-relaxed", size: "sm", children: ["\u201C", testimonial, "\u201D"] }) })), groupLink && (_jsxs(Button, { gap: "md", variant: "primary", size: "lg", onClick: handleJoin, "aria-label": buttonText, className: "!bg-[var(--appkit-color-whatsapp)] hover:!bg-[var(--appkit-color-whatsapp-light)] !text-white !border-0 font-[600]", shadow: "lg", children: [_jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z" }) }), buttonText] }))] })] }) }) }));
19
+ return (_jsx(Section, { paddingX: "x-md-2xl", className: className, surface: "subtle", paddingY: "y-4xl", children: _jsx(Div, { className: "w-full max-w-7xl mx-auto", children: _jsxs(Section, { color: "inverse", tone: "accent-banner", rounded: "2xl", shadow: "lg", className: "relative overflow-hidden", children: [_jsx(Div, { className: "absolute inset-0 opacity-[0.06] pointer-events-none bg-[repeating-linear-gradient(45deg,white_0,white_1px,transparent_0,transparent_50%)] bg-[size:10px_10px]", "aria-hidden": true }), _jsxs(Div, { className: "relative z-10 px-[1.5rem] sm:px-[2.5rem] sm:py-[3rem]", padding: "y-2xl", children: [_jsxs(Row, { align: "center", justify: "between", gap: "md", wrap: true, className: "mb-8", reverse: hand === "left", children: [_jsxs(Row, { align: "center", gap: "3", children: [_jsx(Row, { className: "w-12 h-12 bg-[var(--appkit-color-whatsapp)] flex-shrink-0", align: "center", justify: "center", rounded: "xl", shadow: "lg", children: _jsx("svg", { className: "w-7 h-7 text-white", fill: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z" }) }) }), _jsx(Span, { color: "inverse", size: "sm", weight: "medium", className: "/80 tracking-wide", transform: "uppercase", children: "WhatsApp Community" })] }), memberCount && (_jsxs(Row, { paddingY: "y-2xs-tall", className: "backdrop-blur-sm", surface: "frost", padding: "x-md", align: "center", gap: "sm", rounded: "full", children: [_jsx(Users, { className: "w-4 h-4 text-white/80" }), _jsxs(Span, { color: "inverse", size: "sm", weight: "semibold", children: [memberCount.toLocaleString(getDefaultLocale()), "+ ", memberCountLabel] })] }))] }), _jsx(Heading, { color: "inverse", level: 2, className: "mb-3 leading-tight", smSize: "3xl", size: "2xl", weight: "bold", children: title }), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none prose-invert", className: "text-white/80 text-[length:var(--appkit-text-base)] mb-6 max-w-2xl leading-relaxed" })), benefits.length > 0 && (_jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-1 sm:grid-cols-2 mb-8 max-w-2xl", children: benefits.slice(0, 4).map((benefit, i) => (_jsxs(Row, { align: "start", gap: "2.5", children: [_jsx(Span, { layout: "flex-center", className: "mt-0.5 flex-shrink-0 w-5 h-5 bg-[rgba(255,255,255,0.2)]", rounded: "full", children: _jsx("svg", { className: "w-3 h-3 text-white", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: _jsx("path", { fillRule: "evenodd", d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", clipRule: "evenodd" }) }) }), _jsx(Span, { color: "inverse", size: "sm", className: "/90 leading-snug", children: benefit })] }, i))) })), testimonial && (_jsx(Blockquote, { paddingX: "x-md", className: "border-l-2 border-white/30 mb-8 max-w-xl", children: _jsxs(Text, { color: "inverse", variant: "none", className: "/75 italic leading-relaxed", size: "sm", children: ["\u201C", testimonial, "\u201D"] }) })), groupLink && (_jsxs(Button, { gap: "md", variant: "primary", size: "lg", onClick: handleJoin, "aria-label": buttonText, className: "!bg-[var(--appkit-color-whatsapp)] hover:!bg-[var(--appkit-color-whatsapp-light)] !text-white !border-0 font-[600]", shadow: "lg", children: [_jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z" }) }), buttonText] }))] })] }) }) }));
20
20
  }
@@ -40,7 +40,7 @@ export function BackToTop({ threshold = 400, ariaLabel = "Back to top", classNam
40
40
  const handleDismiss = () => {
41
41
  setDismissed(true);
42
42
  };
43
- return (_jsxs(Div, { layout: "flex", align: "center", gap: "1", className: `fixed bottom-[calc(var(--keyboard-inset-height,0px)+var(--bottom-nav-height,4rem)+var(--appkit-space-4))] ${hand === "left" ? "left-4" : "right-4"} z-[var(--appkit-z-bottom-nav)] ${className}`, children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", "aria-label": ariaLabel, onClick: () => window.scrollTo({ top: 0, behavior: "smooth" }), className: "flex items-center justify-center w-10 h-10 rounded-full bg-[var(--appkit-color-surface)] border border-[var(--appkit-color-border)] shadow-md text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface)] dark:hover:bg-slate-700 transition-colors", children: _jsx(ChevronUp, { className: "w-5 h-5", "aria-hidden": "true" }) }), _jsx(Button, { type: "button", variant: "outline", size: "sm", "aria-label": "Hide back-to-top button", onClick: handleDismiss, className: "flex items-center justify-center w-6 h-6 rounded-full bg-[var(--appkit-color-surface)] border border-[var(--appkit-color-border)] shadow-md text-[var(--appkit-color-text-faint)] hover:text-[var(--appkit-color-text)] transition-colors", children: _jsx(X, { className: "w-3 h-3", "aria-hidden": "true" }) })] }));
43
+ return (_jsxs(Div, { layout: "flex", align: "center", gap: "1", className: `fixed bottom-[calc(var(--keyboard-inset-height,0px)+var(--bottom-nav-height,4rem)+var(--appkit-space-4))] ${hand === "left" ? "left-4" : "right-4"} z-[var(--appkit-z-back-to-top)] ${className}`, children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", "aria-label": ariaLabel, onClick: () => window.scrollTo({ top: 0, behavior: "smooth" }), className: "flex items-center justify-center w-10 h-10 rounded-full bg-[var(--appkit-color-surface)] border border-[var(--appkit-color-border)] shadow-md text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface)] dark:hover:bg-slate-700 transition-colors", children: _jsx(ChevronUp, { className: "w-5 h-5", "aria-hidden": "true" }) }), _jsx(Button, { type: "button", variant: "outline", size: "sm", "aria-label": "Hide back-to-top button", onClick: handleDismiss, className: "flex items-center justify-center w-6 h-6 rounded-full bg-[var(--appkit-color-surface)] border border-[var(--appkit-color-border)] shadow-md text-[var(--appkit-color-text-faint)] hover:text-[var(--appkit-color-text)] transition-colors", children: _jsx(X, { className: "w-3 h-3", "aria-hidden": "true" }) })] }));
44
44
  }
45
45
  /** SkipToMain — visually hidden link that becomes visible on focus for keyboard users. */
46
46
  export function SkipToMain({ mainId = "main-content" }) {
@@ -39,7 +39,7 @@ const BOTTOM_NAV_BG = "bg-[color-mix(in_srgb,var(--appkit-color-bg)_90%,transpar
39
39
  const Z_BOTTOM_ACTIONS = "z-[var(--appkit-z-bottom-nav)]";
40
40
  const BOTTOM_NAV_HEIGHT = "h-14";
41
41
  const FLEX_CENTER = "flex items-center justify-center";
42
- const CLS_COUNT_BADGE = "bg-error-surface text-white";
42
+ const CLS_COUNT_BADGE = "bg-error-solid text-error-on-solid";
43
43
  /** How far the user must scroll before the desktop bar reveals itself. */
44
44
  const DESKTOP_REVEAL_SCROLL_PX = 400;
45
45
  // ─── Sub-components ───────────────────────────────────────────────────────────
@@ -8,7 +8,7 @@ export function FooterLayout({ brandName, brandDescription, socialLinks, linkGro
8
8
  const [openGroups, setOpenGroups] = useState({});
9
9
  const toggleGroup = (idx) => setOpenGroups((prev) => ({ ...prev, [idx]: !prev[idx] }));
10
10
  const visibleTrustItems = trustBarItems.filter((item) => item.visible !== false);
11
- return (_jsxs(BlockFooter, { id: id, className: "bg-[var(--appkit-color-bg)] border-t border-[var(--appkit-color-border-subtle)]", children: [showTrustBar && visibleTrustItems.length > 0 && (_jsx(Div, { border: "subtle", className: "border-b", padding: "y-xl", children: _jsx(Div, { paddingX: "x-page", className: "container mx-auto max-w-[1920px]", children: _jsx(Row, { as: "ul", wrap: true, justify: "center", gap: "lg", className: "lg:gap-[var(--appkit-space-10)]", children: visibleTrustItems.map((item, i) => (_jsx(Li, { children: _jsxs(Row, { textSize: "sm", gap: "sm", className: "min-w-[160px]", children: [_jsx(Span, { className: "flex-shrink-0 text-primary-600 dark:text-secondary-400 w-5 h-5", children: item.icon }), _jsxs(Span, { children: [_jsx(Span, { size: "sm", weight: "bold", className: "block", color: "primary", children: item.label }), item.subtitle && (_jsx(Span, { size: "xs", className: "block", color: "muted", children: item.subtitle }))] })] }) }, item.id ?? String(i)))) }) }) })), _jsxs(Div, { paddingX: "x-page", className: "container mx-auto max-w-[1920px] lg:py-[var(--appkit-space-14)]", padding: "y-2xl", children: [_jsxs(Div, { className: "grid grid-cols-1 lg:grid-cols-5 gap-[var(--appkit-space-10)] lg:gap-[var(--appkit-space-16)]", children: [_jsxs(Stack, { className: "lg:col-span-2", gap: "md", children: [_jsx(Heading, { level: 5, size: "xl", weight: "bold", color: "primary", children: brandName }), _jsx(Text, { className: "leading-relaxed max-w-sm", color: "muted", size: "sm", children: brandDescription }), socialLinks.length > 0 && (_jsx(Row, { as: Ul, gap: "sm", children: socialLinks.map((link) => (_jsx(Li, { children: _jsx(Link, { href: link.href, "aria-label": link.ariaLabel, target: "_blank", rel: "noopener noreferrer", className: "flex items-center justify-center w-9 h-9 rounded-full border border-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)] hover:text-primary dark:hover:text-secondary hover:border-primary/40 dark:hover:border-secondary/40 transition-colors", children: link.icon }) }, link.platform))) })), newsletterEnabled && newsletterSlot && (_jsx(Div, { padding: "t-xs", children: newsletterSlot }))] }), _jsx(Div, { className: `lg:col-span-3 grid grid-cols-1 gap-[var(--appkit-space-0)] lg:gap-[var(--appkit-space-8)] ${linkGroups.length <= 3 ? "lg:grid-cols-3" :
11
+ return (_jsxs(BlockFooter, { id: id, className: "bg-[var(--appkit-color-bg)] border-t border-[var(--appkit-color-border-subtle)]", children: [showTrustBar && visibleTrustItems.length > 0 && (_jsx(Div, { border: "subtle", className: "border-b", padding: "y-xl", children: _jsx(Div, { paddingX: "x-page", className: "container mx-auto max-w-[1920px]", children: _jsx(Row, { as: "ul", wrap: true, justify: "center", gap: "lg", className: "lg:gap-[var(--appkit-space-10)]", children: visibleTrustItems.map((item, i) => (_jsx(Li, { children: _jsxs(Row, { textSize: "sm", gap: "sm", className: "min-w-[160px]", children: [_jsx(Span, { className: "flex-shrink-0 text-primary-600 dark:text-secondary-400 w-5 h-5", children: item.icon }), _jsxs(Span, { children: [_jsx(Span, { size: "sm", weight: "bold", className: "block", color: "primary", children: item.label }), item.subtitle && (_jsx(Span, { size: "xs", className: "block", color: "muted", weight: "medium", children: item.subtitle }))] })] }) }, item.id ?? String(i)))) }) }) })), _jsxs(Div, { paddingX: "x-page", className: "container mx-auto max-w-[1920px] lg:py-[var(--appkit-space-14)]", padding: "y-2xl", children: [_jsxs(Div, { className: "grid grid-cols-1 lg:grid-cols-5 gap-[var(--appkit-space-10)] lg:gap-[var(--appkit-space-16)]", children: [_jsxs(Stack, { className: "lg:col-span-2", gap: "md", children: [_jsx(Heading, { level: 5, size: "xl", weight: "bold", color: "primary", children: brandName }), _jsx(Text, { className: "leading-relaxed max-w-sm", color: "muted", size: "sm", weight: "medium", children: brandDescription }), socialLinks.length > 0 && (_jsx(Row, { as: Ul, gap: "sm", children: socialLinks.map((link) => (_jsx(Li, { children: _jsx(Link, { href: link.href, "aria-label": link.ariaLabel, target: "_blank", rel: "noopener noreferrer", className: "flex items-center justify-center w-9 h-9 rounded-full border border-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)] hover:text-primary dark:hover:text-secondary hover:border-primary/40 dark:hover:border-secondary/40 transition-colors", children: link.icon }) }, link.platform))) })), newsletterEnabled && newsletterSlot && (_jsx(Div, { padding: "t-xs", children: newsletterSlot }))] }), _jsx(Div, { className: `lg:col-span-3 grid grid-cols-1 gap-[var(--appkit-space-0)] lg:gap-[var(--appkit-space-8)] ${linkGroups.length <= 3 ? "lg:grid-cols-3" :
12
12
  linkGroups.length === 4 ? "lg:grid-cols-4" :
13
- "lg:grid-cols-5"}`, children: linkGroups.map((group, idx) => (_jsxs(Div, { children: [_jsxs(Button, { type: "button", variant: "ghost", onClick: () => toggleGroup(idx), className: "w-full justify-between border-b py-[var(--appkit-space-3)] text-left text-[length:var(--appkit-text-sm)] font-semibold transition-colors border-[var(--appkit-color-border-subtle)] text-[var(--appkit-color-text)] hover:text-primary dark:hover:text-secondary lg:hidden", "aria-expanded": !!openGroups[idx], children: [group.heading, _jsx(ChevronDown, { className: `w-4 h-4 transition-transform duration-200 ${openGroups[idx] ? "rotate-180" : ""}` })] }), _jsx(Ul, { className: `lg:hidden overflow-hidden transition-all duration-200 ${openGroups[idx] ? "max-h-96 py-[var(--appkit-space-3)]" : "max-h-0"} space-y-2.5`, children: group.links.map((link) => (_jsx(Li, { children: _jsx(Link, { href: link.href, className: "text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text-muted)] hover:text-primary dark:hover:text-secondary transition-colors", children: link.label }) }, link.href))) }), _jsxs(Div, { className: "hidden lg:block", children: [_jsx(Text, { className: "tracking-wider mb-3", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: group.heading }), _jsx(Ul, { className: "space-y-2.5", children: group.links.map((link) => (_jsx(Li, { children: _jsx(Link, { href: link.href, className: "text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text-muted)] hover:text-primary dark:hover:text-secondary transition-colors", children: link.label }) }, link.href))) })] })] }, idx))) })] }), _jsxs(Stack, { direction: "sm-row", color: "muted", textSize: "xs", justify: "between", border: "default", className: "mt-10 border-t border-[var(--appkit-color-border-subtle)] text-[var(--appkit-color-text-muted)]", padding: "t-lg", align: "center", gap: "3", children: [_jsx(Text, { size: "xs", color: "muted", children: copyrightText }), bottomLinks.length > 0 && (_jsx(Nav, { "aria-label": "Site utilities", children: _jsx(Ul, { className: "flex flex-wrap items-center gap-x-4 gap-y-1 justify-center", children: bottomLinks.map((link) => (_jsx(Li, { children: _jsx(Link, { href: link.href, className: "text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] hover:text-primary dark:hover:text-secondary transition-colors underline-offset-2 hover:underline", children: link.label }) }, link.href))) }) })), madeInText && (_jsx(Text, { size: "xs", color: "muted", children: madeInText }))] })] })] }));
13
+ "lg:grid-cols-5"}`, children: linkGroups.map((group, idx) => (_jsxs(Div, { children: [_jsxs(Button, { type: "button", variant: "ghost", onClick: () => toggleGroup(idx), className: "w-full justify-between border-b py-[var(--appkit-space-3)] text-left text-[length:var(--appkit-text-sm)] font-semibold transition-colors border-[var(--appkit-color-border-subtle)] text-[var(--appkit-color-text)] hover:text-primary dark:hover:text-secondary lg:hidden", "aria-expanded": !!openGroups[idx], children: [group.heading, _jsx(ChevronDown, { className: `w-4 h-4 transition-transform duration-200 ${openGroups[idx] ? "rotate-180" : ""}` })] }), _jsx(Ul, { className: `lg:hidden overflow-hidden transition-all duration-200 ${openGroups[idx] ? "max-h-96 py-[var(--appkit-space-3)]" : "max-h-0"} space-y-2.5`, children: group.links.map((link) => (_jsx(Li, { children: _jsx(Link, { href: link.href, className: "text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-text-muted)] hover:text-primary dark:hover:text-secondary transition-colors", children: link.label }) }, link.href))) }), _jsxs(Div, { className: "hidden lg:block", children: [_jsx(Text, { className: "tracking-wider mb-3", color: "muted", size: "xs", weight: "bold", transform: "uppercase", children: group.heading }), _jsx(Ul, { className: "space-y-2.5", children: group.links.map((link) => (_jsx(Li, { children: _jsx(Link, { href: link.href, className: "text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-text-muted)] hover:text-primary dark:hover:text-secondary transition-colors", children: link.label }) }, link.href))) })] })] }, idx))) })] }), _jsxs(Stack, { direction: "sm-row", color: "muted", textSize: "xs", justify: "between", border: "default", className: "mt-10 border-t border-[var(--appkit-color-border-subtle)] text-[var(--appkit-color-text-muted)]", padding: "t-lg", align: "center", gap: "3", children: [_jsx(Text, { size: "xs", color: "muted", weight: "medium", children: copyrightText }), bottomLinks.length > 0 && (_jsx(Nav, { "aria-label": "Site utilities", children: _jsx(Ul, { className: "flex flex-wrap items-center gap-x-4 gap-y-1 justify-center", children: bottomLinks.map((link) => (_jsx(Li, { children: _jsx(Link, { href: link.href, className: "text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] hover:text-primary dark:hover:text-secondary transition-colors underline-offset-2 hover:underline", children: link.label }) }, link.href))) }) })), madeInText && (_jsx(Text, { size: "xs", color: "muted", weight: "medium", children: madeInText }))] })] })] }));
14
14
  }
@@ -4,7 +4,7 @@ import { Avatar, BlockHeader, Button, Div, Row, Section, SiteLogo, SiteMark, Spa
4
4
  /** Shared icon-button class for all action buttons in the title bar. */
5
5
  const iconBtn = "flex items-center justify-center w-9 h-9 rounded-lg text-[var(--appkit-color-text-muted)] hover:bg-primary-50 hover:text-primary-700 dark:hover:bg-[var(--appkit-color-surface-elevated)] dark:hover:text-secondary-400 transition-colors";
6
6
  /** Badge counter class for wishlist/cart counts. */
7
- const countBadge = "absolute -top-0.5 -right-0.5 flex items-center justify-center w-4 h-4 rounded-full bg-error-surface text-white text-[10px] font-bold leading-none";
7
+ const countBadge = "absolute -top-0.5 -right-0.5 flex items-center justify-center w-4 h-4 rounded-full bg-error-solid text-error-on-solid text-[10px] font-bold leading-none";
8
8
  /**
9
9
  * TitleBarLayout — generic top sticky title-bar shell.
10
10
  *
@@ -10,10 +10,10 @@ import { ROUTES } from "../../../next";
10
10
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
11
11
  import { formatCurrency } from "../../../utils";
12
12
  import { getDefaultCurrency } from "../../../core/baseline-resolver";
13
- const CLS_PREORDER_BADGE = "inline-flex items-center rounded-full bg-warning-surface px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-white";
14
- const CLS_SALE_BADGE = "rounded-full bg-success-surface px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-bold text-white";
13
+ const CLS_PREORDER_BADGE = "inline-flex items-center rounded-full bg-warning-solid px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-warning-on-solid";
14
+ const CLS_SALE_BADGE = "rounded-full bg-success-solid px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-bold text-success-on-solid";
15
15
  const CLS_TRENDING_BADGE = "rounded-full bg-indigo-600 px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-bold text-white";
16
- const CLS_LIMITED_BADGE = "rounded-full bg-info-surface px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-bold text-white";
16
+ const CLS_LIMITED_BADGE = "rounded-full bg-info-solid px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-bold text-info-on-solid";
17
17
  import { BaseListingCard, Button, RichText, Row, Span, Stack, Text, TextLink } from "../../../ui";
18
18
  import { normalizeRichTextHtml } from "../../../utils/string.formatter";
19
19
  import { PreorderBadge } from "./PreorderCard";