@mohasinac/appkit 4.14.0 → 4.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/dist/_internal/server/features/auctions/actions.js +2 -2
  2. package/dist/_internal/server/features/cart/service.d.ts +3 -3
  3. package/dist/_internal/server/features/checkout/actions.js +19 -0
  4. package/dist/_internal/server/features/checkout/source-context.d.ts +5 -0
  5. package/dist/_internal/server/features/checkout/source-context.js +157 -0
  6. package/dist/_internal/server/features/offers/adapters.d.ts +103 -0
  7. package/dist/_internal/server/features/offers/adapters.js +88 -0
  8. package/dist/_internal/server/features/offers/index.d.ts +2 -0
  9. package/dist/_internal/server/features/offers/index.js +1 -0
  10. package/dist/_internal/server/features/orders/adapters.js +24 -0
  11. package/dist/_internal/server/jobs/core/offerExpiry.js +10 -10
  12. package/dist/_internal/shared/actions/action-registry.d.ts +9 -2
  13. package/dist/_internal/shared/actions/action-registry.js +458 -7
  14. package/dist/_internal/shared/features/auctions/schema.d.ts +14 -2
  15. package/dist/_internal/shared/features/auctions/schema.js +13 -1
  16. package/dist/_internal/shared/features/blog/schema.d.ts +4 -4
  17. package/dist/_internal/shared/features/cart/schema.d.ts +25 -20
  18. package/dist/_internal/shared/features/cart/schema.js +15 -0
  19. package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +22 -22
  20. package/dist/_internal/shared/features/orders/schema.d.ts +4 -4
  21. package/dist/_internal/shared/features/products/schema.d.ts +27 -27
  22. package/dist/_internal/shared/features/promotions/schema.d.ts +4 -4
  23. package/dist/_internal/shared/features/reviews/schema.d.ts +2 -2
  24. package/dist/_internal/shared/history/history.d.ts +22 -0
  25. package/dist/_internal/shared/history/history.js +41 -1
  26. package/dist/client.d.ts +13 -2
  27. package/dist/client.js +20 -2
  28. package/dist/constants/field-names.d.ts +8 -6
  29. package/dist/constants/field-names.js +48 -6
  30. package/dist/contracts/extend.d.ts +29 -0
  31. package/dist/contracts/extend.js +20 -0
  32. package/dist/contracts/index.d.ts +2 -1
  33. package/dist/contracts/index.js +2 -0
  34. package/dist/features/account/components/UserBidsView.js +12 -4
  35. package/dist/features/account/components/UserReturnsView.d.ts +3 -0
  36. package/dist/features/account/components/UserReturnsView.js +17 -1
  37. package/dist/features/admin/components/AdminAuditLogView.js +1 -0
  38. package/dist/features/admin/components/AdminBidsView.js +15 -2
  39. package/dist/features/admin/components/AdminBlogEditorView.js +65 -33
  40. package/dist/features/admin/components/AdminFeatureEditorView.js +35 -6
  41. package/dist/features/admin/components/AdminNavEditorView.js +15 -5
  42. package/dist/features/admin/components/AdminNewsletterView.js +18 -2
  43. package/dist/features/admin/components/AdminOffersView.js +62 -18
  44. package/dist/features/admin/components/AdminPaymentMethodsView.js +24 -4
  45. package/dist/features/admin/components/AdminScammerEditorView.d.ts +2 -1
  46. package/dist/features/admin/components/AdminScammerEditorView.js +3 -13
  47. package/dist/features/admin/components/AdminScammersView.js +3 -8
  48. package/dist/features/admin/components/AdminSectionsView.js +32 -23
  49. package/dist/features/admin/components/AdminSessionsView.js +17 -2
  50. package/dist/features/admin/components/AdminViewCards.js +24 -17
  51. package/dist/features/admin/components/DataListingView.js +15 -1
  52. package/dist/features/admin/components/DataTable.js +7 -17
  53. package/dist/features/admin/components/ViewAuditLogEntryModal.js +1 -0
  54. package/dist/features/admin/components/sections/adminSectionsTypes.d.ts +4 -2
  55. package/dist/features/admin/components/sections/adminSectionsTypes.js +45 -27
  56. package/dist/features/admin/constants/filter-tabs.d.ts +3 -0
  57. package/dist/features/admin/constants/filter-tabs.js +1 -0
  58. package/dist/features/admin/schemas/homepage-section-form.d.ts +22 -0
  59. package/dist/features/admin/schemas/homepage-section-form.js +68 -0
  60. package/dist/features/admin/schemas/nav-item-form.d.ts +74 -0
  61. package/dist/features/admin/schemas/nav-item-form.js +72 -0
  62. package/dist/features/auctions/components/AuctionBidsTable.d.ts +10 -1
  63. package/dist/features/auctions/components/AuctionBidsTable.js +22 -4
  64. package/dist/features/auctions/components/bid-detail-fields.d.ts +29 -0
  65. package/dist/features/auctions/components/bid-detail-fields.js +103 -0
  66. package/dist/features/auctions/schemas/index.d.ts +14 -14
  67. package/dist/features/audit-log/schemas/firestore.d.ts +14 -0
  68. package/dist/features/audit-log/schemas/firestore.js +7 -0
  69. package/dist/features/auth/schemas/index.d.ts +2 -2
  70. package/dist/features/blog/actions/blog-actions.d.ts +7 -7
  71. package/dist/features/blog/schemas/blog-form.d.ts +80 -0
  72. package/dist/features/blog/schemas/blog-form.js +100 -0
  73. package/dist/features/blog/schemas/index.d.ts +10 -10
  74. package/dist/features/cart/schemas/index.d.ts +3 -3
  75. package/dist/features/catalogue/schemas/validation.d.ts +12 -12
  76. package/dist/features/categories/schemas/index.d.ts +16 -16
  77. package/dist/features/collections/schemas/index.d.ts +8 -8
  78. package/dist/features/events/components/AdminEventEditorView.js +32 -54
  79. package/dist/features/events/components/AdminEventEntriesView.js +22 -3
  80. package/dist/features/events/schemas/event-form.d.ts +113 -0
  81. package/dist/features/events/schemas/event-form.js +82 -0
  82. package/dist/features/events/schemas/index.d.ts +2 -2
  83. package/dist/features/faq/actions/faq-actions.d.ts +6 -6
  84. package/dist/features/faq/schemas/index.d.ts +6 -6
  85. package/dist/features/grouped/schemas/grouped-listing-form.d.ts +101 -0
  86. package/dist/features/grouped/schemas/grouped-listing-form.js +89 -0
  87. package/dist/features/grouped/schemas/index.d.ts +4 -4
  88. package/dist/features/homepage/actions/homepage-section-actions.d.ts +11 -3
  89. package/dist/features/homepage/actions/homepage-section-actions.js +33 -1
  90. package/dist/features/homepage/schemas/index.d.ts +71 -40
  91. package/dist/features/homepage/schemas/index.js +24 -0
  92. package/dist/features/messages/schemas/index.d.ts +2 -2
  93. package/dist/features/orders/components/OrderStatusTimeline.d.ts +5 -1
  94. package/dist/features/orders/components/OrderStatusTimeline.js +39 -20
  95. package/dist/features/orders/repository/orders.repository.js +5 -0
  96. package/dist/features/orders/schemas/firestore.d.ts +36 -1
  97. package/dist/features/orders/schemas/index.d.ts +24 -24
  98. package/dist/features/orders/types/index.d.ts +35 -7
  99. package/dist/features/pre-orders/schemas/index.d.ts +4 -4
  100. package/dist/features/products/components/GroupSettingsPanel.js +5 -1
  101. package/dist/features/products/schemas/index.d.ts +38 -38
  102. package/dist/features/products/schemas/product-features.validators.d.ts +75 -2
  103. package/dist/features/products/schemas/product-features.validators.js +63 -0
  104. package/dist/features/promotions/schemas/index.d.ts +14 -14
  105. package/dist/features/reviews/schemas/index.d.ts +14 -14
  106. package/dist/features/scams/constants/scammer-status-display.d.ts +28 -0
  107. package/dist/features/scams/constants/scammer-status-display.js +79 -0
  108. package/dist/features/scams/schemas/index.d.ts +2 -2
  109. package/dist/features/search/schemas/index.d.ts +2 -2
  110. package/dist/features/seller/actions/offer-actions.js +32 -2
  111. package/dist/features/seller/components/OfferPhaseTimeline.d.ts +59 -0
  112. package/dist/features/seller/components/OfferPhaseTimeline.js +107 -0
  113. package/dist/features/seller/components/SellerBidsView.js +11 -4
  114. package/dist/features/seller/components/SellerCouponEditorView.js +17 -17
  115. package/dist/features/seller/components/SellerOffersView.js +2 -1
  116. package/dist/features/seller/components/SellerPayoutSettingsView.js +38 -14
  117. package/dist/features/seller/repository/offer.repository.d.ts +69 -8
  118. package/dist/features/seller/repository/offer.repository.js +127 -23
  119. package/dist/features/seller/schemas/coupon-form.d.ts +72 -0
  120. package/dist/features/seller/schemas/coupon-form.js +116 -0
  121. package/dist/features/seller/schemas/firestore.d.ts +56 -2
  122. package/dist/features/seller/schemas/firestore.js +27 -0
  123. package/dist/features/seller/schemas/index.d.ts +2 -2
  124. package/dist/features/seller/schemas/offer-forms.d.ts +21 -0
  125. package/dist/features/seller/schemas/offer-forms.js +21 -0
  126. package/dist/features/shell/build-sections.d.ts +63 -0
  127. package/dist/features/shell/build-sections.js +207 -0
  128. package/dist/features/shell/field-ui-meta.d.ts +115 -0
  129. package/dist/features/shell/field-ui-meta.js +324 -0
  130. package/dist/features/shell/index.d.ts +4 -0
  131. package/dist/features/shell/index.js +2 -0
  132. package/dist/features/shipments/schemas/validation.d.ts +22 -22
  133. package/dist/features/status-history/components/StatusTimeline.d.ts +74 -0
  134. package/dist/features/status-history/components/StatusTimeline.js +46 -0
  135. package/dist/features/status-history/index.d.ts +11 -0
  136. package/dist/features/status-history/index.js +10 -0
  137. package/dist/features/store-extensions/components/ReviewDecisionModal.d.ts +27 -0
  138. package/dist/features/store-extensions/components/ReviewDecisionModal.js +82 -0
  139. package/dist/features/store-extensions/index.d.ts +12 -0
  140. package/dist/features/store-extensions/index.js +12 -0
  141. package/dist/features/store-extensions/schemas/admin-notification-form.d.ts +36 -0
  142. package/dist/features/store-extensions/schemas/admin-notification-form.js +114 -0
  143. package/dist/features/store-extensions/schemas/analytics-forms.d.ts +57 -0
  144. package/dist/features/store-extensions/schemas/analytics-forms.js +158 -0
  145. package/dist/features/store-extensions/schemas/custom-role-form.d.ts +85 -0
  146. package/dist/features/store-extensions/schemas/custom-role-form.js +118 -0
  147. package/dist/features/store-extensions/schemas/firestore.d.ts +21 -1
  148. package/dist/features/store-extensions/schemas/firestore.js +60 -0
  149. package/dist/features/store-extensions/schemas/google-config-form.d.ts +53 -0
  150. package/dist/features/store-extensions/schemas/google-config-form.js +105 -0
  151. package/dist/features/store-extensions/schemas/item-request-create-form.d.ts +28 -0
  152. package/dist/features/store-extensions/schemas/item-request-create-form.js +80 -0
  153. package/dist/features/store-extensions/schemas/listing-template-form.d.ts +78 -0
  154. package/dist/features/store-extensions/schemas/listing-template-form.js +87 -0
  155. package/dist/features/store-extensions/schemas/moderation-review-form.d.ts +49 -0
  156. package/dist/features/store-extensions/schemas/moderation-review-form.js +101 -0
  157. package/dist/features/store-extensions/schemas/payout-method-form.d.ts +153 -0
  158. package/dist/features/store-extensions/schemas/payout-method-form.js +155 -0
  159. package/dist/features/store-extensions/schemas/report-create-form.d.ts +25 -0
  160. package/dist/features/store-extensions/schemas/report-create-form.js +79 -0
  161. package/dist/features/store-extensions/schemas/report-review-form.d.ts +47 -0
  162. package/dist/features/store-extensions/schemas/report-review-form.js +111 -0
  163. package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +148 -0
  164. package/dist/features/store-extensions/schemas/shipping-config-form.js +104 -0
  165. package/dist/features/store-extensions/schemas/store-category-form.d.ts +54 -0
  166. package/dist/features/store-extensions/schemas/store-category-form.js +73 -0
  167. package/dist/features/stores/schemas/index.d.ts +4 -4
  168. package/dist/features/support/schemas/index.d.ts +4 -4
  169. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +350 -0
  170. package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
  171. package/dist/index.d.ts +12 -2
  172. package/dist/index.js +12 -2
  173. package/dist/next/routing/route-map.d.ts +16 -2
  174. package/dist/next/routing/route-map.js +9 -2
  175. package/dist/schemas/firestore-value.d.ts +8 -0
  176. package/dist/schemas/firestore-value.js +44 -0
  177. package/dist/schemas/index.d.ts +1 -1
  178. package/dist/schemas/index.js +1 -1
  179. package/dist/schemas/registry.d.ts +734 -101
  180. package/dist/schemas/registry.js +64 -1
  181. package/dist/seed/bids-seed-data.js +30 -0
  182. package/dist/seed/offers-seed-data.js +212 -7
  183. package/dist/seed/orders-seed-data.js +336 -1
  184. package/dist/seed/products-auctions-seed-data.js +41 -0
  185. package/dist/server.d.ts +2 -0
  186. package/dist/server.js +5 -0
  187. package/dist/styles.css +1 -1
  188. package/dist/tailwind-utilities.css +1 -1
  189. package/dist/ui/DataTable.js +4 -2
  190. package/dist/ui/columns/build-columns.d.ts +6 -1
  191. package/dist/ui/columns/build-columns.js +53 -0
  192. package/dist/ui/columns/cell-renderers.d.ts +68 -0
  193. package/dist/ui/columns/cell-renderers.js +111 -0
  194. package/dist/ui/columns/column-renderers.d.ts +6 -1
  195. package/dist/ui/columns/column-renderers.js +6 -1
  196. package/dist/ui/columns/index.d.ts +2 -1
  197. package/dist/ui/columns/index.js +5 -1
  198. package/dist/ui/components/Button.js +27 -2
  199. package/dist/ui/components/RecordDetailModal.d.ts +10 -1
  200. package/dist/ui/components/RecordDetailModal.js +2 -2
  201. package/dist/ui/icons/icon-registry.d.ts +65 -0
  202. package/dist/ui/icons/icon-registry.js +115 -0
  203. package/package.json +2 -1
  204. package/scripts/node-esm-loader.mjs +65 -0
  205. package/scripts/seed-cli-loader.mjs +6 -49
  206. package/scripts/seed-cli.mjs +1 -1
@@ -6,23 +6,13 @@ import { useQueryClient } from "@tanstack/react-query";
6
6
  import { Button, Code, Div, FormActions, HorizontalRule, Label, Row, Select, SideDrawer, Span, Stack, Text, Textarea, useToast } from "../../../ui";
7
7
  import { apiClient } from "../../../http";
8
8
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
9
+ import { ScammerStatusValues, } from "../../scams/schemas/firestore";
10
+ import { SCAMMER_STATUS_BADGE, SCAMMER_STATUS_LABEL, SCAMMER_STATUS_OPTIONS, } from "../../scams/constants/scammer-status-display";
9
11
  const __P = {
10
12
  p3: "p-[var(--appkit-space-3)]",
11
13
  p4: "p-[var(--appkit-space-4)]",
12
14
  };
13
15
  const CLS_SECTION_LABEL = "text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] uppercase tracking-wide";
14
- const STATUS_OPTIONS = [
15
- { label: "Pending review", value: "pending_review" },
16
- { label: "Verified", value: "verified" },
17
- { label: "Rejected", value: "rejected" },
18
- { label: "Removed", value: "removed" },
19
- ];
20
- const STATUS_COLOR = {
21
- pending_review: "bg-warning-surface text-warning",
22
- verified: "bg-success-surface text-success",
23
- rejected: "bg-error-surface text-error",
24
- removed: "bg-[var(--appkit-color-surface)] text-[var(--appkit-color-text-muted)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]",
25
- };
26
16
  export function AdminScammerEditorView({ open, onClose, scammerId, displayNames = [], scamType, description, phones = [], upiIds = [], currentStatus, verificationNote, reportedBy, reportedByAnon, }) {
27
17
  const queryClient = useQueryClient();
28
18
  const { showToast } = useToast();
@@ -63,5 +53,5 @@ export function AdminScammerEditorView({ open, onClose, scammerId, displayNames
63
53
  showToast(err?.message ?? "Failed to delete profile.", "error");
64
54
  },
65
55
  });
66
- return (_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: displayNames.length > 0 ? displayNames[0] : "Scammer Profile", children: _jsxs(Stack, { className: `${__P.p4}`, gap: "md", children: [_jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Span, { size: "xs", weight: "medium", className: `inline-flex ${STATUS_COLOR[currentStatus ?? "pending_review"] ?? STATUS_COLOR.pending_review}`, rounded: "full", padding: "pill-sm-tall", children: (currentStatus ?? "pending_review").replace(/_/g, " ") }), scamType && (_jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: scamType.replace(/_/g, " ") }))] }), displayNames.length > 0 && (_jsxs(Div, { children: [_jsx(Text, { className: "mb-1 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Names / Aliases" }), _jsx(Row, { wrap: true, gap: "xs", children: displayNames.map((name, i) => (_jsx(Span, { size: "xs", rounded: "full", surface: "subtle", padding: "pill-sm", children: name }, i))) })] })), (phones.length > 0 || upiIds.length > 0) && (_jsxs(Div, { className: `${__P.p3}`, rounded: "lg", surface: "muted", border: "default", children: [phones.length > 0 && (_jsxs(Div, { className: "mb-2", children: [_jsx(Text, { className: CLS_SECTION_LABEL, children: "Phone numbers" }), _jsx(Row, { wrap: true, gap: "xs", children: phones.map((p, i) => (_jsx(Code, { size: "xs", rounded: "default", padding: "sm", surface: "subtle", children: p }, i))) })] })), upiIds.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { className: CLS_SECTION_LABEL, children: "UPI IDs" }), _jsx(Row, { wrap: true, gap: "xs", children: upiIds.map((u, i) => (_jsx(Code, { size: "xs", rounded: "default", padding: "sm", surface: "subtle", children: u }, i))) })] }))] })), description && (_jsxs(Div, { className: `${__P.p3}`, rounded: "lg", surface: "muted", border: "default", children: [_jsx(Text, { className: CLS_SECTION_LABEL, children: "Description" }), _jsx(Text, { className: "whitespace-pre-wrap", color: "primary", size: "sm", children: description })] })), _jsxs(Div, { textSize: "xs", color: "muted", children: ["Reported by:", " ", _jsx(Span, { weight: "medium", color: "muted", children: reportedByAnon ? "Anonymous" : (reportedBy ?? "Unknown") })] }), _jsx(HorizontalRule, { tone: "subtle", spacing: "comfortable" }), _jsx(Select, { label: "Status", options: STATUS_OPTIONS, value: status, onValueChange: setStatus }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { className: "uppercase tracking-wide", color: "muted", size: "xs", weight: "semibold", children: "Verification note (internal)" }), _jsx(Textarea, { value: note, onChange: (e) => setNote(e.target.value), rows: 3, placeholder: "e.g. Verified via 3 independent reports\u2026" })] }), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "danger", size: "sm", disabled: !scammerId, isLoading: deleteMutation.isPending, onClick: () => deleteMutation.mutate(), children: "Delete" }), _jsx(Button, { type: "button", variant: "secondary", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "button", isLoading: updateMutation.isPending, disabled: !scammerId || updateMutation.isPending, onClick: () => updateMutation.mutate(), children: "Save changes" })] })] }) }));
56
+ return (_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: displayNames.length > 0 ? displayNames[0] : "Scammer Profile", children: _jsxs(Stack, { className: `${__P.p4}`, gap: "md", children: [_jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Span, { size: "xs", weight: "medium", className: `inline-flex ${SCAMMER_STATUS_BADGE[currentStatus ?? ScammerStatusValues.PENDING_REVIEW]}`, rounded: "full", padding: "pill-sm-tall", children: SCAMMER_STATUS_LABEL[currentStatus ?? ScammerStatusValues.PENDING_REVIEW] }), scamType && (_jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: scamType.replace(/_/g, " ") }))] }), displayNames.length > 0 && (_jsxs(Div, { children: [_jsx(Text, { className: "mb-1 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Names / Aliases" }), _jsx(Row, { wrap: true, gap: "xs", children: displayNames.map((name, i) => (_jsx(Span, { size: "xs", rounded: "full", surface: "subtle", padding: "pill-sm", children: name }, i))) })] })), (phones.length > 0 || upiIds.length > 0) && (_jsxs(Div, { className: `${__P.p3}`, rounded: "lg", surface: "muted", border: "default", children: [phones.length > 0 && (_jsxs(Div, { className: "mb-2", children: [_jsx(Text, { className: CLS_SECTION_LABEL, children: "Phone numbers" }), _jsx(Row, { wrap: true, gap: "xs", children: phones.map((p, i) => (_jsx(Code, { size: "xs", rounded: "default", padding: "sm", surface: "subtle", children: p }, i))) })] })), upiIds.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { className: CLS_SECTION_LABEL, children: "UPI IDs" }), _jsx(Row, { wrap: true, gap: "xs", children: upiIds.map((u, i) => (_jsx(Code, { size: "xs", rounded: "default", padding: "sm", surface: "subtle", children: u }, i))) })] }))] })), description && (_jsxs(Div, { className: `${__P.p3}`, rounded: "lg", surface: "muted", border: "default", children: [_jsx(Text, { className: CLS_SECTION_LABEL, children: "Description" }), _jsx(Text, { className: "whitespace-pre-wrap", color: "primary", size: "sm", children: description })] })), _jsxs(Div, { textSize: "xs", color: "muted", children: ["Reported by:", " ", _jsx(Span, { weight: "medium", color: "muted", children: reportedByAnon ? "Anonymous" : (reportedBy ?? "Unknown") })] }), _jsx(HorizontalRule, { tone: "subtle", spacing: "comfortable" }), _jsx(Select, { label: "Status", options: SCAMMER_STATUS_OPTIONS, value: status, onValueChange: setStatus }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { className: "uppercase tracking-wide", color: "muted", size: "xs", weight: "semibold", children: "Verification note (internal)" }), _jsx(Textarea, { value: note, onChange: (e) => setNote(e.target.value), rows: 3, placeholder: "e.g. Verified via 3 independent reports\u2026" })] }), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "danger", size: "sm", disabled: !scammerId, isLoading: deleteMutation.isPending, onClick: () => deleteMutation.mutate(), children: "Delete" }), _jsx(Button, { type: "button", variant: "secondary", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "button", isLoading: updateMutation.isPending, disabled: !scammerId || updateMutation.isPending, onClick: () => updateMutation.mutate(), children: "Save changes" })] })] }) }));
67
57
  }
@@ -12,12 +12,7 @@ import { ADMIN_SCAMMER_STATUS_TABS } from "../constants/filter-tabs";
12
12
  import { toRecordArray, toRelativeDate, toStringValue, } from "../hooks/useAdminListingData";
13
13
  import { DataListingView } from "./DataListingView";
14
14
  import { AdminScammerEditorView } from "./AdminScammerEditorView";
15
- const STATUS_BADGE = {
16
- pending_review: "bg-warning-surface text-warning",
17
- verified: "bg-success-surface text-success",
18
- rejected: "bg-error-surface text-error",
19
- removed: "bg-[var(--appkit-color-surface)] text-[var(--appkit-color-text-muted)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]",
20
- };
15
+ import { SCAMMER_STATUS_BADGE, SCAMMER_STATUS_LABEL, toScammerStatus, } from "../../scams/constants/scammer-status-display";
21
16
  const SCAMMER_COLUMNS = [
22
17
  {
23
18
  key: "primary",
@@ -28,7 +23,7 @@ const SCAMMER_COLUMNS = [
28
23
  key: "status",
29
24
  header: "Status",
30
25
  className: "w-36",
31
- render: (row) => (_jsx(Span, { className: `inline-flex ${STATUS_BADGE[row.status] ?? STATUS_BADGE.pending_review}`, size: "xs", weight: "medium", rounded: "full", padding: "pill-sm-tall", children: row.status.replace(/_/g, " ") })),
26
+ render: (row) => (_jsx(Span, { className: `inline-flex ${SCAMMER_STATUS_BADGE[row.status]}`, size: "xs", weight: "medium", rounded: "full", padding: "pill-sm-tall", children: SCAMMER_STATUS_LABEL[row.status] })),
32
27
  },
33
28
  {
34
29
  key: "updatedAt",
@@ -72,7 +67,7 @@ export function AdminScammersView({ children, ...props }) {
72
67
  ]
73
68
  .filter(Boolean)
74
69
  .join(" · "),
75
- status: toStringValue(item.status, "pending_review"),
70
+ status: toScammerStatus(item.status),
76
71
  updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
77
72
  _raw: item,
78
73
  })),
@@ -7,6 +7,8 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
7
7
  import { ProductInlineSelect } from "../../seller/components/ProductInlineSelect";
8
8
  import { CategoryInlineSelect } from "../../seller/components/CategoryInlineSelect";
9
9
  import { Button, Checkbox, Div, Form, FormActions, Input, Modal, Row, Select, Stack, Text, Textarea, useToast } from "../../../ui";
10
+ import { FormErrorSummary, applyZodIssues } from "../../../ui/forms";
11
+ import { homepageSectionFormSchema } from "../schemas/homepage-section-form";
10
12
  import { apiClient } from "../../../http";
11
13
  import { ADMIN_ENDPOINTS } from "../../../constants";
12
14
  import { ROUTES } from "../../../next/routing/route-map";
@@ -954,27 +956,34 @@ export function AdminSectionsView({ children }) {
954
956
  }));
955
957
  const hasReorderChanges = reorderDraft.some((item) => sectionOrderMap.get(item.id) !== item.order);
956
958
  const canUndoReorderChanges = reorderUndoStack.length > 0;
957
- return (_jsxs(_Fragment, { children: [_jsxs(Div, { paddingX: "x-sm-md", padding: "y-md", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "3", children: [_jsxs(Div, { children: [_jsx(Text, { size: "base", weight: "semibold", color: "primary", children: "Homepage Sections" }), _jsx(Text, { size: "sm", color: "muted", children: "Manage homepage sections and their display order" })] }), _jsx(Row, { align: "center", gap: "sm", children: _jsx(Button, { type: "button", variant: "primary", size: "sm", onClick: () => setIsModalOpen(true), children: "Manage Sections" }) })] }), errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), _jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No sections found" })] }), _jsxs(Stack, { className: `mt-4 ${__P.p4}`, gap: "3", rounded: "xl", surface: "default", border: "default", children: [_jsxs(Row, { align: "center", justify: "between", gap: "3", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: "Reorder Sections" }), _jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: reindexDraft, disabled: reorderSections.isPending || reorderDraft.length === 0, children: "Reindex 1..N" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: undoReorderChange, disabled: reorderSections.isPending || !canUndoReorderChanges, children: "Undo unsaved" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: resetToServerOrder, disabled: reorderSections.isPending || !hasReorderChanges, children: "Reset to server" }), _jsx(Button, { type: "button", variant: "primary", size: "sm", onClick: () => reorderSections.mutate(), disabled: !hasReorderChanges || reorderSections.isPending, children: reorderSections.isPending ? "Saving order..." : "Save order" })] })] }), reorderDraft.length === 0 ? (_jsx(Text, { size: "sm", color: "muted", children: "No sections to reorder." })) : (_jsx(Stack, { gap: "sm", children: reorderDraft.map((item, index) => (_jsxs(Div, { layout: "grid", gap: "2", align: "center", className: "grid-cols-[auto_1fr_auto_auto_auto]", rounded: "md", padding: "xs", border: "default", draggable: true, onDragStart: () => setDragIndex(index), onDragOver: (event) => event.preventDefault(), onDrop: () => handleReorderDrop(index), onDragEnd: () => setDragIndex(null), children: [_jsx(Text, { size: "sm", weight: "semibold", color: "muted", children: "\u2261" }), _jsxs(Text, { size: "sm", color: "primary", children: [item.type, " #", item.order] }), _jsx(Input, { type: "number", min: 1, max: reorderDraft.length, value: String(item.order), onChange: (event) => updateReorderItemOrder(index, Number(event.target.value) || 1), className: "w-24" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => moveReorderItem(index, -1), disabled: index === 0, children: "Up" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => moveReorderItem(index, 1), disabled: index === reorderDraft.length - 1, children: "Down" })] }, `reorder-${item.id}`))) }))] }), _jsx(Modal, { isOpen: isModalOpen, onClose: () => setIsModalOpen(false), title: "Manage Homepage Section", size: "lg", children: _jsxs(Form, { onSubmit: (event) => {
958
- event.preventDefault();
959
- saveSection.mutate();
960
- }, spacing: "md", children: [_jsx(Select, { label: "Mode", value: mode, onValueChange: (value) => setMode(value), options: [
961
- { label: "Create new section", value: "create" },
962
- { label: "Edit existing section", value: "edit" },
963
- ] }), mode === "edit" ? (_jsx(Select, { label: "Section", value: selectedSectionId, onValueChange: setSelectedSectionId, placeholder: "Select section", options: sections.map((section) => ({
964
- label: `${section.type} (#${section.order})`,
965
- value: section.id,
966
- })) })) : null, _jsx(Select, { label: "Section type", value: sectionType, onValueChange: (value) => {
967
- setSectionType(value);
968
- if (mode === "create") {
969
- setSelectedSectionId("");
970
- }
971
- }, options: SECTION_TYPE_OPTIONS.map((type) => ({
972
- label: type,
973
- value: type,
974
- })), disabled: mode === "edit" }), _jsx(Input, { label: "Order", type: "number", value: order, onChange: (event) => setOrder(event.target.value), placeholder: "Leave empty to auto-place" }), _jsx(Select, { label: "Enabled", value: enabled ? "true" : "false", onValueChange: (value) => setEnabled(value === "true"), options: [
975
- { label: "Enabled", value: "true" },
976
- { label: "Disabled", value: "false" },
977
- ] }), isTypedBuilder ? renderTypedBuilder() : null, _jsx(Textarea, { label: isTypedBuilder ? "Generated config (JSON preview)" : "Section config (JSON)", value: configJson, onChange: (event) => setConfigJson(event.target.value), rows: 10, readOnly: isTypedBuilder, helperText: isTypedBuilder
978
- ? "This JSON is generated from typed controls above."
979
- : "Provide section config JSON for this section type." }), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => setIsModalOpen(false), children: "Cancel" }), _jsx(Button, { type: "submit", variant: "primary", disabled: saveSection.isPending, children: saveSection.isPending ? "Saving..." : mode === "create" ? "Create section" : "Update section" })] })] }) })] }));
959
+ return (_jsxs(_Fragment, { children: [_jsxs(Div, { paddingX: "x-sm-md", padding: "y-md", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "3", children: [_jsxs(Div, { children: [_jsx(Text, { size: "base", weight: "semibold", color: "primary", children: "Homepage Sections" }), _jsx(Text, { size: "sm", color: "muted", children: "Manage homepage sections and their display order" })] }), _jsx(Row, { align: "center", gap: "sm", children: _jsx(Button, { type: "button", variant: "primary", size: "sm", onClick: () => setIsModalOpen(true), children: "Manage Sections" }) })] }), errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), _jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No sections found" })] }), _jsxs(Stack, { className: `mt-4 ${__P.p4}`, gap: "3", rounded: "xl", surface: "default", border: "default", children: [_jsxs(Row, { align: "center", justify: "between", gap: "3", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: "Reorder Sections" }), _jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: reindexDraft, disabled: reorderSections.isPending || reorderDraft.length === 0, children: "Reindex 1..N" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: undoReorderChange, disabled: reorderSections.isPending || !canUndoReorderChanges, children: "Undo unsaved" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: resetToServerOrder, disabled: reorderSections.isPending || !hasReorderChanges, children: "Reset to server" }), _jsx(Button, { type: "button", variant: "primary", size: "sm", onClick: () => reorderSections.mutate(), disabled: !hasReorderChanges || reorderSections.isPending, children: reorderSections.isPending ? "Saving order..." : "Save order" })] })] }), reorderDraft.length === 0 ? (_jsx(Text, { size: "sm", color: "muted", children: "No sections to reorder." })) : (_jsx(Stack, { gap: "sm", children: reorderDraft.map((item, index) => (_jsxs(Div, { layout: "grid", gap: "2", align: "center", className: "grid-cols-[auto_1fr_auto_auto_auto]", rounded: "md", padding: "xs", border: "default", draggable: true, onDragStart: () => setDragIndex(index), onDragOver: (event) => event.preventDefault(), onDrop: () => handleReorderDrop(index), onDragEnd: () => setDragIndex(null), children: [_jsx(Text, { size: "sm", weight: "semibold", color: "muted", children: "\u2261" }), _jsxs(Text, { size: "sm", color: "primary", children: [item.type, " #", item.order] }), _jsx(Input, { type: "number", min: 1, max: reorderDraft.length, value: String(item.order), onChange: (event) => updateReorderItemOrder(index, Number(event.target.value) || 1), className: "w-24" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => moveReorderItem(index, -1), disabled: index === 0, children: "Up" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => moveReorderItem(index, 1), disabled: index === reorderDraft.length - 1, children: "Down" })] }, `reorder-${item.id}`))) }))] }), _jsx(Modal, { isOpen: isModalOpen, onClose: () => setIsModalOpen(false), title: "Manage Homepage Section", size: "lg", children: _jsx(Form, { schema: homepageSectionFormSchema, onSubmit: (event) => event.preventDefault(), spacing: "md", children: ({ setFieldError, clearErrors }) => (_jsxs(_Fragment, { children: [_jsx(FormErrorSummary, {}), _jsx(Select, { label: "Mode", value: mode, onValueChange: (value) => setMode(value), options: [
960
+ { label: "Create new section", value: "create" },
961
+ { label: "Edit existing section", value: "edit" },
962
+ ] }), mode === "edit" ? (_jsx(Select, { label: "Section", value: selectedSectionId, onValueChange: setSelectedSectionId, placeholder: "Select section", options: sections.map((section) => ({
963
+ label: `${section.type} (#${section.order})`,
964
+ value: section.id,
965
+ })) })) : null, _jsx(Select, { label: "Section type", value: sectionType, onValueChange: (value) => {
966
+ setSectionType(value);
967
+ if (mode === "create") {
968
+ setSelectedSectionId("");
969
+ }
970
+ }, options: SECTION_TYPE_OPTIONS.map((type) => ({
971
+ label: type,
972
+ value: type,
973
+ })), disabled: mode === "edit" }), _jsx(Input, { label: "Order", type: "number", value: order, onChange: (event) => setOrder(event.target.value), placeholder: "Leave empty to auto-place" }), _jsx(Select, { label: "Enabled", value: enabled ? "true" : "false", onValueChange: (value) => setEnabled(value === "true"), options: [
974
+ { label: "Enabled", value: "true" },
975
+ { label: "Disabled", value: "false" },
976
+ ] }), isTypedBuilder ? renderTypedBuilder() : null, _jsx(Textarea, { label: isTypedBuilder ? "Generated config (JSON preview)" : "Section config (JSON)", value: configJson, onChange: (event) => setConfigJson(event.target.value), rows: 10, readOnly: isTypedBuilder, helperText: isTypedBuilder
977
+ ? "This JSON is generated from typed controls above."
978
+ : "Provide section config JSON for this section type." }), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => setIsModalOpen(false), children: "Cancel" }), _jsx(Button, { type: "submit", variant: "primary", disabled: saveSection.isPending, onClick: () => {
979
+ clearErrors();
980
+ const parsed = homepageSectionFormSchema.safeParse({
981
+ sectionType, order, enabled, configJson,
982
+ });
983
+ if (!parsed.success) {
984
+ applyZodIssues(parsed.error.issues, setFieldError);
985
+ return;
986
+ }
987
+ saveSection.mutate();
988
+ }, children: saveSection.isPending ? "Saving..." : mode === "create" ? "Create section" : "Update section" })] })] })) }) })] }));
980
989
  }
@@ -5,7 +5,7 @@ import { SIEVE_OP, sieveFilter } from "@mohasinac/appkit/client";
5
5
  import { sortBy } from "@mohasinac/appkit/client";
6
6
  import React, { useState } from "react";
7
7
  import { useQueryClient } from "@tanstack/react-query";
8
- import { ConfirmDeleteModal, FilterChipGroup, ListingLayout, RowActionMenu, useToast } from "../../../ui";
8
+ import { ConfirmDeleteModal, FilterChipGroup, ListingLayout, RowActionMenu, useToast, RecordDetailModal, } from "../../../ui";
9
9
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
10
10
  import { ADMIN_BULK_ACTIONS, ROW_ACTION_META, ROW_ACTION_ID } from "../../products/constants/action-defs";
11
11
  import { toRecordArray, toRelativeDate, toStringValue, } from "../hooks/useAdminListingData";
@@ -20,6 +20,7 @@ function maskIp(ip) {
20
20
  return ip.replace(/:\w+$/, ":*");
21
21
  }
22
22
  export function AdminSessionsView({ children, ...props }) {
23
+ const [detail, setDetail] = useState(null);
23
24
  const queryClient = useQueryClient();
24
25
  const { showToast } = useToast();
25
26
  const [revokeTarget, setRevokeTarget] = useState(null);
@@ -53,6 +54,9 @@ export function AdminSessionsView({ children, ...props }) {
53
54
  { value: "lastActivity", label: "Least recent" },
54
55
  { value: sortBy("createdAt", "DESC"), label: "Newest" },
55
56
  ],
57
+ // Opening a row is what makes the destructive actions beside it
58
+ // safe to offer at all.
59
+ onRowClick: (row) => setDetail(row),
56
60
  mapRows: (response) => toRecordArray(response.sessions).map((item, index) => {
57
61
  const deviceInfo = (item.deviceInfo ?? {});
58
62
  const userLabel = toStringValue(item.user &&
@@ -60,6 +64,7 @@ export function AdminSessionsView({ children, ...props }) {
60
64
  const ipMasked = maskIp(toStringValue(deviceInfo.ip, ""));
61
65
  return {
62
66
  id: toStringValue(item.id, `session-${index}`),
67
+ detail: item,
63
68
  primary: userLabel,
64
69
  secondary: [
65
70
  toStringValue(deviceInfo.browser, "Unknown browser"),
@@ -94,7 +99,17 @@ export function AdminSessionsView({ children, ...props }) {
94
99
  { id: "true", label: "Active only" },
95
100
  ], value: pendingFilters.isActive || "", onChange: (v) => setPendingFilters((p) => ({ ...p, isActive: v })) })),
96
101
  };
97
- return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(ConfirmDeleteModal, { isOpen: Boolean(revokeTarget), onClose: () => setRevokeTarget(null), onConfirm: () => {
102
+ return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(RecordDetailModal, { isOpen: detail !== null, onClose: () => setDetail(null), title: toStringValue(detail?.detail?.userId, "Session"), fields: (() => {
103
+ const d = (detail?.detail ?? {});
104
+ return detail ? [
105
+ { label: "User", value: toStringValue(d.userId, "—") },
106
+ { label: "Active", value: d.isActive ? "Yes" : "No" },
107
+ { label: "Last activity", value: toRelativeDate(d.lastActivity) },
108
+ { label: "Expires", value: toRelativeDate(d.expiresAt) },
109
+ { label: "Started", value: toRelativeDate(d.createdAt) },
110
+ { label: "Session ID", value: detail?.id ?? "—" },
111
+ ] : undefined;
112
+ })() }), _jsx(ConfirmDeleteModal, { isOpen: Boolean(revokeTarget), onClose: () => setRevokeTarget(null), onConfirm: () => {
98
113
  if (revokeTarget)
99
114
  revokeMutation.mutate(revokeTarget.id);
100
115
  }, isDeleting: revokeMutation.isPending, confirmText: "Revoke", title: "Revoke session?", message: "This will sign the user out of this device immediately.", variant: "danger" })] }));
@@ -1,7 +1,7 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Checkbox, Div, Grid, Row, Span, Stack, Text } from "../../../ui";
4
- import { getStatusTone } from "../../../ui/columns/column-renderers";
4
+ import { STATUS_TONE_CLASSES, renderStatusBadge } from "../../../ui/columns/cell-renderers";
5
5
  import { MediaImage } from "../../media/MediaImage";
6
6
  const __P = {
7
7
  p3: "p-[var(--appkit-space-3)]",
@@ -9,26 +9,33 @@ const __P = {
9
9
  const __O = {
10
10
  hidden: "overflow-hidden",
11
11
  };
12
- const STATUS_TONE_CLASSES = {
13
- success: "bg-success-surface text-success",
14
- warning: "bg-warning-surface text-warning",
15
- error: "bg-error-surface text-error",
16
- info: "bg-info-surface text-info",
17
- neutral: "bg-primary-50 text-primary-800 dark:bg-secondary-900/30 dark:text-secondary-300",
18
- };
19
12
  function RowAvatar({ image, alt, icon, size = "9" }) {
20
13
  const dims = size === "12" ? "h-12 w-12" : "h-9 w-9";
21
14
  return (_jsx(Div, { className: `relative ${dims} shrink-0`, overflow: "hidden", rounded: "full", children: _jsx(MediaImage, { src: image, alt: alt, size: "avatar", fallback: icon }) }));
22
15
  }
16
+ /*
17
+ * Flag chips, all drawn from the shared tone map.
18
+ *
19
+ * Three defects lived in the previous literal list:
20
+ * - `isSold` carried TWO backgrounds and TWO inks on one element
21
+ * (`bg-…surface … bg-…surface-elevated …`). Tailwind emits all four and
22
+ * stylesheet order — not string order — decides the winner, so the painted
23
+ * colour was not what the list said. The identical bug was found and fixed
24
+ * on the scammer status badge the same week.
25
+ * - `isPromoted` was raw dual-theme purple (`bg-purple-100 … dark:bg-purple-900/30`),
26
+ * the hand-written theme branching that variants exist to absorb.
27
+ * - `featured` repeated its light classes under `dark:`, which is a no-op —
28
+ * `-surface` tokens already invert with the theme.
29
+ */
23
30
  const FLAG_BADGES = [
24
- { key: "featured", label: "Featured", color: "bg-warning-surface text-warning dark:bg-warning-surface dark:text-warning" },
25
- { key: "isPromoted", label: "Promoted", color: "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300" },
26
- { key: "isOnSale", label: "Sale", color: "bg-success-surface text-success" },
27
- { key: "isSold", label: "Sold", color: "bg-[var(--appkit-color-surface)] text-[var(--appkit-color-text-muted)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]" },
31
+ { key: "featured", label: "Featured", tone: "warning" },
32
+ { key: "isPromoted", label: "Promoted", tone: "info" },
33
+ { key: "isOnSale", label: "Sale", tone: "success" },
34
+ // Sold is the absence of availability, not a status outcome — neutral.
35
+ { key: "isSold", label: "Sold", tone: "neutral" },
28
36
  ];
29
37
  function StatusBadge({ status }) {
30
- const tone = getStatusTone(status);
31
- return (_jsx(Span, { size: "xs", weight: "medium", className: `inline-flex truncate max-w-[120px] ${STATUS_TONE_CLASSES[tone]}`, rounded: "full", padding: "pill-xs", children: status }));
38
+ return _jsx(_Fragment, { children: renderStatusBadge(status) });
32
39
  }
33
40
  function SkeletonCard({ view }) {
34
41
  if (view === "list") {
@@ -47,14 +54,14 @@ function AdminCardItem({ row, view, selected, onToggleSelect, onRowClick, render
47
54
  return (_jsxs(Row, { gap: "sm", className: [
48
55
  "px-[var(--appkit-space-4)] py-[var(--appkit-space-3)] cursor-pointer transition-colors hover:bg-zinc-50 hover:bg-[var(--appkit-color-surface-elevated)]/50",
49
56
  selected ? "bg-primary-50/40 dark:bg-primary-900/10" : "",
50
- ].filter(Boolean).join(" "), onClick: handleClick, role: onRowClick ? "button" : undefined, children: [onToggleSelect && (_jsx(Div, { "data-no-row-click": true, className: "shrink-0", onClick: (e) => { e.stopPropagation(); onToggleSelect(row.id); }, children: _jsx(Checkbox, { bare: true, checked: selected, onChange: () => onToggleSelect(row.id), className: "h-4 w-4 rounded border-zinc-300 text-primary accent-primary cursor-pointer", "aria-label": `Select ${row.primary}` }) })), _jsx(RowAvatar, { image: row.image, alt: row.primary, icon: resourceIcon }), _jsxs(Stack, { gap: "none", className: "flex-1 min-w-0", children: [_jsx(Text, { size: "sm", weight: "semibold", className: "truncate", color: "primary", children: row.primary }), _jsx(Text, { size: "xs", color: "muted", className: "truncate", children: row.secondary }), row.barcodeId && (_jsx(Text, { size: "xs", color: "faint", className: "truncate font-mono", children: row.barcodeId }))] }), flags.length > 0 && (_jsx(Row, { gap: "xs", className: "hidden sm:flex shrink-0", children: flags.map(({ key, label, color }) => (_jsx(Span, { padding: "pill-2xs", weight: "medium", className: `inline-flex text-[10px] ${color}`, rounded: "full", children: label }, key))) })), _jsx(StatusBadge, { status: row.status }), _jsx(Span, { size: "xs", color: "muted", className: "hidden sm:block shrink-0 w-24", align: "end", children: row.updatedAt }), renderRowActions && (_jsx(Div, { "data-no-row-click": true, className: "shrink-0", onClick: (e) => e.stopPropagation(), children: renderRowActions(row) }))] }));
57
+ ].filter(Boolean).join(" "), onClick: handleClick, role: onRowClick ? "button" : undefined, children: [onToggleSelect && (_jsx(Div, { "data-no-row-click": true, className: "shrink-0", onClick: (e) => { e.stopPropagation(); onToggleSelect(row.id); }, children: _jsx(Checkbox, { bare: true, checked: selected, onChange: () => onToggleSelect(row.id), className: "h-4 w-4 rounded border-zinc-300 text-primary accent-primary cursor-pointer", "aria-label": `Select ${row.primary}` }) })), _jsx(RowAvatar, { image: row.image, alt: row.primary, icon: resourceIcon }), _jsxs(Stack, { gap: "none", className: "flex-1 min-w-0", children: [_jsx(Text, { size: "sm", weight: "semibold", className: "truncate", color: "primary", children: row.primary }), _jsx(Text, { size: "xs", color: "muted", className: "truncate", children: row.secondary }), row.barcodeId && (_jsx(Text, { size: "xs", color: "faint", className: "truncate font-mono", children: row.barcodeId }))] }), flags.length > 0 && (_jsx(Row, { gap: "xs", className: "hidden sm:flex shrink-0", children: flags.map(({ key, label, tone }) => (_jsx(Span, { padding: "pill-2xs", weight: "medium", className: `inline-flex text-[10px] ${STATUS_TONE_CLASSES[tone]}`, rounded: "full", children: label }, key))) })), _jsx(StatusBadge, { status: row.status }), _jsx(Span, { size: "xs", color: "muted", className: "hidden sm:block shrink-0 w-24", align: "end", children: row.updatedAt }), renderRowActions && (_jsx(Div, { "data-no-row-click": true, className: "shrink-0", onClick: (e) => e.stopPropagation(), children: renderRowActions(row) }))] }));
51
58
  }
52
59
  return (_jsxs(Div, { rounded: "xl", className: [
53
60
  "border overflow-hidden cursor-pointer transition-all hover:shadow-md hover:-translate-y-0.5",
54
61
  selected
55
62
  ? "border-primary ring-1 ring-primary/20 bg-primary-50/30 dark:bg-primary-900/10"
56
63
  : "border-zinc-100 border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]",
57
- ].filter(Boolean).join(" "), onClick: handleClick, role: onRowClick ? "button" : undefined, children: [onToggleSelect && (_jsxs(Row, { gap: "xs", "data-no-row-click": true, paddingY: "t-sm", padding: "x-sm", onClick: (e) => { e.stopPropagation(); onToggleSelect(row.id); }, children: [_jsx(Checkbox, { bare: true, checked: selected, onChange: () => onToggleSelect(row.id), className: "h-4 w-4 rounded border-zinc-300 text-primary accent-primary cursor-pointer", "aria-label": `Select ${row.primary}` }), flags.length > 0 && (_jsx(Row, { gap: "xs", wrap: true, children: flags.map(({ key, label, color }) => (_jsx(Span, { padding: "pill-2xs", weight: "medium", className: `inline-flex text-[10px] ${color}`, rounded: "full", children: label }, key))) }))] })), _jsxs(Stack, { gap: "xs", className: `${__P.p3}.5`, children: [_jsxs(Row, { gap: "sm", align: "center", children: [_jsx(RowAvatar, { image: row.image, alt: row.primary, icon: resourceIcon, size: "12" }), _jsxs(Stack, { gap: "none", className: "min-w-0 flex-1", children: [_jsx(Text, { size: "sm", weight: "semibold", className: "line-clamp-2 leading-snug", color: "primary", children: row.primary }), _jsx(Text, { size: "xs", color: "muted", className: "truncate", children: row.secondary })] })] }), row.barcodeId && (_jsx(Text, { size: "xs", color: "faint", className: "truncate font-mono", children: row.barcodeId })), _jsxs(Row, { justify: "between", gap: "xs", children: [_jsx(StatusBadge, { status: row.status }), _jsx(Span, { color: "muted", className: "text-[11px] shrink-0", children: row.updatedAt })] }), renderRowActions && (_jsx(Row, { "data-no-row-click": true, justify: "end", gap: "xs", className: "border-t border-[var(--appkit-color-border)] -mx-[var(--appkit-space-3)] -mb-[var(--appkit-space-3)] mt-[var(--appkit-space-1)] px-[var(--appkit-space-3)] py-[var(--appkit-space-2)]", onClick: (e) => e.stopPropagation(), children: renderRowActions(row) }))] })] }));
64
+ ].filter(Boolean).join(" "), onClick: handleClick, role: onRowClick ? "button" : undefined, children: [onToggleSelect && (_jsxs(Row, { gap: "xs", "data-no-row-click": true, paddingY: "t-sm", padding: "x-sm", onClick: (e) => { e.stopPropagation(); onToggleSelect(row.id); }, children: [_jsx(Checkbox, { bare: true, checked: selected, onChange: () => onToggleSelect(row.id), className: "h-4 w-4 rounded border-zinc-300 text-primary accent-primary cursor-pointer", "aria-label": `Select ${row.primary}` }), flags.length > 0 && (_jsx(Row, { gap: "xs", wrap: true, children: flags.map(({ key, label, tone }) => (_jsx(Span, { padding: "pill-2xs", weight: "medium", className: `inline-flex text-[10px] ${STATUS_TONE_CLASSES[tone]}`, rounded: "full", children: label }, key))) }))] })), _jsxs(Stack, { gap: "xs", className: `${__P.p3}.5`, children: [_jsxs(Row, { gap: "sm", align: "center", children: [_jsx(RowAvatar, { image: row.image, alt: row.primary, icon: resourceIcon, size: "12" }), _jsxs(Stack, { gap: "none", className: "min-w-0 flex-1", children: [_jsx(Text, { size: "sm", weight: "semibold", className: "line-clamp-2 leading-snug", color: "primary", children: row.primary }), _jsx(Text, { size: "xs", color: "muted", className: "truncate", children: row.secondary })] })] }), row.barcodeId && (_jsx(Text, { size: "xs", color: "faint", className: "truncate font-mono", children: row.barcodeId })), _jsxs(Row, { justify: "between", gap: "xs", children: [_jsx(StatusBadge, { status: row.status }), _jsx(Span, { color: "muted", className: "text-[11px] shrink-0", children: row.updatedAt })] }), renderRowActions && (_jsx(Row, { "data-no-row-click": true, justify: "end", gap: "xs", className: "border-t border-[var(--appkit-color-border)] -mx-[var(--appkit-space-3)] -mb-[var(--appkit-space-3)] mt-[var(--appkit-space-1)] px-[var(--appkit-space-3)] py-[var(--appkit-space-2)]", onClick: (e) => e.stopPropagation(), children: renderRowActions(row) }))] })] }));
58
65
  }
59
66
  export function AdminViewCards({ rows, view, isLoading, emptyLabel = "No items found", onRowClick, selectedIdSet, onToggleSelect, renderRowActions, resourceIcon, }) {
60
67
  if (isLoading) {
@@ -7,7 +7,21 @@ import { useAdminListing } from "../hooks/useAdminListing";
7
7
  import { getResourceIcon } from "../../../ui/columns/column-renderers";
8
8
  import { AdminViewCards } from "./AdminViewCards";
9
9
  import { DataTable } from "./DataTable";
10
+ import { useBreakpoint } from "../../../react/hooks/useBreakpoint";
10
11
  export function DataListingView({ config, }) {
12
+ /*
13
+ * Below `md` the table view is suppressed entirely and the list/card view is
14
+ * rendered instead.
15
+ *
16
+ * No amount of per-column hiding rescues a grid at 320px — even a two-column
17
+ * table wraps its cells, so rows grow to different heights and stop being
18
+ * scannable. `TableColumn.priority` handles density from `md` upward; this
19
+ * handles the case below it, where the answer is "not a table at all".
20
+ *
21
+ * The user's stored preference is untouched, so widening the window brings
22
+ * their table straight back.
23
+ */
24
+ const { isMobile } = useBreakpoint();
11
25
  const effectiveInitialView = config.initialView ?? "list";
12
26
  const listing = useAdminListing({
13
27
  ...config,
@@ -66,7 +80,7 @@ export function DataListingView({ config, }) {
66
80
  : undefined;
67
81
  return (_jsxs(Div, { className: config.className ?? "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: config.searchPlaceholder, onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || config.defaultSort, sortOptions: config.sortOptions, onSortChange: (v) => table.set("sort", v), showTableView: !config.hideTableView, toggles: config.toggles, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState, extra: config.primaryAction || config.toolbarExtra ? (_jsxs(Row, { align: "center", gap: "sm", children: [config.toolbarExtra, config.primaryAction && (_jsxs(Button, { gap: "sm", size: "sm", onClick: () => config.primaryAction.onClick({
68
82
  openCreatePanel: panel.openCreatePanel,
69
- }), children: [config.primaryAction.icon ?? _jsx(Plus, { className: "h-4 w-4" }), config.primaryAction.label] }))] })) : undefined }), config.renderAboveContent?.(), bulkActionItems && (_jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: bulkActionItems })), 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), pageSize: pageSize, onPageSizeChange: setPageSize, paginationConfig: { showPageSizeSelector: true, pageSizeOptions: [10, 25, 50, 100] } }) }) })), _jsxs(Div, { paddingX: "x-sm-md", padding: "y-md", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), view === "table" && !config.hideTableView ? (_jsx(DataTable, { columns: config.columns, rows: rows, resourceIcon: resourceIcon, isLoading: isLoading, emptyLabel: config.emptyLabel ?? `No ${config.title.toLowerCase()} found`, selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle, onToggleSelectAll: (next) => next
83
+ }), children: [config.primaryAction.icon ?? _jsx(Plus, { className: "h-4 w-4" }), config.primaryAction.label] }))] })) : undefined }), config.renderAboveContent?.(), bulkActionItems && (_jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: bulkActionItems })), 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), pageSize: pageSize, onPageSizeChange: setPageSize, paginationConfig: { showPageSizeSelector: true, pageSizeOptions: [10, 25, 50, 100] } }) }) })), _jsxs(Div, { paddingX: "x-sm-md", padding: "y-md", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), view === "table" && !config.hideTableView && !isMobile ? (_jsx(DataTable, { columns: config.columns, rows: rows, resourceIcon: resourceIcon, isLoading: isLoading, emptyLabel: config.emptyLabel ?? `No ${config.title.toLowerCase()} found`, selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle, onToggleSelectAll: (next) => next
70
84
  ? selection.setSelectedIds(rows.map((r) => r.id))
71
85
  : selection.clearSelection(), rowHrefTemplate: config.rowHrefTemplate, onRowClick: config.onRowClick
72
86
  ? (row) => config.onRowClick(row, {
@@ -1,36 +1,26 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { BaseListingCard, Button, Checkbox, Div, Row, Span, Stack, Table, Tbody, Td, Text, Th, Thead, Tr } from "../../../ui";
4
- import { getStatusTone } from "../../../ui/columns/column-renderers";
5
- import { MediaImage } from "../../media/MediaImage";
4
+ import { renderStatusBadge, renderThumbnail } from "../../../ui/columns/cell-renderers";
5
+ import { resolveColumnPriority } from "../../../ui/columns/build-columns";
6
+ import { COLUMN_PRIORITY_CLASS } from "../../../contracts/extend";
6
7
  import { useLongPress } from "../../../react/hooks/useLongPress";
7
8
  const __O = {
8
9
  hidden: "overflow-hidden",
9
10
  xAuto: "overflow-x-auto",
10
11
  };
11
- const STATUS_TONE_CLASSES = {
12
- success: "bg-success-surface text-success",
13
- warning: "bg-warning-surface text-warning",
14
- error: "bg-error-surface text-error",
15
- info: "bg-info-surface text-info",
16
- neutral: "bg-primary-50 text-primary-800 dark:bg-secondary-900/30 dark:text-secondary-300",
17
- };
18
12
  function buildDefaultColumns(resourceIcon) {
19
13
  return [
20
14
  {
21
15
  key: "primary",
22
16
  header: "Name",
23
- render: (row) => (_jsxs(Row, { gap: "sm", align: "center", children: [_jsx(Div, { className: "relative h-8 w-8 shrink-0", overflow: "hidden", rounded: "full", children: _jsx(MediaImage, { src: typeof row.image === "string" ? row.image : undefined, alt: String(row.primary ?? ""), size: "avatar", fallback: resourceIcon }) }), _jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsx(Text, { weight: "medium", color: "primary", className: "truncate", children: String(row.primary ?? "") }), row.secondary ? _jsx(Text, { size: "xs", color: "muted", className: "truncate", children: String(row.secondary) }) : null] })] })),
17
+ render: (row) => (_jsxs(Row, { gap: "sm", align: "center", children: [renderThumbnail(typeof row.image === "string" ? row.image : undefined, String(row.primary ?? ""), { rounded: "full", fallback: resourceIcon }), _jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsx(Text, { weight: "medium", color: "primary", className: "truncate", children: String(row.primary ?? "") }), row.secondary ? _jsx(Text, { size: "xs", color: "muted", className: "truncate", children: String(row.secondary) }) : null] })] })),
24
18
  },
25
19
  {
26
20
  key: "status",
27
21
  header: "Status",
28
22
  className: "w-32",
29
- render: (row) => {
30
- const status = String(row.status ?? "—");
31
- const tone = getStatusTone(status);
32
- return (_jsx(Span, { size: "xs", weight: "medium", className: `inline-flex ${STATUS_TONE_CLASSES[tone]}`, rounded: "full", padding: "pill-sm-tall", children: status }));
33
- },
23
+ render: (row) => renderStatusBadge(row.status == null ? null : String(row.status)),
34
24
  },
35
25
  {
36
26
  key: "updatedAt",
@@ -54,7 +44,7 @@ function SelectableRow({ row, columns, isSelected, onToggle, renderRowActions, o
54
44
  }
55
45
  : undefined;
56
46
  const isInteractive = Boolean(onRowClick ?? rowHref);
57
- return (_jsxs(Tr, { "data-testid": "data-table-row", onClick: handleClick, onKeyDown: handleKeyDown, role: isInteractive ? "link" : undefined, tabIndex: isInteractive ? 0 : undefined, onMouseDown: selectionEnabled && !isSelected ? longPress.onMouseDown : undefined, onMouseUp: selectionEnabled && !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: selectionEnabled && !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: selectionEnabled && !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: selectionEnabled && !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: selectionEnabled && !isSelected ? longPress.onTouchCancel : undefined, className: `hover:bg-[var(--appkit-color-bg)] dark:hover:bg-[var(--appkit-color-bg)] ${isInteractive ? "cursor-pointer" : ""} ${isSelected ? "bg-primary/5 dark:bg-primary/10" : ""}`, border: "default", children: [selectionEnabled && (_jsx(Td, { padding: "xs-3", className: "relative w-10", onClick: (e) => e.stopPropagation(), children: _jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => { e.preventDefault(); onToggle?.(row.id, !isSelected); }, label: isSelected ? "Deselect row" : "Select row", position: "top-1/2 left-2 -translate-y-1/2", "data-testid": "row-checkbox" }) })), columns.map((col) => (_jsx(Td, { color: "muted", className: col.className ?? "", padding: "md", children: col.render
47
+ return (_jsxs(Tr, { "data-testid": "data-table-row", onClick: handleClick, onKeyDown: handleKeyDown, role: isInteractive ? "link" : undefined, tabIndex: isInteractive ? 0 : undefined, onMouseDown: selectionEnabled && !isSelected ? longPress.onMouseDown : undefined, onMouseUp: selectionEnabled && !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: selectionEnabled && !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: selectionEnabled && !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: selectionEnabled && !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: selectionEnabled && !isSelected ? longPress.onTouchCancel : undefined, className: `hover:bg-[var(--appkit-color-bg)] dark:hover:bg-[var(--appkit-color-bg)] ${isInteractive ? "cursor-pointer" : ""} ${isSelected ? "bg-primary/5 dark:bg-primary/10" : ""}`, border: "default", children: [selectionEnabled && (_jsx(Td, { padding: "xs-3", className: "relative w-10", onClick: (e) => e.stopPropagation(), children: _jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => { e.preventDefault(); onToggle?.(row.id, !isSelected); }, label: isSelected ? "Deselect row" : "Select row", position: "top-1/2 left-2 -translate-y-1/2", "data-testid": "row-checkbox" }) })), columns.map((col) => (_jsx(Td, { color: "muted", className: `${COLUMN_PRIORITY_CLASS[resolveColumnPriority(col)]} ${col.className ?? ""}`, padding: "md", children: col.render
58
48
  ? col.render(row)
59
49
  : String(row[col.key] ?? "") }, col.key))), renderRowActions && (_jsx(Td, { padding: "xs-3", onClick: (e) => e.stopPropagation(), children: renderRowActions(row) }))] }));
60
50
  }
@@ -62,7 +52,7 @@ export function DataTable({ columns: columnsProp, rows, isLoading, sortKey, sort
62
52
  const columns = (columnsProp ?? buildDefaultColumns(resourceIcon));
63
53
  const selectionEnabled = Boolean(onToggleSelect);
64
54
  const allRowsSelected = selectionEnabled && rows.length > 0 && rows.every((r) => selectedIds?.has(r.id));
65
- return (_jsxs(Div, { surface: "card", className: `${__O.hidden}`, children: [_jsx(Div, { className: `${__O.xAuto}`, children: _jsxs(Table, { "data-testid": "data-table", size: "sm", children: [_jsx(Thead, { children: _jsxs(Tr, { className: "bg-[var(--appkit-color-bg)] dark:bg-[var(--appkit-color-surface-elevated)]", border: "default", children: [selectionEnabled && (_jsx(Th, { padding: "xs-3", scope: "col", className: "w-10", children: onToggleSelectAll && (_jsx(Checkbox, { bare: true, "data-testid": "select-all-checkbox", "aria-label": allRowsSelected ? "Deselect all" : "Select all", checked: allRowsSelected, onChange: () => onToggleSelectAll(!allRowsSelected), className: "h-4 w-4 rounded border-[var(--appkit-color-border)] accent-zinc-900 dark:accent-zinc-100" })) })), columns.map((col) => (_jsxs(Th, { scope: "col", onClick: col.sortable && onSort ? () => onSort(col.key) : undefined, className: `text-left text-[var(--appkit-color-text)] text-[var(--appkit-color-text)] ${col.sortable && onSort ? "cursor-pointer select-none hover:text-primary" : ""} ${col.className ?? ""}`, padding: "md", weight: "semibold", children: [col.header, col.sortable && sortKey === col.key && (_jsx(Span, { className: "ml-1", children: sortDir === "asc" ? "↑" : "↓" }))] }, col.key))), renderRowActions && _jsx(Th, { padding: "xs-3", scope: "col", className: "w-12" })] }) }), _jsx(Tbody, { children: isLoading ? (Array.from({ length: 5 }).map((_, i) => (_jsxs(Tr, { border: "default", children: [selectionEnabled && _jsx(Td, { padding: "xs-3", className: "w-10" }), columns.map((col) => (_jsx(Td, { padding: "md", children: _jsx(Div, { className: "h-4 w-full animate-pulse bg-neutral-200", rounded: "default" }) }, col.key)))] }, i)))) : rows.length === 0 ? (_jsx(Tr, { children: _jsx(Td, { colSpan: columns.length + (selectionEnabled ? 1 : 0) + (renderRowActions ? 1 : 0), align: "center", color: "faint", paddingX: "md", paddingY: "md", children: emptyLabel }) })) : (rows.map((row) => (_jsx(SelectableRow, { row: row, columns: columns, isSelected: selectedIds?.has(row.id) ?? false, onToggle: onToggleSelect, renderRowActions: renderRowActions, onRowClick: onRowClick, rowHref: typeof rowHrefTemplate === "function"
55
+ return (_jsxs(Div, { surface: "card", className: `${__O.hidden}`, children: [_jsx(Div, { className: `${__O.xAuto}`, children: _jsxs(Table, { "data-testid": "data-table", size: "sm", children: [_jsx(Thead, { children: _jsxs(Tr, { className: "bg-[var(--appkit-color-bg)] dark:bg-[var(--appkit-color-surface-elevated)]", border: "default", children: [selectionEnabled && (_jsx(Th, { padding: "xs-3", scope: "col", className: "w-10", children: onToggleSelectAll && (_jsx(Checkbox, { bare: true, "data-testid": "select-all-checkbox", "aria-label": allRowsSelected ? "Deselect all" : "Select all", checked: allRowsSelected, onChange: () => onToggleSelectAll(!allRowsSelected), className: "h-4 w-4 rounded border-[var(--appkit-color-border)] accent-zinc-900 dark:accent-zinc-100" })) })), columns.map((col) => (_jsxs(Th, { scope: "col", onClick: col.sortable && onSort ? () => onSort(col.key) : undefined, className: `text-left text-[var(--appkit-color-text)] ${col.sortable && onSort ? "cursor-pointer select-none hover:text-primary" : ""} ${COLUMN_PRIORITY_CLASS[resolveColumnPriority(col)]} ${col.className ?? ""}`, padding: "md", weight: "semibold", children: [col.header, col.sortable && sortKey === col.key && (_jsx(Span, { className: "ml-1", children: sortDir === "asc" ? "↑" : "↓" }))] }, col.key))), renderRowActions && _jsx(Th, { padding: "xs-3", scope: "col", className: "w-12" })] }) }), _jsx(Tbody, { children: isLoading ? (Array.from({ length: 5 }).map((_, i) => (_jsxs(Tr, { border: "default", children: [selectionEnabled && _jsx(Td, { padding: "xs-3", className: "w-10" }), columns.map((col) => (_jsx(Td, { padding: "md", className: COLUMN_PRIORITY_CLASS[resolveColumnPriority(col)], children: _jsx(Div, { className: "h-4 w-full animate-pulse bg-neutral-200", rounded: "default" }) }, col.key)))] }, i)))) : rows.length === 0 ? (_jsx(Tr, { children: _jsx(Td, { colSpan: columns.length + (selectionEnabled ? 1 : 0) + (renderRowActions ? 1 : 0), align: "center", color: "faint", paddingX: "md", paddingY: "md", children: emptyLabel }) })) : (rows.map((row) => (_jsx(SelectableRow, { row: row, columns: columns, isSelected: selectedIds?.has(row.id) ?? false, onToggle: onToggleSelect, renderRowActions: renderRowActions, onRowClick: onRowClick, rowHref: typeof rowHrefTemplate === "function"
66
56
  ? rowHrefTemplate(row)
67
57
  : rowHrefTemplate?.replace("{id}", encodeURIComponent(row.id)), selectionEnabled: selectionEnabled }, row.id)))) })] }) }), totalPages > 1 && onPageChange && (_jsx(Row, { className: "border-t border-neutral-200", padding: "inline", align: "center", justify: "end", gap: "sm", children: Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => (_jsx(Button, { onClick: () => onPageChange(p), variant: p === currentPage ? "primary" : "ghost", size: "sm", className: `h-8 w-8 rounded text-[length:var(--appkit-text-xs)] font-medium transition ${p === currentPage ? "bg-neutral-900 text-white" : "text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface)] hover:bg-[var(--appkit-color-surface-elevated)]"}`, children: p }, p))) }))] }));
68
58
  }
@@ -12,6 +12,7 @@ const ACTION_LABELS = {
12
12
  checkout_bypass: "Admin checkout bypass",
13
13
  coupon_update: "Coupon updated",
14
14
  payout_mark_paid: "Payout marked paid",
15
+ offer_cancel: "Offer cancelled",
15
16
  store_status_change: "Store status changed",
16
17
  user_role_change: "User role changed",
17
18
  };
@@ -1,6 +1,8 @@
1
1
  import type { FirestoreDocument } from "@mohasinac/appkit";
2
- export declare const SECTION_TYPE_OPTIONS: readonly ["welcome", "stats", "trust-indicators", "categories", "brands", "products", "pre-orders", "auctions", "banner", "features", "reviews", "whatsapp-community", "faq", "blog-articles", "newsletter", "stores", "events", "social-feed", "carousel", "custom-cards", "google-reviews", "featured-bundles", "prize-draws", "event-raffles", "collection-cards"];
3
- export type SectionType = (typeof SECTION_TYPE_OPTIONS)[number];
2
+ import type { SectionType as CanonicalSectionType } from "../../../homepage/schemas/firestore";
3
+ export declare const SECTION_TYPE_OPTIONS: CanonicalSectionType[];
4
+ /** Re-exported, NOT re-declared. There is one `SectionType` in this codebase. */
5
+ export type SectionType = CanonicalSectionType;
4
6
  export interface SectionPatchPayload {
5
7
  enabled: boolean;
6
8
  order?: number;
@@ -1,32 +1,50 @@
1
1
  import { ROUTES } from "../../../../next/routing/route-map";
2
2
  const LABEL_VERIFIED_SELLERS = "Verified Sellers";
3
- export const SECTION_TYPE_OPTIONS = [
4
- "welcome",
5
- "stats",
6
- "trust-indicators",
7
- "categories",
8
- "brands",
9
- "products",
10
- "pre-orders",
11
- "auctions",
12
- "banner",
13
- "features",
14
- "reviews",
15
- "whatsapp-community",
16
- "faq",
17
- "blog-articles",
18
- "newsletter",
19
- "stores",
20
- "events",
21
- "social-feed",
22
- "carousel",
23
- "custom-cards",
24
- "google-reviews",
25
- "featured-bundles",
26
- "prize-draws",
27
- "event-raffles",
28
- "collection-cards",
29
- ];
3
+ /*
4
+ * The admin dropdown's section types.
5
+ *
6
+ * 🛑 This used to be a bare `as const` array with its OWN `SectionType` derived
7
+ * from it — a second declaration shadowing the real `SectionType` in
8
+ * `features/homepage/schemas/firestore.ts`, which is how the two came to
9
+ * disagree with the create route (Recurrent Root Cause #36's shape, one union
10
+ * over). The route's own copy was missing `featured-bundles`, `prize-draws`,
11
+ * `event-raffles` and `collection-cards`, so choosing any of those four in this
12
+ * dropdown produced a 400 on save — four of twenty-five section types could be
13
+ * configured in full and never created.
14
+ *
15
+ * It is now a `Record<CanonicalSectionType, true>`, so a type added to the
16
+ * union and not offered here is a COMPILE error, and an entry here that is not
17
+ * a real section type is too. Declaration order is preserved because the array
18
+ * is derived from this map's keys, and that order is the dropdown's order.
19
+ */
20
+ const SECTION_TYPE_PRESENCE = {
21
+ welcome: true,
22
+ stats: true,
23
+ "trust-indicators": true,
24
+ categories: true,
25
+ brands: true,
26
+ products: true,
27
+ "pre-orders": true,
28
+ auctions: true,
29
+ banner: true,
30
+ features: true,
31
+ reviews: true,
32
+ "whatsapp-community": true,
33
+ faq: true,
34
+ "blog-articles": true,
35
+ newsletter: true,
36
+ stores: true,
37
+ events: true,
38
+ "social-feed": true,
39
+ carousel: true,
40
+ "custom-cards": true,
41
+ "google-reviews": true,
42
+ "featured-bundles": true,
43
+ "prize-draws": true,
44
+ "event-raffles": true,
45
+ "collection-cards": true,
46
+ };
47
+ export const SECTION_TYPE_OPTIONS = Object.keys(SECTION_TYPE_PRESENCE);
30
48
  export const DEFAULT_PRODUCTS_BUILDER = {
31
49
  title: "Featured Products",
32
50
  subtitle: "",
@@ -188,6 +188,9 @@ export declare const ADMIN_AUDIT_LOG_ACTION_TABS: readonly [{
188
188
  }, {
189
189
  readonly id: "payout_mark_paid";
190
190
  readonly label: "Payout paid";
191
+ }, {
192
+ readonly id: "offer_cancel";
193
+ readonly label: "Offer cancelled";
191
194
  }, {
192
195
  readonly id: "store_status_change";
193
196
  readonly label: "Store status";
@@ -105,6 +105,7 @@ export const ADMIN_AUDIT_LOG_ACTION_TABS = [
105
105
  { id: "checkout_bypass", label: "Checkout bypass" },
106
106
  { id: "coupon_update", label: "Coupon update" },
107
107
  { id: "payout_mark_paid", label: "Payout paid" },
108
+ { id: "offer_cancel", label: "Offer cancelled" },
108
109
  { id: "store_status_change", label: "Store status" },
109
110
  { id: "user_role_change", label: "Role change" },
110
111
  ];
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
4
+ * by schema instance and every zod wrapper returns a new one.
5
+ */
6
+ export declare const homepageSectionFormSchema: z.ZodObject<{
7
+ sectionType: z.ZodString;
8
+ order: z.ZodOptional<z.ZodNumber>;
9
+ enabled: z.ZodBoolean;
10
+ configJson: z.ZodEffects<z.ZodString, string, string>;
11
+ }, "strip", z.ZodTypeAny, {
12
+ enabled: boolean;
13
+ sectionType: string;
14
+ configJson: string;
15
+ order?: number | undefined;
16
+ }, {
17
+ enabled: boolean;
18
+ sectionType: string;
19
+ configJson: string;
20
+ order?: number | undefined;
21
+ }>;
22
+ export type HomepageSectionFormValues = z.infer<typeof homepageSectionFormSchema>;