@mohasinac/appkit 2.7.49 → 2.7.52

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 (232) hide show
  1. package/dist/_internal/client/features/layout/SidebarCollapseToggle.d.ts +19 -0
  2. package/dist/_internal/client/features/layout/SidebarCollapseToggle.js +15 -0
  3. package/dist/_internal/client/features/layout/index.d.ts +2 -0
  4. package/dist/_internal/client/features/layout/index.js +1 -0
  5. package/dist/_internal/server/features/checkout/actions.js +25 -2
  6. package/dist/_internal/server/features/promotions/actions.js +3 -1
  7. package/dist/_internal/server/jobs/core/__tests__/couponExpiry.test.d.ts +1 -0
  8. package/dist/_internal/server/jobs/core/__tests__/couponExpiry.test.js +65 -0
  9. package/dist/_internal/server/jobs/core/__tests__/listingProcessor.test.d.ts +1 -0
  10. package/dist/_internal/server/jobs/core/__tests__/listingProcessor.test.js +163 -0
  11. package/dist/_internal/server/jobs/core/__tests__/mediaTmpCleanup.test.d.ts +1 -0
  12. package/dist/_internal/server/jobs/core/__tests__/mediaTmpCleanup.test.js +115 -0
  13. package/dist/_internal/server/jobs/core/__tests__/pendingOrderTimeout.test.d.ts +1 -0
  14. package/dist/_internal/server/jobs/core/__tests__/pendingOrderTimeout.test.js +96 -0
  15. package/dist/_internal/shared/actions/action-registry.js +149 -0
  16. package/dist/client.d.ts +45 -6
  17. package/dist/client.js +27 -4
  18. package/dist/constants/api-endpoints.d.ts +42 -0
  19. package/dist/constants/api-endpoints.js +14 -0
  20. package/dist/features/_guide-cls.d.ts +13 -0
  21. package/dist/features/_guide-cls.js +14 -0
  22. package/dist/features/about/components/HelpPageView.js +29 -26
  23. package/dist/features/account/components/BuyerAccountGuideView.d.ts +1 -0
  24. package/dist/features/account/components/BuyerAccountGuideView.js +39 -0
  25. package/dist/features/account/components/BuyerAuctionsGuideView.d.ts +1 -0
  26. package/dist/features/account/components/BuyerAuctionsGuideView.js +38 -0
  27. package/dist/features/account/components/BuyerOrdersGuideView.d.ts +1 -0
  28. package/dist/features/account/components/BuyerOrdersGuideView.js +48 -0
  29. package/dist/features/account/components/BuyerShoppingGuideView.d.ts +1 -0
  30. package/dist/features/account/components/BuyerShoppingGuideView.js +38 -0
  31. package/dist/features/account/components/UserSidebar.js +2 -1
  32. package/dist/features/admin/components/AdminAnalyticsGuideView.d.ts +1 -0
  33. package/dist/features/admin/components/AdminAnalyticsGuideView.js +24 -0
  34. package/dist/features/admin/components/AdminBlogEditorView.js +102 -75
  35. package/dist/features/admin/components/AdminBundleEditorView.js +20 -14
  36. package/dist/features/admin/components/AdminBundlesView.d.ts +1 -3
  37. package/dist/features/admin/components/AdminBundlesView.js +173 -52
  38. package/dist/features/admin/components/AdminCatalogGuideView.d.ts +1 -0
  39. package/dist/features/admin/components/AdminCatalogGuideView.js +28 -0
  40. package/dist/features/admin/components/AdminContentGuideView.d.ts +1 -0
  41. package/dist/features/admin/components/AdminContentGuideView.js +36 -0
  42. package/dist/features/admin/components/AdminCouponEditorView.js +1 -1
  43. package/dist/features/admin/components/AdminGuideHubView.d.ts +5 -0
  44. package/dist/features/admin/components/AdminGuideHubView.js +79 -0
  45. package/dist/features/admin/components/AdminOrdersGuideView.d.ts +1 -0
  46. package/dist/features/admin/components/AdminOrdersGuideView.js +44 -0
  47. package/dist/features/admin/components/AdminProductsView.js +8 -3
  48. package/dist/features/admin/components/AdminSidebar.js +2 -1
  49. package/dist/features/admin/components/AdminSiteConfigGuideView.d.ts +1 -0
  50. package/dist/features/admin/components/AdminSiteConfigGuideView.js +21 -0
  51. package/dist/features/admin/components/AdminStoresGuideView.d.ts +1 -0
  52. package/dist/features/admin/components/AdminStoresGuideView.js +32 -0
  53. package/dist/features/admin/components/AdminTeamGuideView.d.ts +1 -0
  54. package/dist/features/admin/components/AdminTeamGuideView.js +33 -0
  55. package/dist/features/admin/components/AdminTrustGuideView.d.ts +1 -0
  56. package/dist/features/admin/components/AdminTrustGuideView.js +40 -0
  57. package/dist/features/admin/components/AdminUsersGuideView.d.ts +1 -0
  58. package/dist/features/admin/components/AdminUsersGuideView.js +38 -0
  59. package/dist/features/auth/repository/session.repository.js +16 -1
  60. package/dist/features/cart/hooks/useAddToCart.d.ts +2 -0
  61. package/dist/features/cart/hooks/useAddToCart.js +2 -0
  62. package/dist/features/cart/hooks/useGuestCart.d.ts +2 -0
  63. package/dist/features/cart/utils/guest-cart.d.ts +7 -0
  64. package/dist/features/cart/utils/pending-ops.d.ts +6 -0
  65. package/dist/features/categories/components/CategoryBundlesListing.js +3 -25
  66. package/dist/features/categories/components/CategoryProductsListing.js +6 -10
  67. package/dist/features/categories/components/index.d.ts +0 -2
  68. package/dist/features/categories/components/index.js +0 -1
  69. package/dist/features/categories/schemas/firestore.d.ts +9 -0
  70. package/dist/features/events/components/AdminEventEditorView.js +223 -269
  71. package/dist/features/events/components/EventRaffleWinnerView.d.ts +9 -1
  72. package/dist/features/events/components/EventRaffleWinnerView.js +3 -3
  73. package/dist/features/events/components/SpinWheelView.js +3 -3
  74. package/dist/features/grouped/repository/grouped-listings.repository.d.ts +9 -0
  75. package/dist/features/grouped/repository/grouped-listings.repository.js +12 -0
  76. package/dist/features/layout/BottomNavLayout.d.ts +5 -0
  77. package/dist/features/layout/BottomNavLayout.js +21 -0
  78. package/dist/features/layout/TitleBar.js +7 -1
  79. package/dist/features/layout/TitleBarLayout.d.ts +8 -2
  80. package/dist/features/layout/TitleBarLayout.js +11 -7
  81. package/dist/features/media/upload/MediaUploadField.js +1 -1
  82. package/dist/features/orders/schemas/index.d.ts +2 -2
  83. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +12 -6
  84. package/dist/features/products/components/AuctionsIndexListing.js +3 -1
  85. package/dist/features/products/components/MarketplaceBundleCard.d.ts +21 -0
  86. package/dist/features/products/components/MarketplaceBundleCard.js +56 -0
  87. package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
  88. package/dist/features/products/components/PrizeDrawEntryActions.d.ts +3 -1
  89. package/dist/features/products/components/PrizeDrawEntryActions.js +8 -12
  90. package/dist/features/products/components/ProductDetailActions.d.ts +5 -1
  91. package/dist/features/products/components/ProductDetailActions.js +3 -1
  92. package/dist/features/products/components/ProductDetailPageView.d.ts +2 -0
  93. package/dist/features/products/components/ProductDetailPageView.js +4 -0
  94. package/dist/features/products/components/ProductGrid.js +3 -3
  95. package/dist/features/products/components/ProductsIndexListing.js +18 -9
  96. package/dist/features/products/components/index.d.ts +2 -4
  97. package/dist/features/products/components/index.js +1 -2
  98. package/dist/features/promotions/components/CouponCard.d.ts +11 -1
  99. package/dist/features/promotions/components/CouponCard.js +36 -2
  100. package/dist/features/promotions/repository/claimed-coupons.repository.d.ts +27 -0
  101. package/dist/features/promotions/repository/claimed-coupons.repository.js +115 -0
  102. package/dist/features/promotions/schemas/firestore.d.ts +34 -0
  103. package/dist/features/promotions/schemas/firestore.js +14 -1
  104. package/dist/features/reviews/components/index.d.ts +0 -2
  105. package/dist/features/reviews/components/index.js +0 -1
  106. package/dist/features/seller/components/BrandInlineSelect.js +23 -3
  107. package/dist/features/seller/components/CategoryInlineSelect.js +20 -3
  108. package/dist/features/seller/components/SellerAddressesView.js +3 -2
  109. package/dist/features/seller/components/SellerAnalyticsAlertsView.d.ts +6 -0
  110. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +124 -0
  111. package/dist/features/seller/components/SellerAnalyticsView.d.ts +2 -0
  112. package/dist/features/seller/components/SellerAnalyticsView.js +17 -9
  113. package/dist/features/seller/components/SellerBidsView.js +33 -3
  114. package/dist/features/seller/components/SellerBundlesView.d.ts +5 -0
  115. package/dist/features/seller/components/SellerBundlesView.js +104 -0
  116. package/dist/features/seller/components/SellerClassifiedView.d.ts +5 -0
  117. package/dist/features/seller/components/SellerClassifiedView.js +113 -0
  118. package/dist/features/seller/components/SellerDashboardView.js +19 -7
  119. package/dist/features/seller/components/SellerDigitalCodesView.d.ts +5 -0
  120. package/dist/features/seller/components/SellerDigitalCodesView.js +115 -0
  121. package/dist/features/seller/components/SellerGoogleReviewsView.d.ts +14 -0
  122. package/dist/features/seller/components/SellerGoogleReviewsView.js +95 -0
  123. package/dist/features/seller/components/SellerGroupedListingsView.d.ts +6 -0
  124. package/dist/features/seller/components/SellerGroupedListingsView.js +112 -0
  125. package/dist/features/seller/components/SellerLiveView.d.ts +5 -0
  126. package/dist/features/seller/components/SellerLiveView.js +113 -0
  127. package/dist/features/seller/components/SellerOrdersView.js +53 -2
  128. package/dist/features/seller/components/SellerPayoutMethodsView.d.ts +7 -0
  129. package/dist/features/seller/components/SellerPayoutMethodsView.js +120 -0
  130. package/dist/features/seller/components/SellerPayoutSettingsView.js +50 -12
  131. package/dist/features/seller/components/SellerProductShell.d.ts +3 -1
  132. package/dist/features/seller/components/SellerProductShell.js +35 -11
  133. package/dist/features/seller/components/SellerProductsCards.d.ts +19 -0
  134. package/dist/features/seller/components/SellerProductsCards.js +19 -0
  135. package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +14 -0
  136. package/dist/features/seller/components/SellerProductsFilterDrawer.js +22 -0
  137. package/dist/features/seller/components/SellerProductsView.d.ts +3 -1
  138. package/dist/features/seller/components/SellerProductsView.js +92 -49
  139. package/dist/features/seller/components/SellerReviewsView.js +67 -4
  140. package/dist/features/seller/components/SellerShippingConfigsView.d.ts +7 -0
  141. package/dist/features/seller/components/SellerShippingConfigsView.js +149 -0
  142. package/dist/features/seller/components/SellerShippingView.js +37 -10
  143. package/dist/features/seller/components/SellerSidebar.js +2 -1
  144. package/dist/features/seller/components/SellerStoreCategoriesView.d.ts +9 -0
  145. package/dist/features/seller/components/SellerStoreCategoriesView.js +122 -0
  146. package/dist/features/seller/components/SellerStorefrontView.d.ts +0 -7
  147. package/dist/features/seller/components/SellerStorefrontView.js +26 -29
  148. package/dist/features/seller/components/SellerTemplatesView.d.ts +10 -0
  149. package/dist/features/seller/components/SellerTemplatesView.js +265 -0
  150. package/dist/features/seller/components/index.d.ts +26 -0
  151. package/dist/features/seller/components/index.js +13 -0
  152. package/dist/features/seller/components/seller-products-styles.d.ts +7 -0
  153. package/dist/features/seller/components/seller-products-styles.js +14 -0
  154. package/dist/features/shell/FormShell.d.ts +7 -1
  155. package/dist/features/shell/FormShell.js +5 -2
  156. package/dist/features/store-extensions/index.d.ts +4 -0
  157. package/dist/features/store-extensions/index.js +4 -0
  158. package/dist/features/store-extensions/repository/rbac.repositories.d.ts +23 -0
  159. package/dist/features/store-extensions/repository/rbac.repositories.js +32 -0
  160. package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +91 -0
  161. package/dist/features/store-extensions/repository/store-extensions.repositories.js +127 -0
  162. package/dist/features/store-extensions/schemas/firestore.d.ts +244 -0
  163. package/dist/features/store-extensions/schemas/firestore.js +158 -0
  164. package/dist/features/store-extensions/schemas/rbac.d.ts +65 -0
  165. package/dist/features/store-extensions/schemas/rbac.js +43 -0
  166. package/dist/features/stores/api/[storeSlug]/reviews/route.js +49 -7
  167. package/dist/features/stores/components/InteractiveStoreCard.js +7 -3
  168. package/dist/features/stores/components/StoreCapabilitiesGuideView.d.ts +3 -0
  169. package/dist/features/stores/components/StoreCapabilitiesGuideView.js +101 -0
  170. package/dist/features/stores/components/StoreFinanceGuideView.d.ts +3 -0
  171. package/dist/features/stores/components/StoreFinanceGuideView.js +79 -0
  172. package/dist/features/stores/components/StoreGuideHubView.d.ts +5 -0
  173. package/dist/features/stores/components/StoreGuideHubView.js +89 -0
  174. package/dist/features/stores/components/StoreListingsGuideView.d.ts +3 -0
  175. package/dist/features/stores/components/StoreListingsGuideView.js +151 -0
  176. package/dist/features/stores/components/StoreOrdersGuideView.d.ts +3 -0
  177. package/dist/features/stores/components/StoreOrdersGuideView.js +122 -0
  178. package/dist/features/stores/components/StoreProductsListing.js +6 -10
  179. package/dist/features/stores/components/StoreReviewsListing.js +100 -23
  180. package/dist/features/stores/components/StoreSettingsGuideView.d.ts +3 -0
  181. package/dist/features/stores/components/StoreSettingsGuideView.js +56 -0
  182. package/dist/features/stores/components/index.d.ts +12 -1
  183. package/dist/features/stores/components/index.js +6 -1
  184. package/dist/features/stores/hooks/useStores.d.ts +5 -0
  185. package/dist/features/stores/hooks/useStores.js +10 -0
  186. package/dist/features/stores/manifest.js +1 -1
  187. package/dist/features/stores/schemas/firestore.d.ts +2 -0
  188. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +13 -2
  189. package/dist/index.d.ts +60 -9
  190. package/dist/index.js +66 -15
  191. package/dist/next/routing/route-map.d.ts +134 -0
  192. package/dist/next/routing/route-map.js +59 -0
  193. package/dist/providers/db-firebase/__tests__/filter-aliases.test.d.ts +1 -0
  194. package/dist/providers/db-firebase/__tests__/filter-aliases.test.js +93 -0
  195. package/dist/providers/db-firebase/sieve.d.ts +49 -0
  196. package/dist/providers/db-firebase/sieve.js +61 -8
  197. package/dist/react/hooks/useFormStatePreservation.d.ts +17 -0
  198. package/dist/react/hooks/useFormStatePreservation.js +62 -0
  199. package/dist/react/hooks/useInlineRowEdit.d.ts +24 -0
  200. package/dist/react/hooks/useInlineRowEdit.js +68 -0
  201. package/dist/react/index.d.ts +4 -0
  202. package/dist/react/index.js +4 -0
  203. package/dist/repositories/index.d.ts +5 -0
  204. package/dist/repositories/index.js +7 -0
  205. package/dist/seed/actions/demo-seed-actions.d.ts +1 -1
  206. package/dist/seed/claimed-coupons-seed-data.d.ts +13 -0
  207. package/dist/seed/claimed-coupons-seed-data.js +79 -0
  208. package/dist/seed/index.d.ts +2 -0
  209. package/dist/seed/index.js +3 -0
  210. package/dist/seed/manifest.js +17 -0
  211. package/dist/seed/store-extensions-seed-data.d.ts +19 -0
  212. package/dist/seed/store-extensions-seed-data.js +421 -0
  213. package/dist/tailwind-utilities.css +1 -1
  214. package/dist/ui/components/Button.style.css +20 -12
  215. package/dist/ui/components/ClaimCouponButton.d.ts +28 -0
  216. package/dist/ui/components/ClaimCouponButton.js +89 -0
  217. package/dist/ui/components/ListingToolbar.d.ts +8 -1
  218. package/dist/ui/components/ListingToolbar.js +4 -2
  219. package/dist/ui/components/QuickCreateModal.d.ts +15 -0
  220. package/dist/ui/components/QuickCreateModal.js +48 -0
  221. package/dist/ui/components/QuickCreateModal.style.css +84 -0
  222. package/dist/ui/components/StickyBottomBar.d.ts +22 -0
  223. package/dist/ui/components/StickyBottomBar.js +20 -0
  224. package/dist/ui/components/VacationBanner.d.ts +11 -0
  225. package/dist/ui/components/VacationBanner.js +16 -0
  226. package/dist/ui/components/__tests__/BulkActionBar.test.d.ts +1 -0
  227. package/dist/ui/components/__tests__/BulkActionBar.test.js +96 -0
  228. package/dist/ui/components/__tests__/ListingToolbar.test.d.ts +1 -0
  229. package/dist/ui/components/__tests__/ListingToolbar.test.js +125 -0
  230. package/dist/ui/index.d.ts +9 -1
  231. package/dist/ui/index.js +4 -0
  232. package/package.json +8 -2
@@ -81,41 +81,49 @@
81
81
  gap: 0.625rem;
82
82
  }
83
83
 
84
- .appkit-button--primary {
84
+ /* Filled variants — colour rules use a chained-class selector (specificity
85
+ (0,2,0)) so they beat single-class Tailwind text utilities (0,1,0). This
86
+ guarantees contrast-safe foregrounds even when call-sites add a stray
87
+ `text-*` className. Tailwind utilities still apply for non-color props
88
+ like `text-xs` (font-size). See plan §4 "Defensive layer" + the global
89
+ sweep request 2026-05-18. */
90
+
91
+ .appkit-button.appkit-button--primary {
85
92
  background: var(--appkit-color-primary-700);
86
93
  color: var(--appkit-color-text-on-primary);
87
94
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
88
95
  }
89
96
 
90
- .appkit-button--primary:hover:not(:disabled) {
97
+ .appkit-button.appkit-button--primary:hover:not(:disabled) {
91
98
  background: var(--appkit-color-primary-800);
92
99
  }
93
100
 
94
- .dark .appkit-button--primary {
101
+ .dark .appkit-button.appkit-button--primary {
95
102
  background: var(--appkit-color-secondary);
103
+ color: var(--appkit-color-text-on-primary);
96
104
  }
97
105
 
98
- .dark .appkit-button--primary:hover:not(:disabled) {
106
+ .dark .appkit-button.appkit-button--primary:hover:not(:disabled) {
99
107
  background: var(--appkit-color-secondary-400);
100
108
  }
101
109
 
102
- .appkit-button--secondary {
110
+ .appkit-button.appkit-button--secondary {
103
111
  background: var(--appkit-color-surface);
104
112
  border-color: var(--appkit-color-primary-700);
105
113
  color: var(--appkit-color-primary-700);
106
114
  }
107
115
 
108
- .appkit-button--secondary:hover:not(:disabled) {
116
+ .appkit-button.appkit-button--secondary:hover:not(:disabled) {
109
117
  background: color-mix(in srgb, var(--appkit-color-primary-700) 8%, transparent);
110
118
  }
111
119
 
112
- .dark .appkit-button--secondary {
120
+ .dark .appkit-button.appkit-button--secondary {
113
121
  background: transparent;
114
122
  border-color: var(--appkit-color-secondary);
115
123
  color: var(--appkit-color-secondary);
116
124
  }
117
125
 
118
- .dark .appkit-button--secondary:hover:not(:disabled) {
126
+ .dark .appkit-button.appkit-button--secondary:hover:not(:disabled) {
119
127
  background: color-mix(in srgb, var(--appkit-color-secondary) 12%, transparent);
120
128
  }
121
129
 
@@ -156,21 +164,21 @@
156
164
  background: rgba(30, 41, 59, 0.5);
157
165
  }
158
166
 
159
- .appkit-button--danger {
167
+ .appkit-button.appkit-button--danger {
160
168
  background: var(--appkit-color-error);
161
169
  color: var(--appkit-color-text-on-primary);
162
170
  }
163
171
 
164
- .appkit-button--danger:hover:not(:disabled) {
172
+ .appkit-button.appkit-button--danger:hover:not(:disabled) {
165
173
  background: var(--appkit-color-error-hover);
166
174
  }
167
175
 
168
- .appkit-button--warning {
176
+ .appkit-button.appkit-button--warning {
169
177
  background: var(--appkit-color-warning);
170
178
  color: var(--appkit-color-text-on-primary);
171
179
  }
172
180
 
173
- .appkit-button--warning:hover:not(:disabled) {
181
+ .appkit-button.appkit-button--warning:hover:not(:disabled) {
174
182
  background: var(--appkit-color-amber-400);
175
183
  }
176
184
 
@@ -0,0 +1,28 @@
1
+ /**
2
+ * ClaimCouponButton — surfaced anywhere a user wins or earns a coupon
3
+ * (SpinWheel "YOU WON" panel, raffle winner page, prize-draw reveal modal,
4
+ * promotional inserts).
5
+ *
6
+ * Behaviour:
7
+ * 1. If the visitor is not signed in, opens the standard LoginRequiredModal —
8
+ * no coupon is copied or saved. The wallet is per-user, so anonymous
9
+ * claims would be lost.
10
+ * 2. POST `/api/user/coupons/claim` to persist the claim into the user's
11
+ * wallet (idempotent — second claim is a no-op).
12
+ * 3. Copy the code to the clipboard so the user can paste it at checkout.
13
+ *
14
+ * No more cart deep-link — the wallet ("My Coupons" page) is the single home
15
+ * for claimed coupons, and the user applies them from there at checkout.
16
+ */
17
+ import React from "react";
18
+ export interface ClaimCouponButtonProps {
19
+ couponCode: string;
20
+ /** Optional server-side claim handler — overrides the default wallet POST. */
21
+ onClaim?: (code: string) => Promise<void> | void;
22
+ /** Override the button label. */
23
+ label?: string;
24
+ className?: string;
25
+ size?: "sm" | "md" | "lg";
26
+ source?: "manual" | "promo" | "spin" | "raffle" | "prize-draw";
27
+ }
28
+ export declare function ClaimCouponButton({ couponCode, onClaim, label, className, size, source, }: ClaimCouponButtonProps): React.JSX.Element;
@@ -0,0 +1,89 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * ClaimCouponButton — surfaced anywhere a user wins or earns a coupon
5
+ * (SpinWheel "YOU WON" panel, raffle winner page, prize-draw reveal modal,
6
+ * promotional inserts).
7
+ *
8
+ * Behaviour:
9
+ * 1. If the visitor is not signed in, opens the standard LoginRequiredModal —
10
+ * no coupon is copied or saved. The wallet is per-user, so anonymous
11
+ * claims would be lost.
12
+ * 2. POST `/api/user/coupons/claim` to persist the claim into the user's
13
+ * wallet (idempotent — second claim is a no-op).
14
+ * 3. Copy the code to the clipboard so the user can paste it at checkout.
15
+ *
16
+ * No more cart deep-link — the wallet ("My Coupons" page) is the single home
17
+ * for claimed coupons, and the user applies them from there at checkout.
18
+ */
19
+ import { useCallback, useState } from "react";
20
+ import { Button } from "./Button";
21
+ import { LoginRequiredModal } from "./LoginRequiredModal";
22
+ import { useSession } from "../../react/contexts/SessionContext";
23
+ import { useToast } from "./Toast";
24
+ async function postClaim(couponCode, source) {
25
+ if (typeof fetch === "undefined")
26
+ return false;
27
+ try {
28
+ const res = await fetch("/api/user/coupons/claim", {
29
+ method: "POST",
30
+ headers: { "Content-Type": "application/json" },
31
+ credentials: "include",
32
+ body: JSON.stringify({ couponCode, source }),
33
+ });
34
+ return res.ok;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ async function copyToClipboard(text) {
41
+ if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
42
+ return false;
43
+ }
44
+ try {
45
+ await navigator.clipboard.writeText(text);
46
+ return true;
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
52
+ export function ClaimCouponButton({ couponCode, onClaim, label, className, size = "md", source = "manual", }) {
53
+ const { user, loading } = useSession();
54
+ const { showToast } = useToast();
55
+ const [busy, setBusy] = useState(false);
56
+ const [claimed, setClaimed] = useState(false);
57
+ const [loginOpen, setLoginOpen] = useState(false);
58
+ const handleClick = useCallback(async () => {
59
+ if (busy || !couponCode)
60
+ return;
61
+ if (loading)
62
+ return;
63
+ if (!user) {
64
+ setLoginOpen(true);
65
+ return;
66
+ }
67
+ setBusy(true);
68
+ try {
69
+ const ok = onClaim != null
70
+ ? (await Promise.resolve(onClaim(couponCode)), true)
71
+ : await postClaim(couponCode, source);
72
+ if (!ok) {
73
+ showToast("Could not save coupon. Please try again.", "error");
74
+ return;
75
+ }
76
+ await copyToClipboard(couponCode);
77
+ setClaimed(true);
78
+ showToast(`${couponCode} saved to your coupons.`, "success");
79
+ }
80
+ finally {
81
+ setBusy(false);
82
+ }
83
+ }, [busy, couponCode, loading, onClaim, showToast, source, user]);
84
+ return (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", variant: "primary", size: size, onClick: handleClick, disabled: busy || !couponCode || loading, className: className, children: busy
85
+ ? "Saving…"
86
+ : claimed
87
+ ? "Saved to My Coupons"
88
+ : (label ?? "Claim coupon") }), _jsx(LoginRequiredModal, { isOpen: loginOpen, onClose: () => setLoginOpen(false), message: "Sign in to save coupons to your wallet." })] }));
89
+ }
@@ -3,6 +3,11 @@ export interface ListingToolbarSortOption {
3
3
  value: string;
4
4
  label: string;
5
5
  }
6
+ export interface ListingToolbarToggle {
7
+ label: string;
8
+ active: boolean;
9
+ onChange: (next: boolean) => void;
10
+ }
6
11
  export interface ListingToolbarLabels {
7
12
  search?: string;
8
13
  filters?: string;
@@ -48,10 +53,12 @@ export interface ListingToolbarProps {
48
53
  bulkTotalCount?: number;
49
54
  onBulkSelectAll?: () => void;
50
55
  onBulkClear?: () => void;
56
+ /** Pill-style filter toggles rendered between view controls and extra */
57
+ toggles?: ListingToolbarToggle[];
51
58
  /** Any extra action buttons placed after the view toggle */
52
59
  extra?: React.ReactNode;
53
60
  /** Overridable text labels for i18n */
54
61
  labels?: ListingToolbarLabels;
55
62
  className?: string;
56
63
  }
57
- export declare function ListingToolbar({ filterCount, onFiltersClick, searchValue, searchPlaceholder, onSearchChange, onSearchCommit, onSearchKeyDown, sortValue, sortOptions, onSortChange, view, onViewChange, hideViewToggle, showTableView, onResetAll, hasActiveState, bulkMode, bulkSelectedCount, bulkTotalCount, onBulkSelectAll, onBulkClear, extra, labels, className, }: ListingToolbarProps): import("react/jsx-runtime").JSX.Element;
64
+ export declare function ListingToolbar({ filterCount, onFiltersClick, searchValue, searchPlaceholder, onSearchChange, onSearchCommit, onSearchKeyDown, sortValue, sortOptions, onSortChange, view, onViewChange, hideViewToggle, showTableView, onResetAll, hasActiveState, bulkMode, bulkSelectedCount, bulkTotalCount, onBulkSelectAll, onBulkClear, toggles, extra, labels, className, }: ListingToolbarProps): import("react/jsx-runtime").JSX.Element;
@@ -18,7 +18,7 @@ const DEFAULT_LABELS = {
18
18
  const VIEW_BTN_BASE = "p-1.5 sm:p-2 transition-colors";
19
19
  const VIEW_BTN_ACTIVE = "bg-[var(--appkit-color-primary,theme(colors.violet.600))] text-white";
20
20
  const VIEW_BTN_INACTIVE = "text-zinc-500 hover:bg-zinc-50 dark:hover:bg-slate-800 dark:text-zinc-400";
21
- export function ListingToolbar({ filterCount = 0, onFiltersClick, searchValue = "", searchPlaceholder = "Search…", onSearchChange, onSearchCommit, onSearchKeyDown, sortValue, sortOptions, onSortChange, view = "grid", onViewChange, hideViewToggle = false, showTableView = false, onResetAll, hasActiveState = false, bulkMode = false, bulkSelectedCount = 0, bulkTotalCount = 0, onBulkSelectAll, onBulkClear, extra, labels, className = "", }) {
21
+ export function ListingToolbar({ filterCount = 0, onFiltersClick, searchValue = "", searchPlaceholder = "Search…", onSearchChange, onSearchCommit, onSearchKeyDown, sortValue, sortOptions, onSortChange, view = "grid", onViewChange, hideViewToggle = false, showTableView = false, onResetAll, hasActiveState = false, bulkMode = false, bulkSelectedCount = 0, bulkTotalCount = 0, onBulkSelectAll, onBulkClear, toggles, extra, labels, className = "", }) {
22
22
  const l = { ...DEFAULT_LABELS, ...labels };
23
23
  const handleKeyDown = (e) => {
24
24
  if (onSearchKeyDown) {
@@ -31,5 +31,7 @@ export function ListingToolbar({ filterCount = 0, onFiltersClick, searchValue =
31
31
  const allSelected = bulkTotalCount > 0 && bulkSelectedCount === bulkTotalCount;
32
32
  return (_jsx("div", { "data-testid": "listing-toolbar", className: `sticky top-[var(--header-height,0px)] z-20 border-b border-zinc-200 dark:border-slate-700 bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm py-2 px-3 sm:py-2.5 sm:px-4 ${className}`, children: _jsxs("div", { className: "flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-2.5", children: [bulkMode ? (_jsxs("div", { className: "flex flex-1 items-center gap-2", children: [_jsxs("button", { type: "button", onClick: onBulkSelectAll, className: "flex items-center gap-1.5 rounded-lg border border-zinc-300 dark:border-slate-600 px-3 py-1.5 text-sm font-medium text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-slate-800 transition-colors", children: [allSelected
33
33
  ? _jsx(CheckSquare, { className: "h-4 w-4 text-[var(--appkit-color-primary,theme(colors.violet.600))]" })
34
- : _jsx(Square, { className: "h-4 w-4" }), allSelected ? l.deselectAll : l.selectAll(bulkTotalCount)] }), _jsx("span", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: l.selected(bulkSelectedCount) }), _jsx("button", { type: "button", onClick: onBulkClear, className: "text-xs text-zinc-400 hover:text-rose-500 dark:text-zinc-500 transition-colors", children: l.clearSelection })] })) : onSearchChange ? (_jsxs("div", { className: "flex flex-1 items-center overflow-hidden rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-900 min-w-0", children: [_jsx("input", { type: "text", value: searchValue, onChange: (e) => onSearchChange(e.target.value), onKeyDown: handleKeyDown, placeholder: searchPlaceholder, className: "min-w-0 flex-1 bg-transparent px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 outline-none" }), _jsx("button", { type: "button", onClick: onSearchCommit, className: "flex shrink-0 items-center justify-center px-2.5 py-2 text-zinc-400 hover:text-[var(--appkit-color-primary,theme(colors.violet.600))] transition-colors", "aria-label": l.search, children: _jsx(Search, { className: "h-4 w-4" }) })] })) : null, _jsxs("div", { className: "flex items-center gap-1.5 sm:gap-2 shrink-0", children: [onFiltersClick && (_jsxs("button", { type: "button", onClick: onFiltersClick, className: "relative flex shrink-0 items-center gap-1.5 rounded-lg border border-zinc-300 dark:border-slate-600 px-2.5 py-1.5 sm:px-3.5 sm:py-2 text-sm font-medium text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-slate-800 transition-colors", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: l.filters }), filterCount > 0 && (_jsx("span", { className: "absolute -top-1.5 -right-1.5 flex h-4 w-4 items-center justify-center rounded-full bg-[var(--appkit-color-primary,theme(colors.violet.600))] text-[10px] font-bold text-white", children: filterCount }))] })), sortOptions && sortValue !== undefined && onSortChange && (_jsxs("div", { className: "flex items-center gap-1.5 text-sm text-zinc-500 dark:text-zinc-400", children: [_jsx("span", { className: "hidden md:inline whitespace-nowrap text-xs", children: l.sort }), _jsx(SortDropdown, { value: sortValue, onChange: onSortChange, options: sortOptions })] })), !hideViewToggle && onViewChange && (_jsxs("div", { className: "flex items-center rounded-lg border border-zinc-300 dark:border-slate-600 overflow-hidden", children: [_jsx("button", { type: "button", onClick: () => onViewChange("grid"), "aria-label": l.gridView, className: `${VIEW_BTN_BASE} ${view === "grid" ? VIEW_BTN_ACTIVE : VIEW_BTN_INACTIVE}`, children: _jsx(LayoutGrid, { className: "h-4 w-4" }) }), _jsx("button", { type: "button", onClick: () => onViewChange("list"), "aria-label": l.listView, className: `${VIEW_BTN_BASE} ${view === "list" ? VIEW_BTN_ACTIVE : VIEW_BTN_INACTIVE}`, children: _jsx(List, { className: "h-4 w-4" }) }), showTableView && (_jsx("button", { type: "button", onClick: () => onViewChange("table"), "aria-label": l.tableView, className: `${VIEW_BTN_BASE} ${view === "table" ? VIEW_BTN_ACTIVE : VIEW_BTN_INACTIVE}`, children: _jsx(Table2, { className: "h-4 w-4" }) }))] })), onResetAll && hasActiveState && (_jsx("button", { type: "button", onClick: onResetAll, "aria-label": l.resetAll, title: l.resetAll, className: "flex shrink-0 items-center justify-center rounded-lg border border-zinc-300 dark:border-slate-600 p-1.5 sm:p-2 text-zinc-500 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-slate-800 hover:text-rose-500 dark:hover:text-rose-400 transition-colors", children: _jsx(RotateCcw, { className: "h-4 w-4" }) })), extra] })] }) }));
34
+ : _jsx(Square, { className: "h-4 w-4" }), allSelected ? l.deselectAll : l.selectAll(bulkTotalCount)] }), _jsx("span", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: l.selected(bulkSelectedCount) }), _jsx("button", { type: "button", onClick: onBulkClear, className: "text-xs text-zinc-400 hover:text-rose-500 dark:text-zinc-500 transition-colors", children: l.clearSelection })] })) : onSearchChange ? (_jsxs("div", { className: "flex flex-1 items-center overflow-hidden rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-900 min-w-0", children: [_jsx("input", { type: "text", value: searchValue, onChange: (e) => onSearchChange(e.target.value), onKeyDown: handleKeyDown, placeholder: searchPlaceholder, className: "min-w-0 flex-1 bg-transparent px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 outline-none" }), _jsx("button", { type: "button", onClick: onSearchCommit, className: "flex shrink-0 items-center justify-center px-2.5 py-2 text-zinc-400 hover:text-[var(--appkit-color-primary,theme(colors.violet.600))] transition-colors", "aria-label": l.search, children: _jsx(Search, { className: "h-4 w-4" }) })] })) : null, _jsxs("div", { className: "flex items-center gap-1.5 sm:gap-2 shrink-0", children: [onFiltersClick && (_jsxs("button", { type: "button", onClick: onFiltersClick, className: "relative flex shrink-0 items-center gap-1.5 rounded-lg border border-zinc-300 dark:border-slate-600 px-2.5 py-1.5 sm:px-3.5 sm:py-2 text-sm font-medium text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-slate-800 transition-colors", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: l.filters }), filterCount > 0 && (_jsx("span", { className: "absolute -top-1.5 -right-1.5 flex h-4 w-4 items-center justify-center rounded-full bg-[var(--appkit-color-primary,theme(colors.violet.600))] text-[10px] font-bold text-white", children: filterCount }))] })), sortOptions && sortValue !== undefined && onSortChange && (_jsxs("div", { className: "flex items-center gap-1.5 text-sm text-zinc-500 dark:text-zinc-400", children: [_jsx("span", { className: "hidden md:inline whitespace-nowrap text-xs", children: l.sort }), _jsx(SortDropdown, { value: sortValue, onChange: onSortChange, options: sortOptions })] })), !hideViewToggle && onViewChange && (_jsxs("div", { className: "flex items-center rounded-lg border border-zinc-300 dark:border-slate-600 overflow-hidden", children: [_jsx("button", { type: "button", onClick: () => onViewChange("grid"), "aria-label": l.gridView, className: `${VIEW_BTN_BASE} ${view === "grid" ? VIEW_BTN_ACTIVE : VIEW_BTN_INACTIVE}`, children: _jsx(LayoutGrid, { className: "h-4 w-4" }) }), _jsx("button", { type: "button", onClick: () => onViewChange("list"), "aria-label": l.listView, className: `${VIEW_BTN_BASE} ${view === "list" ? VIEW_BTN_ACTIVE : VIEW_BTN_INACTIVE}`, children: _jsx(List, { className: "h-4 w-4" }) }), showTableView && (_jsx("button", { type: "button", onClick: () => onViewChange("table"), "aria-label": l.tableView, className: `${VIEW_BTN_BASE} ${view === "table" ? VIEW_BTN_ACTIVE : VIEW_BTN_INACTIVE}`, children: _jsx(Table2, { className: "h-4 w-4" }) }))] })), onResetAll && hasActiveState && (_jsx("button", { type: "button", onClick: onResetAll, "aria-label": l.resetAll, title: l.resetAll, className: "flex shrink-0 items-center justify-center rounded-lg border border-zinc-300 dark:border-slate-600 p-1.5 sm:p-2 text-zinc-500 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-slate-800 hover:text-rose-500 dark:hover:text-rose-400 transition-colors", children: _jsx(RotateCcw, { className: "h-4 w-4" }) })), toggles && toggles.length > 0 && (_jsx("div", { className: "flex items-center gap-1", children: toggles.map((t) => (_jsx("button", { type: "button", role: "switch", "aria-checked": t.active, onClick: () => t.onChange(!t.active), className: `rounded-full border px-2.5 py-1 text-xs font-medium transition-colors whitespace-nowrap ${t.active
35
+ ? "border-transparent bg-[var(--appkit-color-primary,theme(colors.violet.600))] text-white"
36
+ : "border-zinc-300 dark:border-slate-600 text-zinc-600 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-slate-800"}`, children: t.label }, t.label))) })), extra] })] }) }));
35
37
  }
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+ export interface QuickCreateModalProps<T = unknown> {
3
+ isOpen: boolean;
4
+ title: string;
5
+ onSave: () => Promise<T> | T;
6
+ onSaved?: (doc: T) => void;
7
+ onCancel: () => void;
8
+ children: React.ReactNode;
9
+ fullPageHref?: string;
10
+ saveLabel?: string;
11
+ cancelLabel?: string;
12
+ isSaving?: boolean;
13
+ saveDisabled?: boolean;
14
+ }
15
+ export declare function QuickCreateModal<T = unknown>({ isOpen, title, onSave, onSaved, onCancel, children, fullPageHref, saveLabel, cancelLabel, isSaving, saveDisabled, }: QuickCreateModalProps<T>): React.ReactPortal | null;
@@ -0,0 +1,48 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { Button } from "./Button";
6
+ import { Div } from "./Div";
7
+ import { Heading } from "./Typography";
8
+ import { Row } from "./Layout";
9
+ export function QuickCreateModal({ isOpen, title, onSave, onSaved, onCancel, children, fullPageHref, saveLabel = "Save", cancelLabel = "Cancel", isSaving = false, saveDisabled = false, }) {
10
+ const [busy, setBusy] = React.useState(false);
11
+ const titleId = React.useId();
12
+ const panelRef = React.useRef(null);
13
+ React.useEffect(() => {
14
+ if (!isOpen)
15
+ return;
16
+ const prev = document.body.style.overflow;
17
+ document.body.style.overflow = "hidden";
18
+ return () => {
19
+ document.body.style.overflow = prev;
20
+ };
21
+ }, [isOpen]);
22
+ React.useEffect(() => {
23
+ if (!isOpen)
24
+ return;
25
+ const handler = (e) => {
26
+ if (e.key === "Escape" && !busy)
27
+ onCancel();
28
+ };
29
+ document.addEventListener("keydown", handler);
30
+ return () => document.removeEventListener("keydown", handler);
31
+ }, [isOpen, onCancel, busy]);
32
+ const handleSave = async () => {
33
+ if (saveDisabled || busy)
34
+ return;
35
+ setBusy(true);
36
+ try {
37
+ const doc = await onSave();
38
+ onSaved?.(doc);
39
+ }
40
+ finally {
41
+ setBusy(false);
42
+ }
43
+ };
44
+ if (!isOpen || typeof document === "undefined")
45
+ return null;
46
+ const saving = busy || isSaving;
47
+ return createPortal(_jsxs(Div, { className: "appkit-quick-create-modal", role: "dialog", "aria-modal": "true", "aria-labelledby": titleId, children: [_jsx(Div, { className: "appkit-quick-create-modal__backdrop", "aria-hidden": "true", onClick: () => !saving && onCancel() }), _jsxs(Div, { ref: panelRef, tabIndex: -1, className: "appkit-quick-create-modal__panel", children: [_jsxs(Div, { className: "appkit-quick-create-modal__header", children: [_jsx(Heading, { level: 2, id: titleId, className: "appkit-quick-create-modal__title", children: title }), _jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: onCancel, "aria-label": "Close", disabled: saving, children: "\u00D7" })] }), _jsx(Div, { className: "appkit-quick-create-modal__body", children: children }), _jsxs(Div, { className: "appkit-quick-create-modal__footer", children: [fullPageHref ? (_jsx("a", { href: fullPageHref, target: "_blank", rel: "noreferrer", className: "appkit-quick-create-modal__full-link", children: "Add more details \u2192" })) : (_jsx("span", {})), _jsxs(Row, { className: "appkit-quick-create-modal__actions", children: [_jsx(Button, { variant: "ghost", type: "button", onClick: onCancel, disabled: saving, children: cancelLabel }), _jsx(Button, { variant: "primary", type: "button", onClick: handleSave, disabled: saving || saveDisabled, isLoading: saving, children: saveLabel })] })] })] })] }), document.body);
48
+ }
@@ -0,0 +1,84 @@
1
+ .appkit-quick-create-modal {
2
+ position: fixed;
3
+ inset: 0;
4
+ z-index: var(--appkit-z-modal, 1000);
5
+ display: flex;
6
+ align-items: stretch;
7
+ justify-content: center;
8
+ }
9
+
10
+ .appkit-quick-create-modal__backdrop {
11
+ position: absolute;
12
+ inset: 0;
13
+ background: rgb(0 0 0 / 0.45);
14
+ }
15
+
16
+ .appkit-quick-create-modal__panel {
17
+ position: relative;
18
+ display: flex;
19
+ flex-direction: column;
20
+ background: var(--appkit-color-surface, #ffffff);
21
+ color: var(--appkit-color-foreground, inherit);
22
+ width: 100%;
23
+ max-width: 560px;
24
+ margin: auto;
25
+ max-height: 92vh;
26
+ border-radius: 0.75rem;
27
+ box-shadow: var(--appkit-shadow-lg, 0 20px 40px rgb(0 0 0 / 0.25));
28
+ outline: none;
29
+ }
30
+
31
+ .appkit-quick-create-modal__header {
32
+ display: flex;
33
+ align-items: center;
34
+ justify-content: space-between;
35
+ padding: 1rem 1.25rem;
36
+ border-bottom: 1px solid var(--appkit-color-border, rgb(0 0 0 / 0.1));
37
+ }
38
+
39
+ .appkit-quick-create-modal__title {
40
+ margin: 0;
41
+ font-size: 1.125rem;
42
+ font-weight: 600;
43
+ }
44
+
45
+ .appkit-quick-create-modal__body {
46
+ flex: 1 1 auto;
47
+ overflow-y: auto;
48
+ padding: 1.25rem;
49
+ }
50
+
51
+ .appkit-quick-create-modal__footer {
52
+ display: flex;
53
+ align-items: center;
54
+ justify-content: space-between;
55
+ gap: 0.75rem;
56
+ padding: 1rem 1.25rem;
57
+ border-top: 1px solid var(--appkit-color-border, rgb(0 0 0 / 0.1));
58
+ }
59
+
60
+ .appkit-quick-create-modal__full-link {
61
+ font-size: 0.875rem;
62
+ color: var(--appkit-color-primary, #6366f1);
63
+ text-decoration: underline;
64
+ }
65
+
66
+ .appkit-quick-create-modal__actions {
67
+ gap: 0.5rem;
68
+ }
69
+
70
+ @media (max-width: 640px) {
71
+ .appkit-quick-create-modal {
72
+ align-items: flex-end;
73
+ }
74
+ .appkit-quick-create-modal__panel {
75
+ max-width: 100%;
76
+ margin: 0;
77
+ border-radius: 0.75rem 0.75rem 0 0;
78
+ max-height: 92vh;
79
+ }
80
+ }
81
+
82
+ [data-theme="dark"] .appkit-quick-create-modal__panel {
83
+ background: var(--appkit-color-surface-dark, #1e293b);
84
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * StickyBottomBar — fixed-bottom action bar that sits above the mobile
3
+ * BottomNavLayout (when present) and below any modals.
4
+ *
5
+ * Reads `--bottom-nav-height` written by `BottomNavLayout` (4rem when the
6
+ * navbar is rendered below `lg`, 0px otherwise). Falls back to 0px when the
7
+ * variable is unset so the bar still pins to the viewport bottom on routes
8
+ * that don't render a bottom nav.
9
+ *
10
+ * z-index sits above the navbar (`z-40`) but below modal overlays.
11
+ */
12
+ import React from "react";
13
+ export interface StickyBottomBarProps {
14
+ children: React.ReactNode;
15
+ /** Optional extra className appended to the bar wrapper. */
16
+ className?: string;
17
+ /** Hide on `lg` and up. Defaults to `true` — matches BottomNavLayout's responsive scope. */
18
+ mobileOnly?: boolean;
19
+ /** Test id passthrough. */
20
+ "data-testid"?: string;
21
+ }
22
+ export declare function StickyBottomBar({ children, className, mobileOnly, "data-testid": testId, }: StickyBottomBarProps): React.JSX.Element;
@@ -0,0 +1,20 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ const BAR_STYLE = {
4
+ bottom: "var(--bottom-nav-height, 0px)",
5
+ zIndex: 45,
6
+ };
7
+ export function StickyBottomBar({ children, className, mobileOnly = true, "data-testid": testId, }) {
8
+ return (_jsx("div", {
9
+ // eslint-disable-next-line lir/no-inline-static-style
10
+ style: BAR_STYLE, "data-testid": testId, className: [
11
+ "fixed left-0 right-0",
12
+ mobileOnly ? "lg:hidden" : "",
13
+ "bg-white/95 dark:bg-slate-950/95 backdrop-blur-md",
14
+ "border-t border-zinc-200 dark:border-slate-800",
15
+ "shadow-2xl pb-safe",
16
+ className ?? "",
17
+ ]
18
+ .filter(Boolean)
19
+ .join(" "), children: children }));
20
+ }
@@ -0,0 +1,11 @@
1
+ export interface VacationBannerProps {
2
+ storeName?: string;
3
+ message?: string;
4
+ returnDate?: Date | string;
5
+ className?: string;
6
+ }
7
+ /**
8
+ * S-STORE-6-A — Vacation-mode banner shown on product detail + checkout
9
+ * pages when the seller has paused new orders.
10
+ */
11
+ export declare function VacationBanner({ storeName, message, returnDate, className, }: VacationBannerProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,16 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Alert } from "./Alert";
4
+ /**
5
+ * S-STORE-6-A — Vacation-mode banner shown on product detail + checkout
6
+ * pages when the seller has paused new orders.
7
+ */
8
+ export function VacationBanner({ storeName, message, returnDate, className, }) {
9
+ const formattedDate = returnDate &&
10
+ new Date(returnDate).toLocaleDateString(undefined, {
11
+ year: "numeric",
12
+ month: "short",
13
+ day: "numeric",
14
+ });
15
+ return (_jsx(Alert, { variant: "warning", className: className, children: _jsxs("div", { className: "text-sm", children: [_jsx("strong", { children: storeName ? `${storeName} is on vacation` : "Store is on vacation" }), message ? _jsxs("span", { children: [" \u2014 ", message] }) : null, formattedDate ? _jsxs("span", { children: [" Returns ", formattedDate, "."] }) : null, _jsx("div", { className: "text-xs opacity-80 mt-1", children: "New orders are paused; you can still wishlist items." })] }) }));
16
+ }
@@ -0,0 +1,96 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render, screen, fireEvent } from "@testing-library/react";
3
+ import { describe, it, expect, vi } from "vitest";
4
+ import { BulkActionBar } from "../BulkActionBar";
5
+ const actions = [
6
+ { id: "publish", label: "Publish", onClick: vi.fn() },
7
+ { id: "delete", label: "Delete", variant: "danger", onClick: vi.fn() },
8
+ ];
9
+ describe("BulkActionBar", () => {
10
+ it("renders nothing when selectedCount is 0", () => {
11
+ render(_jsx(BulkActionBar, { selectedCount: 0, actions: actions }));
12
+ expect(screen.queryByTestId("bulk-action-bar")).toBeNull();
13
+ });
14
+ it("renders the bar when selectedCount > 0", () => {
15
+ render(_jsx(BulkActionBar, { selectedCount: 3, actions: actions }));
16
+ expect(screen.getByTestId("bulk-action-bar")).toBeTruthy();
17
+ });
18
+ it("shows the selected count in the count pill", () => {
19
+ render(_jsx(BulkActionBar, { selectedCount: 5, actions: actions }));
20
+ expect(screen.getByTestId("bulk-selected-count").textContent).toContain("5");
21
+ });
22
+ it("calls onClearSelection when the count pill button is clicked", () => {
23
+ const onClear = vi.fn();
24
+ render(_jsx(BulkActionBar, { selectedCount: 2, actions: actions, onClearSelection: onClear }));
25
+ fireEvent.click(screen.getByRole("button", { name: /clear selection/i }));
26
+ expect(onClear).toHaveBeenCalledOnce();
27
+ });
28
+ it("renders no action buttons when actions array is empty", () => {
29
+ render(_jsx(BulkActionBar, { selectedCount: 1, actions: [] }));
30
+ expect(screen.queryByRole("button", { name: /apply/i })).toBeNull();
31
+ });
32
+ it("renders the picker trigger and apply button when actions are provided", () => {
33
+ render(_jsx(BulkActionBar, { selectedCount: 1, actions: actions }));
34
+ // Apply button must exist
35
+ expect(screen.getByRole("button", { name: /apply/i })).toBeTruthy();
36
+ // Picker trigger has aria-haspopup="listbox"
37
+ expect(screen.getByRole("button", { expanded: false })).toBeTruthy();
38
+ });
39
+ it("opens the action picker on trigger click", () => {
40
+ render(_jsx(BulkActionBar, { selectedCount: 1, actions: actions }));
41
+ const trigger = screen.getByRole("button", { expanded: false });
42
+ fireEvent.click(trigger);
43
+ // Trigger now has aria-expanded="true"
44
+ expect(screen.getByRole("button", { expanded: true })).toBeTruthy();
45
+ });
46
+ it("selects an action when an option is clicked and closes the picker", () => {
47
+ render(_jsx(BulkActionBar, { selectedCount: 1, actions: actions }));
48
+ // Open picker
49
+ const trigger = screen.getByRole("button", { expanded: false });
50
+ fireEvent.click(trigger);
51
+ // Trigger is expanded
52
+ expect(trigger.getAttribute("aria-expanded")).toBe("true");
53
+ // Click Delete option
54
+ fireEvent.click(screen.getByRole("option", { name: /delete/i }));
55
+ // Picker should close — aria-expanded returns to false
56
+ expect(trigger.getAttribute("aria-expanded")).toBe("false");
57
+ });
58
+ it("calls the selected action's onClick when Apply is clicked", () => {
59
+ const publishFn = vi.fn();
60
+ const actionsWithFns = [
61
+ { id: "publish", label: "Publish", onClick: publishFn },
62
+ ];
63
+ render(_jsx(BulkActionBar, { selectedCount: 1, actions: actionsWithFns }));
64
+ fireEvent.click(screen.getByRole("button", { name: /apply/i }));
65
+ expect(publishFn).toHaveBeenCalledOnce();
66
+ });
67
+ it("executes the newly selected action when a different option is chosen then Apply clicked", () => {
68
+ const publishFn = vi.fn();
69
+ const deleteFn = vi.fn();
70
+ const actionsWithFns = [
71
+ { id: "publish", label: "Publish", onClick: publishFn },
72
+ { id: "delete", label: "Delete", variant: "danger", onClick: deleteFn },
73
+ ];
74
+ render(_jsx(BulkActionBar, { selectedCount: 1, actions: actionsWithFns }));
75
+ // Select Delete via picker
76
+ fireEvent.click(screen.getByRole("button", { expanded: false }));
77
+ fireEvent.click(screen.getByRole("option", { name: /delete/i }));
78
+ // Apply
79
+ fireEvent.click(screen.getByRole("button", { name: /apply/i }));
80
+ expect(deleteFn).toHaveBeenCalledOnce();
81
+ expect(publishFn).not.toHaveBeenCalled();
82
+ });
83
+ it("disables the apply button when the selected action is disabled", () => {
84
+ const disabledActions = [
85
+ { id: "archive", label: "Archive", onClick: vi.fn(), disabled: true },
86
+ ];
87
+ render(_jsx(BulkActionBar, { selectedCount: 1, actions: disabledActions }));
88
+ const applyBtn = screen.getByRole("button", { name: /apply/i });
89
+ expect(applyBtn.disabled).toBe(true);
90
+ });
91
+ it("uses custom labels", () => {
92
+ render(_jsx(BulkActionBar, { selectedCount: 2, actions: actions, labels: { apply: "Run", selected: "chosen" } }));
93
+ expect(screen.getByRole("button", { name: /run/i })).toBeTruthy();
94
+ expect(screen.getByTestId("bulk-selected-count").textContent).toContain("chosen");
95
+ });
96
+ });