@mohasinac/appkit 4.30.15 → 4.30.17

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 (120) hide show
  1. package/dist/_internal/server/functions/https.js +17 -2
  2. package/dist/_internal/server/jobs/core/listingProcessor.js +5 -1
  3. package/dist/_internal/shared/types/action-error-reporter.d.ts +40 -0
  4. package/dist/_internal/shared/types/action-error-reporter.js +27 -0
  5. package/dist/_internal/shared/types/action-result.d.ts +10 -0
  6. package/dist/_internal/shared/types/action-result.js +26 -0
  7. package/dist/client/api/surface-error.d.ts +6 -4
  8. package/dist/client/api/surface-error.js +19 -8
  9. package/dist/contracts/realtime-session.d.ts +27 -0
  10. package/dist/contracts/realtime-session.js +118 -0
  11. package/dist/core/unit-of-work.js +21 -13
  12. package/dist/errors/base-error.js +14 -1
  13. package/dist/errors/database-error.js +11 -1
  14. package/dist/errors/describe-cause.d.ts +5 -0
  15. package/dist/errors/describe-cause.js +76 -0
  16. package/dist/errors/error-display-map.d.ts +32 -1
  17. package/dist/errors/error-display-map.js +46 -0
  18. package/dist/errors/error-mapping.d.ts +47 -7
  19. package/dist/errors/error-mapping.js +46 -5
  20. package/dist/errors/index.d.ts +1 -0
  21. package/dist/errors/index.js +4 -0
  22. package/dist/features/about/components/PublicProfileView.js +4 -4
  23. package/dist/features/account/components/AddressBook.js +2 -2
  24. package/dist/features/account/components/AddressesIndexListing.js +2 -2
  25. package/dist/features/addresses/repository/addresses.repository.d.ts +12 -1
  26. package/dist/features/addresses/repository/addresses.repository.js +16 -7
  27. package/dist/features/admin/components/AdminAnalyticsView.js +2 -2
  28. package/dist/features/admin/components/AdminCouponsView.js +2 -2
  29. package/dist/features/admin/components/AdminGuideHubView.js +2 -2
  30. package/dist/features/admin/components/AdminMediaView.js +2 -2
  31. package/dist/features/admin/components/AdminViewCards.js +2 -2
  32. package/dist/features/auctions/components/AuctionDetailPageView.js +1 -1
  33. package/dist/features/auctions/components/MarketplaceAuctionGrid.d.ts +1 -2
  34. package/dist/features/auctions/components/MarketplaceAuctionGrid.js +8 -7
  35. package/dist/features/auctions/components/PlaceBidFormClient.js +32 -15
  36. package/dist/features/auth/repository/user.repository.d.ts +22 -1
  37. package/dist/features/auth/repository/user.repository.js +31 -28
  38. package/dist/features/before-after/components/BeforeAfterGallery.js +2 -2
  39. package/dist/features/blog/components/BlogIndexListing.js +3 -3
  40. package/dist/features/blog/components/BlogListView.js +3 -3
  41. package/dist/features/blog/components/BlogPostView.js +2 -2
  42. package/dist/features/cart/repository/cart.repository.d.ts +15 -2
  43. package/dist/features/cart/repository/cart.repository.js +15 -13
  44. package/dist/features/categories/components/BundleCollage.js +2 -2
  45. package/dist/features/categories/components/CategoriesIndexListing.js +3 -3
  46. package/dist/features/categories/components/CategoryBundlesListing.js +2 -2
  47. package/dist/features/categories/components/CategoryGrid.js +2 -2
  48. package/dist/features/categories/components/CategoryProductsListing.js +2 -2
  49. package/dist/features/categories/components/CategoryStoresListing.js +2 -2
  50. package/dist/features/collections/components/CollectionCard.js +2 -2
  51. package/dist/features/events/components/EventRafflesSection.js +2 -2
  52. package/dist/features/events/components/EventsIndexListing.js +2 -2
  53. package/dist/features/events/components/EventsListView.js +3 -3
  54. package/dist/features/homepage/components/FeaturedBundlesSection.js +2 -2
  55. package/dist/features/homepage/components/GoogleReviewsSection.js +2 -2
  56. package/dist/features/homepage/components/HomepageSkeleton.js +1 -1
  57. package/dist/features/homepage/components/SecurityHighlightsSection.js +1 -1
  58. package/dist/features/homepage/components/SocialFeedSection.js +2 -2
  59. package/dist/features/homepage/components/StatsCounterSection.js +1 -1
  60. package/dist/features/homepage/components/TrustBadges.js +2 -2
  61. package/dist/features/homepage/components/TrustFeaturesSection.js +1 -1
  62. package/dist/features/messages/hooks/useConversation.js +39 -15
  63. package/dist/features/messages/hooks/useConversations.js +36 -14
  64. package/dist/features/messages/repository/conversations.repository.d.ts +23 -0
  65. package/dist/features/messages/repository/conversations.repository.js +53 -0
  66. package/dist/features/orders/repository/orders.repository.d.ts +12 -1
  67. package/dist/features/orders/repository/orders.repository.js +14 -5
  68. package/dist/features/payments/repository/payout.repository.d.ts +19 -9
  69. package/dist/features/payments/repository/payout.repository.js +26 -20
  70. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +2 -3
  71. package/dist/features/products/components/AuctionsIndexListing.js +2 -3
  72. package/dist/features/products/components/PrizeDrawCollage.js +2 -2
  73. package/dist/features/products/components/PrizeDrawsIndexListing.js +2 -3
  74. package/dist/features/products/components/PrizeDrawsSection.js +2 -2
  75. package/dist/features/products/components/ProductFilters.d.ts +19 -1
  76. package/dist/features/products/components/ProductFilters.js +11 -3
  77. package/dist/features/products/components/ProductGrid.js +2 -7
  78. package/dist/features/products/components/ProductsIndexListing.d.ts +4 -2
  79. package/dist/features/products/components/ProductsIndexListing.js +41 -28
  80. package/dist/features/products/components/RelatedProducts.js +2 -2
  81. package/dist/features/promotions/components/CouponsIndexListing.js +1 -1
  82. package/dist/features/reviews/components/ReviewDetailPageView.js +2 -2
  83. package/dist/features/reviews/components/ReviewsListingPanel.js +2 -2
  84. package/dist/features/reviews/repository/reviews.repository.d.ts +10 -8
  85. package/dist/features/reviews/repository/reviews.repository.js +16 -19
  86. package/dist/features/seller/components/SellerCouponsView.js +2 -2
  87. package/dist/features/seller/components/SellerDashboardView.js +3 -3
  88. package/dist/features/server-errors/schemas/firestore.d.ts +9 -0
  89. package/dist/features/stores/components/StoreAuctionsListing.js +2 -3
  90. package/dist/features/stores/components/StoreClassifiedsListing.js +2 -3
  91. package/dist/features/stores/components/StoreDigitalCodesListing.js +2 -3
  92. package/dist/features/stores/components/StoreGuideHubView.js +2 -2
  93. package/dist/features/stores/components/StoreLiveItemsListing.js +2 -3
  94. package/dist/features/stores/components/StorePreOrdersListing.js +2 -3
  95. package/dist/features/stores/components/StoreProductsListing.js +2 -2
  96. package/dist/features/stores/components/StoresIndexListing.js +3 -3
  97. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +215 -18
  98. package/dist/next/api/routeHandler.js +27 -7
  99. package/dist/providers/db-firebase/base.repository.d.ts +12 -0
  100. package/dist/providers/db-firebase/base.repository.js +48 -20
  101. package/dist/react/hooks/usePendingFilters.d.ts +12 -2
  102. package/dist/react/hooks/usePendingFilters.js +2 -2
  103. package/dist/react/hooks/usePendingTable.d.ts +9 -2
  104. package/dist/react/hooks/useRealtimeEvent.js +22 -4
  105. package/dist/security/pii-schemas.d.ts +19 -1
  106. package/dist/security/pii-schemas.js +19 -1
  107. package/dist/server.d.ts +2 -0
  108. package/dist/server.js +5 -0
  109. package/dist/styles.css +84 -20
  110. package/dist/tailwind-utilities.css +1 -1
  111. package/dist/tokens/tokens.css +7 -2
  112. package/dist/ui/components/Layout.d.ts +31 -18
  113. package/dist/ui/components/Layout.js +30 -19
  114. package/dist/ui/components/Layout.style.css +76 -17
  115. package/dist/ui/components/ListingLayout.d.ts +2 -0
  116. package/dist/ui/components/ListingLayout.js +16 -2
  117. package/dist/ui/components/StickyToolbar.js +3 -48
  118. package/dist/ui/components/toolbar-collapse.d.ts +44 -0
  119. package/dist/ui/components/toolbar-collapse.js +93 -0
  120. package/package.json +1 -1
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useEffect, useRef, useState, useTransition } from "react";
4
4
  import { useRouter } from "next/navigation";
5
+ import { useTranslations } from "next-intl";
5
6
  import { formatCurrency } from "../../../utils/number.formatter";
6
7
  import { isAuthError } from "../../../utils/auth-error";
7
8
  import { Button, CountdownDisplay, Div, LoginRequiredModal, Modal, Row, Span, Stack, Text } from "../../../ui";
@@ -14,16 +15,25 @@ import { AUCTION_BUYOUT_WINDOW_MINUTES } from "../../../_internal/shared/checkou
14
15
  import { BID_PRESET_MULTIPLIERS, isBuyNowAvailable, resolveMinBid, resolveMinBidIncrement, } from "../../../_internal/shared/features/auctions/config";
15
16
  import { normalizeError } from "../../../errors/normalize";
16
17
  import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
18
+ import { toUserMessage } from "../../../errors/error-display-map";
17
19
  const __P = {
18
20
  p5: "p-[var(--appkit-space-5)]",
19
21
  };
20
- const BID_ERROR_DISPLAY = {
21
- BID_AUCTION_ENDED: "This auction has closed. No more bids are accepted.",
22
- BID_AMOUNT_TOO_LOW: "Your bid must exceed the current winning bid.",
23
- BID_INCREMENT_TOO_LOW: "Your bid does not meet the minimum increment requirement.",
24
- BID_SELF_BID: "You cannot bid on your own listing.",
25
- BID_USER_BANNED: "Your account is currently restricted from bidding. Contact support.",
26
- };
22
+ // The local `BID_ERROR_DISPLAY` map that used to live here is gone.
23
+ //
24
+ // It covered 5 of the ~78 codes the server can emit, and — the actual defect —
25
+ // fell back to `result.error`, the server's own message. `result.error` is
26
+ // always a string, so the safe literal after it was unreachable, and a 500
27
+ // therefore printed its raw internals into the bid field: this component is
28
+ // where `Cannot find module '../providers/db-firebase' Require stack: -
29
+ // /var/task/.next/server/chunks/...` was shown to users.
30
+ //
31
+ // Its five strings already existed as `errors.codes.BID_*` in messages/en.json,
32
+ // so nothing was lost by deleting it. `toUserMessage` resolves every code and
33
+ // terminates in `errors.codes.UNKNOWN` — never in server text.
34
+ /** Feature-specific last resort. Authored copy, never derived from an error. */
35
+ const BID_FALLBACK = "Failed to place bid. Please try again.";
36
+ const BUY_NOW_FALLBACK = "Buy Now failed. Please try again.";
27
37
  export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, startingBid, tiers, minBidIncrementOverride, currency, isEnded, auctionEndDate, buyNowPrice, bidCount: ssrBidCount, tags = [], onPlaceBid, onBuyNow, }) {
28
38
  const router = useRouter();
29
39
  // Live-updates while the bid card is open — reflects other bidders'
@@ -47,6 +57,10 @@ export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, start
47
57
  const [isPending, startTransition] = useTransition();
48
58
  const [isBuyNowPending, startBuyNowTransition] = useTransition();
49
59
  const [success, setSuccess] = useState(false);
60
+ // Root translator (no namespace) — `toUserMessage` passes fully-qualified
61
+ // `errors.codes.*` keys, and resolves them itself, so a missing key degrades
62
+ // to generic copy rather than throwing MISSING_MESSAGE at render time.
63
+ const t = useTranslations();
50
64
  const [showLoginModal, setShowLoginModal] = useState(false);
51
65
  /**
52
66
  * Buy Now has no field to hang an error off — it is a standalone button, not
@@ -141,16 +155,17 @@ export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, start
141
155
  setShowLoginModal(true);
142
156
  }
143
157
  else {
144
- setFieldError("bidAmount", err instanceof Error ? err.message : "Failed to place bid. Please try again.");
158
+ // `err.message` deliberately NOT used — a thrown error here can carry
159
+ // server internals just as an envelope can.
160
+ setFieldError("bidAmount", BID_FALLBACK);
145
161
  }
146
162
  }
147
163
  }
148
164
  function bidErrorMessage(result) {
149
165
  if (result.ok)
150
166
  return null;
151
- return ((result.code && BID_ERROR_DISPLAY[result.code]) ??
152
- result.error ??
153
- "Failed to place bid. Please try again.");
167
+ // No `?? result.error` tail. That is what leaked the require stack.
168
+ return toUserMessage(result.code, t, { fallback: BID_FALLBACK });
154
169
  }
155
170
  /**
156
171
  * Show a Buy Now failure.
@@ -164,14 +179,16 @@ export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, start
164
179
  * `place_bids` soft ban. A login prompt for those tells the buyer to fix
165
180
  * something that is not the problem.
166
181
  */
167
- function reportBuyNowFailure(code, message) {
182
+ function reportBuyNowFailure(code, _message) {
168
183
  if (code === "UNAUTHENTICATED") {
169
184
  setShowLoginModal(true);
170
185
  return;
171
186
  }
172
- setBuyNowError((code && BID_ERROR_DISPLAY[code]) ??
173
- message ??
174
- "Buy Now failed. Please try again.");
187
+ // `_message` is the SERVER's message and is intentionally ignored — it was
188
+ // the `?? message` tail here, same defect as bidErrorMessage above. Kept in
189
+ // the signature so callers need no change and the omission is visible.
190
+ void _message;
191
+ setBuyNowError(toUserMessage(code, t, { fallback: BUY_NOW_FALLBACK }));
175
192
  }
176
193
  function handleBuyNow() {
177
194
  if (!onBuyNow)
@@ -6,7 +6,28 @@ export declare class UserRepository extends BaseRepository<UserDocument> {
6
6
  static readonly SIEVE_FIELDS: FirebaseSieveFields;
7
7
  constructor();
8
8
  private decryptUser;
9
- private encryptUserData;
9
+ /**
10
+ * Declared, not hand-rolled.
11
+ *
12
+ * `BaseRepository.applyWriteHooks` already does exactly what the private
13
+ * `encryptUserData` did — encrypt the listed fields, then add the mapped blind
14
+ * indices derived from the PLAINTEXT source. Declaring the fields means all
15
+ * nine mutating methods get it, including the `createWithId` / `*InTx` /
16
+ * `*InBatch` family that inherited unhooked base implementations while the
17
+ * encryption lived in a private method only three call sites remembered to
18
+ * call.
19
+ */
20
+ protected piiFields: readonly ["email", "phoneNumber", "googleLinkedEmail"];
21
+ protected piiIndexMap: Record<string, string>;
22
+ /**
23
+ * The base hook, plus the nested objects it cannot see.
24
+ *
25
+ * `piiFields` is a flat list of top-level names, so a nested blob has to be
26
+ * encrypted separately. Calling `super.applyWriteHooks` FIRST keeps a single
27
+ * encryption path with one extra step on top, rather than two parallel paths
28
+ * that have to agree — which is what the hand-rolled override was.
29
+ */
30
+ protected applyWriteHooks<D extends object>(data: D): D;
10
31
  protected mapDoc<D = UserDocument>(snap: DocumentSnapshot): D;
11
32
  createWithId(id: string, data: Partial<UserDocument>): Promise<UserDocument>;
12
33
  create(input: Omit<UserDocument, "id" | "createdAt" | "updatedAt">): Promise<UserDocument>;
@@ -1,11 +1,24 @@
1
1
  import { normalizeError } from "../../../errors/normalize";
2
2
  import { DatabaseError } from "../../../errors";
3
3
  import { BaseRepository, getFirestoreCount, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase";
4
- import { USER_PII_FIELDS, USER_PII_INDEX_MAP, piiIndicesFor, decryptPayoutDetails, decryptPiiFields, decryptShippingConfig, encryptPayoutDetails, encryptPiiFields, encryptShippingConfig, piiBlindIndex, } from "../../../security";
4
+ import { USER_PII_FIELDS, USER_PII_INDEX_MAP, decryptPayoutDetails, decryptPiiFields, decryptShippingConfig, encryptPayoutDetails, encryptShippingConfig, piiBlindIndex, } from "../../../security";
5
5
  import { USER_COLLECTION, USER_FIELDS, } from "../schemas";
6
6
  export class UserRepository extends BaseRepository {
7
7
  constructor() {
8
8
  super(USER_COLLECTION);
9
+ /**
10
+ * Declared, not hand-rolled.
11
+ *
12
+ * `BaseRepository.applyWriteHooks` already does exactly what the private
13
+ * `encryptUserData` did — encrypt the listed fields, then add the mapped blind
14
+ * indices derived from the PLAINTEXT source. Declaring the fields means all
15
+ * nine mutating methods get it, including the `createWithId` / `*InTx` /
16
+ * `*InBatch` family that inherited unhooked base implementations while the
17
+ * encryption lived in a private method only three call sites remembered to
18
+ * call.
19
+ */
20
+ this.piiFields = USER_PII_FIELDS;
21
+ this.piiIndexMap = USER_PII_INDEX_MAP;
9
22
  }
10
23
  decryptUser(doc) {
11
24
  const decrypted = decryptPiiFields(doc, [...USER_PII_FIELDS]);
@@ -17,32 +30,22 @@ export class UserRepository extends BaseRepository {
17
30
  }
18
31
  return decrypted;
19
32
  }
20
- encryptUserData(data) {
21
- // encryptPiiFields encrypts each PII field in-place AND adds the corresponding
22
- // blind-index sibling (e.g. email → emailIndex) from the plaintext value.
23
- // addPiiIndices is intentionally NOT called here — it spreads the original
24
- // plaintext `data` back into the result, which would overwrite the encrypted
25
- // ciphertext with the original plaintext values, defeating the encryption.
26
- //
27
- // The mapped indices below are not optional. `encryptPiiFields` derives its
28
- // sibling name mechanically as `${field}Index`, so `phoneNumber` produces
29
- // `phoneNumberIndex` — but `findByPhone` queries `USER_FIELDS.PHONE_INDEX`,
30
- // which is `phoneIndex`. Only the seed paths ever wrote that name, so
31
- // **phone lookup could never match a user created by the app.**
32
- // `piiIndicesFor` applies USER_PII_INDEX_MAP's real names and, unlike
33
- // `addPiiIndices`, carries no plaintext back over the ciphertext.
34
- const encrypted = {
35
- ...encryptPiiFields(data, [...USER_PII_FIELDS]),
36
- ...piiIndicesFor(data, USER_PII_INDEX_MAP),
37
- };
38
- const access = encrypted;
39
- if (access.payoutDetails) {
33
+ /**
34
+ * The base hook, plus the nested objects it cannot see.
35
+ *
36
+ * `piiFields` is a flat list of top-level names, so a nested blob has to be
37
+ * encrypted separately. Calling `super.applyWriteHooks` FIRST keeps a single
38
+ * encryption path with one extra step on top, rather than two parallel paths
39
+ * that have to agree — which is what the hand-rolled override was.
40
+ */
41
+ applyWriteHooks(data) {
42
+ const out = super.applyWriteHooks(data);
43
+ const access = out;
44
+ if (access.payoutDetails)
40
45
  access.payoutDetails = encryptPayoutDetails(access.payoutDetails);
41
- }
42
- if (access.shippingConfig) {
46
+ if (access.shippingConfig)
43
47
  access.shippingConfig = encryptShippingConfig(access.shippingConfig);
44
- }
45
- return encrypted;
48
+ return out;
46
49
  }
47
50
  mapDoc(snap) {
48
51
  const raw = super.mapDoc(snap);
@@ -57,7 +60,7 @@ export class UserRepository extends BaseRepository {
57
60
  // check with no error anywhere (found via a live RBAC bug where an
58
61
  // admin account created this way couldn't access /admin).
59
62
  const withUid = { uid: id, ...data };
60
- const encrypted = this.encryptUserData(withUid);
63
+ const encrypted = this.applyWriteHooks(withUid);
61
64
  return super.createWithId(id, encrypted);
62
65
  }
63
66
  async create(input) {
@@ -78,7 +81,7 @@ export class UserRepository extends BaseRepository {
78
81
  createdAt: new Date(),
79
82
  updatedAt: new Date(),
80
83
  };
81
- const encrypted = this.encryptUserData(userData);
84
+ const encrypted = this.applyWriteHooks(userData);
82
85
  await this.db
83
86
  .collection(this.collection)
84
87
  .doc(id)
@@ -155,7 +158,7 @@ export class UserRepository extends BaseRepository {
155
158
  }
156
159
  }
157
160
  async update(uid, data) {
158
- const encrypted = this.encryptUserData(data);
161
+ const encrypted = this.applyWriteHooks(data);
159
162
  return super.update(uid, encrypted);
160
163
  }
161
164
  async markEmailAsVerified(uid) {
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { BeforeAfterSlider } from "./BeforeAfterSlider";
3
- import { Div, Stack, Text } from "../../../ui";
3
+ import { Div, Grid, Stack, Text } from "../../../ui";
4
4
  export function BeforeAfterGallery({ items, className = "", }) {
5
5
  if (items.length === 0)
6
6
  return null;
7
- return (_jsx(Div, { layout: "grid", gap: "6", className: `grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 ${className}`, children: items.map((item) => (_jsxs(Stack, { gap: "sm", children: [_jsx(BeforeAfterSlider, { item: item }), (item.title || item.description) && (_jsxs(Div, { className: "text-center", children: [item.title && (_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: item.title })), item.description && (_jsx(Text, { className: "mt-0.5", color: "muted", size: "xs", children: item.description }))] }))] }, item.id))) }));
7
+ return (_jsx(Grid, { cols: "cards", gap: "lg", className: className, children: items.map((item) => (_jsxs(Stack, { gap: "sm", children: [_jsx(BeforeAfterSlider, { item: item }), (item.title || item.description) && (_jsxs(Div, { className: "text-center", children: [item.title && (_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: item.title })), item.description && (_jsx(Text, { className: "mt-0.5", color: "muted", size: "xs", children: item.description }))] }))] }, item.id))) }));
8
8
  }
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useCallback, useMemo } from "react";
4
4
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
5
5
  import { useBlogPosts } from "../hooks/useBlog";
6
- import { Div, ListingFilterDrawer, ListingToolbar, Pagination, Row, Stack, Text, StickyToolbar } from "../../../ui";
6
+ import { Div, Grid, ListingFilterDrawer, ListingToolbar, Pagination, Row, Stack, Text, StickyToolbar } from "../../../ui";
7
7
  import { ROUTES } from "../../../next";
8
8
  import { BlogCard } from "./BlogListView";
9
9
  import { BlogFilters, BLOG_PUBLIC_SORT_OPTIONS } from "./BlogFilters";
@@ -19,7 +19,7 @@ const __O = {
19
19
  function renderBlogGrid(props) {
20
20
  const { isLoading, posts, view } = props;
21
21
  if (isLoading) {
22
- return (_jsx(Div, { layout: "grid", gap: "6", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-video", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p5}`, gap: "sm", children: [_jsx(Div, { className: "h-3 w-1/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-full", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-2/3", surface: "subtle", rounded: "default" })] })] }, i))) }));
22
+ return (_jsx(Grid, { cols: "cards", gap: "lg", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-video", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p5}`, gap: "sm", children: [_jsx(Div, { className: "h-3 w-1/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-full", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-2/3", surface: "subtle", rounded: "default" })] })] }, i))) }));
23
23
  }
24
24
  if (posts.length === 0) {
25
25
  return _jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "No posts found." });
@@ -27,7 +27,7 @@ function renderBlogGrid(props) {
27
27
  if (view === "list") {
28
28
  return (_jsx(Stack, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", border: "subtle", rounded: "xl", children: posts.map((post) => _jsx(BlogCard, { post: post, href: String(ROUTES.BLOG.ARTICLE(post.slug)) }, post.id)) }));
29
29
  }
30
- return (_jsx(Div, { layout: "grid", gap: "6", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: posts.map((post) => _jsx(BlogCard, { post: post, href: String(ROUTES.BLOG.ARTICLE(post.slug)) }, post.id)) }));
30
+ return (_jsx(Grid, { cols: "cards", gap: "lg", children: posts.map((post) => _jsx(BlogCard, { post: post, href: String(ROUTES.BLOG.ARTICLE(post.slug)) }, post.id)) }));
31
31
  }
32
32
  function renderBlogFilterDrawer(props) {
33
33
  const { filterOpen, setFilterOpen, activeFilterCount, clearFilters, applyFilters, pendingTable } = props;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React from "react";
3
3
  import Link from "next/link";
4
- import { Article, Button, Div, Heading, Pagination, Row, Span, Stack, Text } from "../../../ui";
4
+ import { Article, Button, Div, Grid, Heading, Pagination, Row, Span, Stack, Text } from "../../../ui";
5
5
  import { MediaImage } from "../../media/MediaImage";
6
6
  import { getDefaultLocale } from "../../../core/baseline-resolver";
7
7
  import { getMediaUrl } from "../../media/types/index";
@@ -37,7 +37,7 @@ export function BlogCategoryTabs({ categories, active, onSelect, labels = {}, })
37
37
  }
38
38
  export function BlogListView({ posts, isLoading, totalPages = 1, currentPage = 1, total = 0, onPageChange, onPostClick, emptyLabel = "No posts found", slots, }) {
39
39
  if (isLoading) {
40
- return (_jsx(Div, { layout: "grid", gap: "6", className: "sm:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Div, { className: "animate-pulse overflow-hidden border border-neutral-200 bg-[var(--appkit-color-surface)]", rounded: "xl", children: [_jsx(Div, { className: "aspect-video bg-neutral-200" }), _jsxs(Stack, { className: `${__P.p5}`, gap: "sm", children: [_jsx(Div, { className: "h-4 w-16 bg-neutral-200", rounded: "default" }), _jsx(Div, { className: "h-5 w-full bg-neutral-200", rounded: "default" }), _jsx(Div, { className: "h-4 w-3/4 bg-neutral-200", rounded: "default" })] })] }, i))) }));
40
+ return (_jsx(Grid, { cols: "cards", gap: "lg", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Div, { className: "animate-pulse overflow-hidden border border-neutral-200 bg-[var(--appkit-color-surface)]", rounded: "xl", children: [_jsx(Div, { className: "aspect-video bg-neutral-200" }), _jsxs(Stack, { className: `${__P.p5}`, gap: "sm", children: [_jsx(Div, { className: "h-4 w-16 bg-neutral-200", rounded: "default" }), _jsx(Div, { className: "h-5 w-full bg-neutral-200", rounded: "default" }), _jsx(Div, { className: "h-4 w-3/4 bg-neutral-200", rounded: "default" })] })] }, i))) }));
41
41
  }
42
42
  if (posts.length === 0) {
43
43
  if (slots?.renderEmptyState) {
@@ -47,7 +47,7 @@ export function BlogListView({ posts, isLoading, totalPages = 1, currentPage = 1
47
47
  }
48
48
  return (_jsxs(Stack, { gap: "xl", children: [slots?.renderHeader
49
49
  ? slots.renderHeader({ total })
50
- : null, _jsx(Div, { layout: "grid", gap: "6", className: "sm:grid-cols-2 lg:grid-cols-3", children: posts.map((post, i) => slots?.renderCard ? (_jsx(React.Fragment, { children: slots.renderCard(post, i) }, post.id)) : (_jsx(BlogCard, { post: post, onClick: onPostClick }, post.id))) }), slots?.renderFooter ? slots.renderFooter({
50
+ : null, _jsx(Grid, { cols: "cards", gap: "lg", children: posts.map((post, i) => slots?.renderCard ? (_jsx(React.Fragment, { children: slots.renderCard(post, i) }, post.id)) : (_jsx(BlogCard, { post: post, onClick: onPostClick }, post.id))) }), slots?.renderFooter ? slots.renderFooter({
51
51
  page: currentPage,
52
52
  totalPages,
53
53
  }) : totalPages > 1 && onPageChange ? (_jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: onPageChange }) })) : null] }));
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import React from "react";
4
4
  import Link from "next/link";
5
- import { Div, Heading, RichText, Row, Scrim, Span, Stack, Text } from "../../../ui";
5
+ import { Div, Grid, Heading, RichText, Row, Scrim, Span, Stack, Text } from "../../../ui";
6
6
  import { Iframe } from "../../../ui/components/Iframe";
7
7
  import { MediaImage } from "../../media/MediaImage";
8
8
  import { useBlogPost } from "../hooks/useBlog";
@@ -31,7 +31,7 @@ function renderBlogPostHeader(post, date, labels) {
31
31
  function renderBlogPostRelated(related, title, renderRelatedCard) {
32
32
  if (related.length === 0)
33
33
  return null;
34
- return (_jsxs(Div, { children: [_jsx(Heading, { level: 2, className: "mb-6", size: "xl", weight: "semibold", children: title }), _jsx(Div, { layout: "grid", gap: "6", className: "sm:grid-cols-3", children: related.map((rel, i) => renderRelatedCard ? (_jsx(React.Fragment, { children: renderRelatedCard(rel, i) }, rel.id)) : (_jsx(BlogCard, { post: rel, href: String(ROUTES.BLOG.ARTICLE(rel.slug)) }, rel.id))) })] }));
34
+ return (_jsxs(Div, { children: [_jsx(Heading, { level: 2, className: "mb-6", size: "xl", weight: "semibold", children: title }), _jsx(Grid, { cols: "cards", gap: "lg", children: related.map((rel, i) => renderRelatedCard ? (_jsx(React.Fragment, { children: renderRelatedCard(rel, i) }, rel.id)) : (_jsx(BlogCard, { post: rel, href: String(ROUTES.BLOG.ARTICLE(rel.slug)) }, rel.id))) })] }));
35
35
  }
36
36
  export function BlogPostView({ slug, initialData, labels = {}, renderImage, renderContent, renderBackButton, renderLoading, renderError, renderRelatedCard, renderAuthorBio, className = "", }) {
37
37
  const { post, related, relatedByTags, relatedByAuthor, isLoading, error } = useBlogPost(slug, {
@@ -1,5 +1,4 @@
1
1
  import { type DocumentReference } from "firebase-admin/firestore";
2
- import type { DocumentSnapshot } from "../../../providers/db-firebase";
3
2
  import { BaseRepository, type FirebaseSieveResult, type SieveModel } from "../../../providers/db-firebase";
4
3
  import { type AddToCartInput, type CartAppliedCoupon, type CartDocument, type CartStoreAddons, type CartItemDocument, type AddGroupLineInput, type UpdateCartItemInput, type UpdateCartGroupMembersInput } from "../schemas";
5
4
  export declare class CartRepository extends BaseRepository<CartDocument> {
@@ -7,7 +6,21 @@ export declare class CartRepository extends BaseRepository<CartDocument> {
7
6
  constructor();
8
7
  /** Admin-facing paginated list of carts. */
9
8
  list(model: SieveModel): Promise<FirebaseSieveResult<CartDocument>>;
10
- protected mapDoc<D = CartDocument>(snap: DocumentSnapshot): D;
9
+ /**
10
+ * `mapDoc` is deliberately NOT overridden.
11
+ *
12
+ * It used to decrypt `items[].storeName` on every read — and nothing has
13
+ * ever encrypted that field. `CartRepository` declares no `piiFields`, and
14
+ * `storeName` appears nowhere in pii-schemas.ts. `decryptPii` passes a
15
+ * non-`enc:v1:` string straight through, so the call was a no-op on every
16
+ * document while reading as protection — the most expensive kind of dead
17
+ * code, because it answers the question "is this handled?" wrongly.
18
+ *
19
+ * A store name is not PII: it is the seller's public shopfront name, shown
20
+ * on every listing. If a genuinely private field is added to a cart line,
21
+ * declare it in `piiFields` so `applyWriteHooks` encrypts it on all nine
22
+ * write paths — do not reintroduce a decrypt with no matching encrypt.
23
+ */
11
24
  findByUserId(userId: string): Promise<CartDocument | null>;
12
25
  getOrCreate(userId: string): Promise<CartDocument>;
13
26
  createEmpty(userId: string): Promise<CartDocument>;
@@ -6,7 +6,6 @@ import { normalizeError } from "../../../errors/normalize";
6
6
  const ERR_CART_ITEM_NOT_FOUND = "Cart item not found";
7
7
  const ERR_CART_ITEM_LOCKED = "This item requires payment and cannot be removed or modified.";
8
8
  import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase";
9
- import { decryptPii } from "../../../security";
10
9
  import { CART_COLLECTION, } from "../schemas";
11
10
  export class CartRepository extends BaseRepository {
12
11
  constructor() {
@@ -16,18 +15,21 @@ export class CartRepository extends BaseRepository {
16
15
  async list(model) {
17
16
  return this.sieveQuery(model, CartRepository.SIEVE_FIELDS);
18
17
  }
19
- mapDoc(snap) {
20
- const raw = super.mapDoc(snap);
21
- return {
22
- ...raw,
23
- items: (raw.items ?? []).map((item) => ({
24
- ...item,
25
- storeName: typeof item.storeName === "string"
26
- ? (decryptPii(item.storeName) ?? item.storeName)
27
- : item.storeName,
28
- })),
29
- };
30
- }
18
+ /**
19
+ * `mapDoc` is deliberately NOT overridden.
20
+ *
21
+ * It used to decrypt `items[].storeName` on every read — and nothing has
22
+ * ever encrypted that field. `CartRepository` declares no `piiFields`, and
23
+ * `storeName` appears nowhere in pii-schemas.ts. `decryptPii` passes a
24
+ * non-`enc:v1:` string straight through, so the call was a no-op on every
25
+ * document while reading as protection — the most expensive kind of dead
26
+ * code, because it answers the question "is this handled?" wrongly.
27
+ *
28
+ * A store name is not PII: it is the seller's public shopfront name, shown
29
+ * on every listing. If a genuinely private field is added to a cart line,
30
+ * declare it in `piiFields` so `applyWriteHooks` encrypts it on all nine
31
+ * write paths — do not reintroduce a decrypt with no matching encrypt.
32
+ */
31
33
  async findByUserId(userId) {
32
34
  return this.findById(userId);
33
35
  }
@@ -10,7 +10,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
10
10
  */
11
11
  import { useState } from "react";
12
12
  import Link from "next/link";
13
- import { Button, Div, Row, Text } from "../../../ui";
13
+ import { Button, Div, Grid, Row, Text } from "../../../ui";
14
14
  import { MediaImage } from "../../media/MediaImage";
15
15
  import { ImageLightbox } from "../../../ui/components/ImageLightbox";
16
16
  import { ROUTES } from "../../../next/routing/route-map";
@@ -40,7 +40,7 @@ export function BundleCollage({ members, onItemClick }) {
40
40
  if (!members.length)
41
41
  return null;
42
42
  const galleryImages = toGalleryImages(members);
43
- return (_jsxs(_Fragment, { children: [_jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-2 sm:grid-cols-3 md:grid-cols-4", children: members.map((p, idx) => {
43
+ return (_jsxs(_Fragment, { children: [_jsx(Grid, { cols: "categoryCards", gap: "3", children: members.map((p, idx) => {
44
44
  const cover = p.mainImage ?? p.images?.[0];
45
45
  const href = String(ROUTES.PUBLIC.PRODUCT_DETAIL?.(p.slug ?? p.id) ?? "#");
46
46
  return (_jsxs(Div, { layout: "flex-col", className: "group relative overflow-hidden border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] transition-transform hover:scale-[1.01]", rounded: "lg", children: [_jsx(Button, { variant: "ghost", type: "button", onClick: makeBundleItemClickHandler(p, idx, onItemClick, setLightboxIndex), paddingX: "none", paddingY: "none", rounded: "none", className: "relative aspect-square w-full overflow-hidden focus-visible:ring-2 focus-visible:ring-[var(--appkit-color-primary)]", "aria-label": `View ${p.title} in lightbox`, children: _jsxs(Div, { className: "relative aspect-square w-full", children: [cover ? (_jsx(MediaImage, { src: cover, alt: p.title, size: "card", className: "transition-transform group-hover:scale-105" })) : (_jsx(Row, { textSize: "3xl", surface: "muted", className: "absolute inset-0", align: "center", justify: "center", children: PLACEHOLDER_EMOJI })), _jsxs(Div, { textWeight: "semibold", textSize: "xs", surface: "overlay-xl", padding: "chip-2xs", className: "absolute left-2 top-2 text-white", rounded: "default", children: ["#", idx + 1] })] }) }), _jsxs(Div, { padding: "xs", children: [_jsx(Link, { href: href, className: "line-clamp-2 text-[length:var(--appkit-text-sm)] font-medium hover:underline", children: p.title }), _jsx(Text, { className: "mt-0.5 text-[var(--appkit-color-text-muted)]", size: "xs", children: formatCurrency(p.price ?? 0, p.currency ?? "INR") })] })] }, p.id));
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useCallback, useMemo } from "react";
4
4
  import { useCategoriesFiltered } from "../hooks/useCategories";
5
5
  import { ROUTES } from "../../../next";
6
- import { Button, Div, ListingFilterDrawer, ListingToolbar, Pagination, Row, Stack, Text, StickyToolbar } from "../../../ui";
6
+ import { Button, Div, Grid, ListingFilterDrawer, ListingToolbar, Pagination, Row, Stack, Text, StickyToolbar } from "../../../ui";
7
7
  import { CategoryCard } from "./CategoryGrid";
8
8
  import { CategoryFilters } from "./CategoryFilters";
9
9
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
@@ -21,7 +21,7 @@ function renderCategoryGrid(props) {
21
21
  const { isLoading, categories, view, activeSearch, activeTab, brandsOnly } = props;
22
22
  const isBrandView = activeTab === "brands" || brandsOnly;
23
23
  if (isLoading) {
24
- return (_jsx(Div, { layout: "grid", gap: "4", className: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5", children: Array.from({ length: 10 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-[4/3]", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p3}.5`, gap: "sm", children: [_jsx(Div, { className: "h-3.5 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-full", surface: "subtle", rounded: "default" })] })] }, i))) }));
24
+ return (_jsx(Grid, { cols: "categoryCards", gap: "md", children: Array.from({ length: 10 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-[4/3]", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p3}.5`, gap: "sm", children: [_jsx(Div, { className: "h-3.5 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-full", surface: "subtle", rounded: "default" })] })] }, i))) }));
25
25
  }
26
26
  if (categories.length === 0) {
27
27
  return (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: activeSearch ? `No ${isBrandView ? "brands" : "categories"} matching "${activeSearch}"` : isBrandView ? "No brands found" : "No categories found" }));
@@ -29,7 +29,7 @@ function renderCategoryGrid(props) {
29
29
  if (view === "list") {
30
30
  return (_jsx(Stack, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", border: "subtle", rounded: "xl", children: categories.map((category) => _jsx(CategoryCard, { category: category, href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category.slug)) }, category.id)) }));
31
31
  }
32
- return (_jsx(Div, { layout: "grid", gap: "4", className: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5", children: categories.map((category) => _jsx(CategoryCard, { category: category, href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category.slug)) }, category.id)) }));
32
+ return (_jsx(Grid, { cols: "categoryCards", gap: "md", children: categories.map((category) => _jsx(CategoryCard, { category: category, href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category.slug)) }, category.id)) }));
33
33
  }
34
34
  function renderCategoryFilterDrawer(props) {
35
35
  const { filterOpen, setFilterOpen, activeFilterCount, clearFilters, applyFilters, pendingTable } = props;
@@ -10,7 +10,7 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
10
10
  */
11
11
  import { Row, Stack, sortBy } from "@mohasinac/appkit/client";
12
12
  import { useMemo, useCallback, useState } from "react";
13
- import { Div, Label, Span, Text, Toggle, StickyToolbar } from "../../../ui";
13
+ import { Div, Grid, Label, Span, Text, Toggle, StickyToolbar } from "../../../ui";
14
14
  import { ListingToolbar, Pagination, FilterDrawer } from "../../../ui";
15
15
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
16
16
  import { BundleBuyNowCta } from "./BundleBuyNowCta";
@@ -93,5 +93,5 @@ export function CategoryBundlesListing({ initialBundles, brandName, onBuyNow, })
93
93
  // Apply → close. <FilterDrawer> below stays available for parity
94
94
  // with every other listing page's filter affordance.
95
95
  { label: "Show out-of-stock", active: showOutOfStock, onChange: (next) => table.setMany({ showOutOfStock: next ? "true" : "", page: "1" }) },
96
- ] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: safePage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: pageItems.length === 0 ? (_jsxs(Text, { paddingY: "4xl", color: "muted", size: "sm", align: "start", children: ["No bundles match your search", brandName ? ` for ${brandName}` : "", "."] })) : (_jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: pageItems.map((bundle) => (_jsxs(Stack, { children: [_jsx(MarketplaceBundleCard, { bundle: bundle }), onBuyNow && (_jsx(Div, { border: "default", surface: "default", className: "-mt-px border-t border-t-0 border-[var(--appkit-color-border)] pt-[0.5rem] pb-[0.75rem]", padding: "x-sm", roundedBottom: "lg", children: _jsx(BundleBuyNowCta, { bundleSlug: bundle.slug, outOfStock: bundle.bundleStockStatus === "out_of_stock", onBuyNow: onBuyNow, compact: true }) }))] }, bundle.id))) })) }), _jsx(FilterDrawer, { open: filterOpen, onOpen: openFilters, onClose: () => setFilterOpen(false), onApply: applyFilters, onReset: () => setPendingShowOutOfStock(false), activeCount: filterActiveCount, hideTrigger: true, children: _jsx(Stack, { className: `${__P.p4}`, gap: "md", children: _jsxs(Label, { layout: "flex", gap: "lg", className: "justify-[space-between] cursor-pointer", children: [_jsx(Span, { size: "sm", weight: "medium", color: "muted", children: "Show out-of-stock bundles" }), _jsx(Toggle, { checked: pendingShowOutOfStock, onChange: setPendingShowOutOfStock, size: "sm" })] }) }) })] }));
96
+ ] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: safePage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: pageItems.length === 0 ? (_jsxs(Text, { paddingY: "4xl", color: "muted", size: "sm", align: "start", children: ["No bundles match your search", brandName ? ` for ${brandName}` : "", "."] })) : (_jsx(Grid, { cols: "cards", gap: "lg", children: pageItems.map((bundle) => (_jsxs(Stack, { children: [_jsx(MarketplaceBundleCard, { bundle: bundle }), onBuyNow && (_jsx(Div, { border: "default", surface: "default", className: "-mt-px border-t border-t-0 border-[var(--appkit-color-border)] pt-[0.5rem] pb-[0.75rem]", padding: "x-sm", roundedBottom: "lg", children: _jsx(BundleBuyNowCta, { bundleSlug: bundle.slug, outOfStock: bundle.bundleStockStatus === "out_of_stock", onBuyNow: onBuyNow, compact: true }) }))] }, bundle.id))) })) }), _jsx(FilterDrawer, { open: filterOpen, onOpen: openFilters, onClose: () => setFilterOpen(false), onApply: applyFilters, onReset: () => setPendingShowOutOfStock(false), activeCount: filterActiveCount, hideTrigger: true, children: _jsx(Stack, { className: `${__P.p4}`, gap: "md", children: _jsxs(Label, { layout: "flex", gap: "lg", className: "justify-[space-between] cursor-pointer", children: [_jsx(Span, { size: "sm", weight: "medium", color: "muted", children: "Show out-of-stock bundles" }), _jsx(Toggle, { checked: pendingShowOutOfStock, onChange: setPendingShowOutOfStock, size: "sm" })] }) }) })] }));
97
97
  }
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import Link from "next/link";
3
3
  import { ArrowRight } from "lucide-react";
4
- import { Div, Row, Span, Stack, Text } from "../../../ui";
4
+ import { Div, Grid, Row, Span, Stack, Text } from "../../../ui";
5
5
  import { DynamicBgDiv } from "../../../ui/components/DynamicBgDiv";
6
6
  import { MediaImage } from "../../media/MediaImage";
7
7
  const __P = {
@@ -30,5 +30,5 @@ export function CategoryGrid({ categories, getHref, onCategoryClick, emptyLabel
30
30
  if (categories.length === 0) {
31
31
  return (_jsx(Text, { paddingY: "3xl", className: "text-[var(--appkit-color-text-muted)]", size: "sm", align: "start", children: emptyLabel }));
32
32
  }
33
- return (_jsx(Div, { layout: "grid", gap: "4", className: `grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 ${className}`, children: categories.map((cat) => (_jsx(CategoryCard, { category: cat, href: getHref ? getHref(cat) : undefined, onClick: !getHref ? onCategoryClick : undefined }, cat.id))) }));
33
+ return (_jsx(Grid, { cols: "categoryCards", gap: "md", className: className, children: categories.map((cat) => (_jsx(CategoryCard, { category: cat, href: getHref ? getHref(cat) : undefined, onClick: !getHref ? onCategoryClick : undefined }, cat.id))) }));
34
34
  }
@@ -4,7 +4,7 @@ import { useState, useCallback } from "react";
4
4
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
5
5
  import { usePendingTable } from "../../../react/hooks/usePendingTable";
6
6
  import { useProducts } from "../../products/hooks/useProducts";
7
- import { Div, FilterDrawer, ListingToolbar, LoginRequiredModal, Pagination, Row, Stack, useToast, StickyToolbar } from "../../../ui";
7
+ import { Div, Grid, FilterDrawer, ListingToolbar, LoginRequiredModal, Pagination, Row, Stack, useToast, StickyToolbar } from "../../../ui";
8
8
  import { useAuthGate } from "../../../react/hooks/useAuthGate";
9
9
  import { ACTION_ID } from "../../products/constants/action-defs";
10
10
  import { ROUTES } from "../../../next";
@@ -26,7 +26,7 @@ const FILTER_KEYS = ["condition", "brand", "minPrice", "maxPrice"];
26
26
  function renderCategoryProductGrid(props) {
27
27
  const { isLoading, products, view, getProductHref, onWishlistToggle, wishlistedIds, onAddToCart } = props;
28
28
  if (isLoading) {
29
- return (_jsx(Div, { layout: "grid", gap: "6", className: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-4", children: Array.from({ length: 8 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-square", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p3}`, gap: "sm", children: [_jsx(Div, { className: "h-3 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-1/2", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-1/3", surface: "subtle", rounded: "default" })] })] }, i))) }));
29
+ return (_jsx(Grid, { cols: "cards", gap: "lg", children: Array.from({ length: 8 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-square", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p3}`, gap: "sm", children: [_jsx(Div, { className: "h-3 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-1/2", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-1/3", surface: "subtle", rounded: "default" })] })] }, i))) }));
30
30
  }
31
31
  return (_jsx(ProductGrid, { products: products, getProductHref: getProductHref, view: view, emptyLabel: "No products found in this category.", onWishlistToggle: onWishlistToggle, wishlistedIds: wishlistedIds, onAddToCart: onAddToCart }));
32
32
  }
@@ -2,10 +2,10 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { InteractiveStoreCard } from "../../stores/components/InteractiveStoreCard";
4
4
  import { ROUTES } from "../../../next";
5
- import { Div, Stack, Text } from "../../../ui";
5
+ import { Grid, Stack, Text } from "../../../ui";
6
6
  export function CategoryStoresListing({ stores }) {
7
7
  if (stores.length === 0) {
8
8
  return (_jsx(Stack, { justify: "center", className: "text-left", padding: "y-4xl", align: "start", children: _jsx(Text, { size: "sm", color: "muted", children: "No stores found in this category." }) }));
9
9
  }
10
- return (_jsx(Div, { layout: "grid", gap: "5", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4", children: stores.map((store) => (_jsx(InteractiveStoreCard, { store: store, href: String(ROUTES.PUBLIC.STORE_DETAIL(store.storeSlug)) }, store.id))) }));
10
+ return (_jsx(Grid, { cols: "cards", gap: "lg", children: stores.map((store) => (_jsx(InteractiveStoreCard, { store: store, href: String(ROUTES.PUBLIC.STORE_DETAIL(store.storeSlug)) }, store.id))) }));
11
11
  }
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Div, Heading, Text, TextLink } from "../../../ui";
2
+ import { Div, Grid, Heading, Text, TextLink } from "../../../ui";
3
3
  import { MediaImage } from "../../media/MediaImage";
4
4
  import { LAYOUT } from "../../../tokens";
5
5
  const CLS_PLACEHOLDER = "w-full";
@@ -7,5 +7,5 @@ export function CollectionCard({ collection, href }) {
7
7
  return (_jsxs(TextLink, { rounded: "xl", href: href, shadow: "hover-lg", className: "group relative block overflow-hidden bg-[var(--appkit-color-surface)] dark:bg-[var(--appkit-color-surface-elevated)] transition", children: [collection.image ? (_jsx(Div, { className: `relative ${LAYOUT.cardHeight.md} w-full`, overflow: "hidden", children: _jsx(MediaImage, { src: collection.image, alt: collection.title, size: "card", className: "transition-transform duration-300 group-hover:scale-105" }) })) : (_jsx(Div, { surface: "muted", className: `${LAYOUT.cardHeight.md} ${CLS_PLACEHOLDER}` })), _jsxs(Div, { surface: "default", padding: "sm", children: [_jsx(Heading, { level: 3, className: "text-[var(--appkit-color-text)]", weight: "semibold", children: collection.title }), collection.subtitle && (_jsx(Text, { className: "mt-1 text-[var(--appkit-color-text-muted)]", size: "sm", children: collection.subtitle })), collection.productCount !== undefined && (_jsxs(Text, { className: "mt-2 text-gray-400", size: "xs", children: [collection.productCount, " ", collection.productCount === 1 ? "item" : "items"] }))] })] }));
8
8
  }
9
9
  export function CollectionGrid({ collections, getHref }) {
10
- return (_jsx(Div, { layout: "grid", gap: "6", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: collections.map((c) => (_jsx(CollectionCard, { collection: c, href: getHref(c.slug) }, c.slug))) }));
10
+ return (_jsx(Grid, { cols: "cards", gap: "lg", children: collections.map((c) => (_jsx(CollectionCard, { collection: c, href: getHref(c.slug) }, c.slug))) }));
11
11
  }
@@ -8,7 +8,7 @@ import { serverLogger } from "../../../monitoring/server-logger";
8
8
  // this section silently rendered nothing instead of failing loudly.
9
9
  import { sieveFilter, sieveAnd, SIEVE_OP } from "../../../utils/sieve-builder";
10
10
  import { sortBy } from "../../../constants/sort";
11
- import { Container, Div, Heading, Section, Stack, Text, } from "../../../ui";
11
+ import { Container, Grid, Heading, Section, Stack, Text } from "../../../ui";
12
12
  import { eventRepository } from "../repository/events.repository";
13
13
  import { EventCard } from "./EventCard";
14
14
  /**
@@ -40,5 +40,5 @@ export async function EventRafflesSection({ config, }) {
40
40
  }
41
41
  if (events.length === 0)
42
42
  return null;
43
- return (_jsx(Section, { padding: "y-2xl", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "2xl", weight: "semibold", color: "primary", children: title }), subtitle ? (_jsx(Text, { size: "sm", color: "muted", children: subtitle })) : null] }), _jsx(Div, { gap: "3", className: "fluid-grid-card", children: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) })] }) }) }));
43
+ return (_jsx(Section, { padding: "y-2xl", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "2xl", weight: "semibold", color: "primary", children: title }), subtitle ? (_jsx(Text, { size: "sm", color: "muted", children: subtitle })) : null] }), _jsx(Grid, { cols: "cardsWide", gap: "3", children: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) })] }) }) }));
44
44
  }
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useCallback, useMemo } from "react";
4
4
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
5
5
  import { useEvents } from "../hooks/useEvents";
6
- import { Div, ListingFilterDrawer, ListingToolbar, Pagination, Row, Stack, Text, StickyToolbar } from "../../../ui";
6
+ import { Div, Grid, ListingFilterDrawer, ListingToolbar, Pagination, Row, Stack, Text, StickyToolbar } from "../../../ui";
7
7
  import { EventCard } from "./EventCard";
8
8
  import { EventFilters, EVENT_PUBLIC_SORT_OPTIONS } from "./EventFilters";
9
9
  import { EVENT_FIELDS } from "../../../constants/field-names";
@@ -123,5 +123,5 @@ export function EventsIndexListing({ initialData, facetCounts }) {
123
123
  }, [searchInput, table]);
124
124
  return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search events...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get(TABLE_KEYS.SORT) || sortBy(EVENT_FIELDS.STARTS_AT, "ASC"), sortOptions: EVENT_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, toggles: [
125
125
  { label: "Show expired", active: showExpired, onChange: (next) => table.set(TABLE_KEYS.SHOW_EXPIRED, next ? "true" : "") },
126
- ] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Div, { layout: "grid", gap: "6", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-video", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p4}`, gap: "sm", children: [_jsx(Div, { className: "h-4 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-full", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-2/3", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-8 mt-2", surface: "subtle", rounded: "default" })] })] }, i))) })) : events.length === 0 ? (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "No events found." })) : view === "list" ? (_jsx(Stack, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", border: "subtle", rounded: "xl", children: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) })) : (_jsx(Div, { layout: "grid", gap: "6", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) })) }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsx(EventFilters, { table: pendingTable, variant: "public", counts: facetCounts, hideEmpty: Boolean(facetCounts) }) })] }));
126
+ ] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Grid, { cols: "cards", gap: "lg", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-video", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p4}`, gap: "sm", children: [_jsx(Div, { className: "h-4 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-full", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-2/3", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-8 mt-2", surface: "subtle", rounded: "default" })] })] }, i))) })) : events.length === 0 ? (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "No events found." })) : view === "list" ? (_jsx(Stack, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", border: "subtle", rounded: "xl", children: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) })) : (_jsx(Grid, { cols: "cards", gap: "lg", children: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) })) }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsx(EventFilters, { table: pendingTable, variant: "public", counts: facetCounts, hideEmpty: Boolean(facetCounts) }) })] }));
127
127
  }