@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.
- package/dist/_internal/server/features/auctions/service.d.ts +7 -1
- package/dist/_internal/server/features/auctions/service.js +13 -5
- package/dist/_internal/server/features/checkout/actions.d.ts +0 -15
- package/dist/_internal/server/features/checkout/actions.js +7 -3
- package/dist/_internal/server/features/orders/actions.js +5 -3
- package/dist/_internal/server/functions/firestore.d.ts +1 -2
- package/dist/_internal/server/functions/firestore.js +1 -9
- package/dist/_internal/server/jobs/core/index.d.ts +0 -1
- package/dist/_internal/server/jobs/core/index.js +0 -1
- package/dist/_internal/server/jobs/core/offerExpiry.js +33 -1
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -2
- package/dist/_internal/server/jobs/handlers/index.js +1 -2
- package/dist/_internal/shared/features/auctions/config.d.ts +34 -0
- package/dist/_internal/shared/features/auctions/config.js +47 -3
- package/dist/_internal/shared/listing-types/auction/config.js +1 -1
- package/dist/_internal/shared/listing-types/digital-code/config.js +1 -1
- package/dist/_internal/shared/listing-types/live/config.js +1 -1
- package/dist/_internal/shared/listing-types/pre-order/config.js +1 -1
- package/dist/features/account/components/NotificationBell.js +1 -1
- package/dist/features/admin/components/AdminNotificationsView.js +4 -1
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +7 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +16 -4
- package/dist/features/admin/components/AdminOrdersView.js +2 -1
- package/dist/features/auctions/actions/bid-actions.js +71 -9
- package/dist/features/auctions/components/AuctionDetailPageView.js +8 -2
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -3
- package/dist/features/auctions/components/PlaceBidFormClient.js +89 -14
- package/dist/features/auctions/schemas/bid-input.d.ts +19 -6
- package/dist/features/auctions/schemas/bid-input.js +19 -7
- package/dist/features/auth/repository/user.repository.js +6 -0
- package/dist/features/faq/components/FAQHelpfulButtons.js +2 -2
- package/dist/features/filters/FilterFacetSection.js +1 -1
- package/dist/features/homepage/components/WhatsAppCommunitySection.js +1 -1
- package/dist/features/layout/BackToTop.js +1 -1
- package/dist/features/layout/BottomActions.js +1 -1
- package/dist/features/layout/FooterLayout.js +2 -2
- package/dist/features/layout/TitleBarLayout.js +1 -1
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -3
- package/dist/features/products/components/MarketplaceBundleCard.js +2 -2
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +2 -2
- package/dist/features/products/components/PrizeDrawCollage.js +1 -1
- package/dist/features/products/components/PrizeDrawItemsEditor.js +1 -1
- package/dist/features/products/components/ProductDetailPageView.js +1 -1
- package/dist/features/products/components/ProductGrid.js +3 -3
- package/dist/features/products/repository/products.repository.d.ts +0 -4
- package/dist/features/products/repository/products.repository.js +0 -15
- package/dist/features/promotions/repository/coupons.repository.d.ts +12 -0
- package/dist/features/promotions/repository/coupons.repository.js +12 -0
- package/dist/features/seller/components/SellerSidebar.js +1 -1
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +279 -0
- package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +1 -1
- package/dist/jobs.d.ts +1 -1
- package/dist/jobs.js +1 -1
- package/dist/seed/homepage-sections-seed-data.js +5 -1
- package/dist/seed/site-settings-seed-data.js +18 -0
- package/dist/styles.css +55 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/color-pairs.js +1 -0
- package/dist/tokens/tokens.css +33 -0
- package/dist/ui/components/Button.style.css +21 -0
- package/dist/ui/components/ImageLightbox.js +7 -4
- package/dist/ui/components/surface-tokens.d.ts +7 -0
- package/dist/ui/components/surface-tokens.js +7 -0
- package/package.json +1 -1
- package/dist/_internal/server/jobs/core/onBidPlaced.d.ts +0 -15
- package/dist/_internal/server/jobs/core/onBidPlaced.js +0 -63
- package/dist/_internal/server/jobs/handlers/onBidPlaced.d.ts +0 -3
- 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
|
-
|
|
58
|
-
|
|
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
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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(
|
|
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-
|
|
17
|
-
const CLS_ENDING_BADGE = "rounded-full bg-warning-
|
|
18
|
-
const CLS_RESERVE_BADGE = "inline-flex items-center gap-[var(--appkit-space-1)] rounded-full bg-warning-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
58
|
-
|
|
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: [
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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 }),
|
|
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.
|
|
5
|
-
* Zod custom-message map.
|
|
4
|
+
* <Form> + FieldInput.
|
|
6
5
|
*
|
|
7
|
-
* `minBid`
|
|
8
|
-
*
|
|
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,
|
|
11
|
-
bidAmount: z.ZodEffects<z.ZodEffects<z.
|
|
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.
|
|
5
|
-
* Zod custom-message map.
|
|
4
|
+
* <Form> + FieldInput.
|
|
6
5
|
*
|
|
7
|
-
* `minBid`
|
|
8
|
-
*
|
|
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,
|
|
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 ${
|
|
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-
|
|
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-
|
|
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-
|
|
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: "
|
|
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-
|
|
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-
|
|
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: "
|
|
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-
|
|
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-
|
|
14
|
-
const CLS_SALE_BADGE = "rounded-full bg-success-
|
|
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-
|
|
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";
|