@mohasinac/appkit 4.30.19 → 4.31.1

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 (417) hide show
  1. package/dist/_internal/client/features/layout/useSidebarSearch.d.ts +21 -5
  2. package/dist/_internal/client/features/layout/useSidebarSearch.js +27 -6
  3. package/dist/_internal/client/features/lottery/LotteryAdminEditView.d.ts +23 -3
  4. package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +106 -87
  5. package/dist/_internal/client/features/lottery/LotteryPullForm.d.ts +16 -5
  6. package/dist/_internal/client/features/lottery/LotteryPullForm.js +65 -40
  7. package/dist/_internal/server/features/auth/permissions.d.ts +2 -0
  8. package/dist/_internal/server/features/auth/permissions.js +3 -2
  9. package/dist/_internal/server/features/categories/data.js +15 -4
  10. package/dist/_internal/server/features/classified/index.d.ts +0 -1
  11. package/dist/_internal/server/features/classified/index.js +0 -1
  12. package/dist/_internal/server/features/digital-code/index.d.ts +0 -1
  13. package/dist/_internal/server/features/digital-code/index.js +0 -1
  14. package/dist/_internal/server/features/live/index.d.ts +0 -1
  15. package/dist/_internal/server/features/live/index.js +0 -1
  16. package/dist/_internal/server/features/products/data.js +15 -5
  17. package/dist/_internal/server/features/products/list-public.d.ts +9 -0
  18. package/dist/_internal/server/features/products/list-public.js +95 -7
  19. package/dist/_internal/server/features/site-settings/adapters.d.ts +2 -2
  20. package/dist/_internal/server/features/site-settings/adapters.js +8 -5
  21. package/dist/_internal/server/features/tester/visibility.d.ts +2 -1
  22. package/dist/_internal/server/features/tester/visibility.js +2 -2
  23. package/dist/_internal/server/functions/scheduled.d.ts +3 -1
  24. package/dist/_internal/server/functions/scheduled.js +27 -1
  25. package/dist/_internal/server/jobs/core/hardBanCascade.js +4 -0
  26. package/dist/_internal/server/jobs/core/index.d.ts +2 -0
  27. package/dist/_internal/server/jobs/core/index.js +2 -0
  28. package/dist/_internal/server/jobs/core/pageViewPrune.d.ts +10 -0
  29. package/dist/_internal/server/jobs/core/pageViewPrune.js +44 -0
  30. package/dist/_internal/server/jobs/core/pageViewRollup.d.ts +2 -0
  31. package/dist/_internal/server/jobs/core/pageViewRollup.js +87 -0
  32. package/dist/_internal/server/jobs/handlers/index.d.ts +2 -0
  33. package/dist/_internal/server/jobs/handlers/index.js +2 -0
  34. package/dist/_internal/server/jobs/handlers/pageViewPrune.d.ts +2 -0
  35. package/dist/_internal/server/jobs/handlers/pageViewPrune.js +2 -0
  36. package/dist/_internal/server/jobs/handlers/pageViewRollup.d.ts +2 -0
  37. package/dist/_internal/server/jobs/handlers/pageViewRollup.js +2 -0
  38. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  39. package/dist/_internal/shared/actions/action-registry.js +69 -22
  40. package/dist/_internal/shared/checkout/rules/classified.rule.d.ts +9 -4
  41. package/dist/_internal/shared/checkout/rules/classified.rule.js +7 -2
  42. package/dist/_internal/shared/features/layout/matchesNavQuery.d.ts +14 -0
  43. package/dist/_internal/shared/features/layout/matchesNavQuery.js +72 -0
  44. package/dist/_internal/shared/features/layout/types.d.ts +37 -1
  45. package/dist/_internal/shared/features/layout/types.js +19 -1
  46. package/dist/_internal/shared/features/notifications/action-url.d.ts +9 -1
  47. package/dist/_internal/shared/features/notifications/action-url.js +47 -0
  48. package/dist/_internal/shared/features/offers/config.d.ts +65 -0
  49. package/dist/_internal/shared/features/offers/config.js +73 -2
  50. package/dist/_internal/shared/features/orders/schema.d.ts +19 -1
  51. package/dist/_internal/shared/features/orders/schema.js +22 -2
  52. package/dist/_internal/shared/features/site-settings/types.d.ts +12 -3
  53. package/dist/_internal/shared/listing-types/capabilities.d.ts +8 -2
  54. package/dist/_internal/shared/listing-types/capabilities.js +6 -1
  55. package/dist/_internal/shared/listing-types/classified/ctas.js +6 -1
  56. package/dist/_internal/shared/listing-types/feature-flags.d.ts +10 -5
  57. package/dist/_internal/shared/listing-types/feature-flags.js +11 -6
  58. package/dist/client.d.ts +37 -20
  59. package/dist/client.js +36 -14
  60. package/dist/configs/next.js +10 -2
  61. package/dist/constants/api-endpoints.d.ts +18 -54
  62. package/dist/constants/api-endpoints.js +6 -26
  63. package/dist/constants/field-names.d.ts +6 -24
  64. package/dist/constants/field-names.js +10 -27
  65. package/dist/constants/geo/countries.d.ts +50 -0
  66. package/dist/constants/geo/countries.js +129 -0
  67. package/dist/constants/geo/subdivisions.d.ts +13 -0
  68. package/dist/constants/geo/subdivisions.js +104 -0
  69. package/dist/constants/index.d.ts +2 -2
  70. package/dist/constants/index.js +2 -2
  71. package/dist/constants/table-keys.d.ts +24 -0
  72. package/dist/constants/table-keys.js +28 -0
  73. package/dist/contracts/extend.d.ts +0 -9
  74. package/dist/contracts/index.d.ts +1 -1
  75. package/dist/contracts/realtime-session.js +7 -0
  76. package/dist/core/browser.d.ts +1 -1
  77. package/dist/core/browser.js +1 -1
  78. package/dist/core/index.d.ts +1 -1
  79. package/dist/core/index.js +1 -1
  80. package/dist/core/site-config.d.ts +0 -3
  81. package/dist/core/site-config.js +0 -3
  82. package/dist/errors/messages.d.ts +0 -8
  83. package/dist/errors/messages.js +0 -9
  84. package/dist/features/about/schemas/index.d.ts +8 -8
  85. package/dist/features/account/components/AddressFilters.js +32 -11
  86. package/dist/features/account/components/AddressForm.d.ts +26 -25
  87. package/dist/features/account/components/AddressForm.js +81 -103
  88. package/dist/features/account/components/AddressesIndexListing.js +8 -4
  89. package/dist/features/account/components/UserSidebar.d.ts +3 -4
  90. package/dist/features/account/components/UserSupportView.js +89 -58
  91. package/dist/features/account/components/index.d.ts +1 -7
  92. package/dist/features/account/components/index.js +0 -3
  93. package/dist/features/account/hooks/useAddresses.d.ts +4 -3
  94. package/dist/features/account/hooks/useAddresses.js +4 -6
  95. package/dist/features/account/hooks/useCollapsedSections.d.ts +51 -10
  96. package/dist/features/account/hooks/useCollapsedSections.js +211 -38
  97. package/dist/features/account/hooks/useProfile.d.ts +8 -0
  98. package/dist/features/account/index.d.ts +0 -3
  99. package/dist/features/account/index.js +0 -3
  100. package/dist/features/account/schemas/index.d.ts +0 -92
  101. package/dist/features/account/schemas/index.js +42 -20
  102. package/dist/features/addresses/components/AddressLocationFields.d.ts +13 -0
  103. package/dist/features/addresses/components/AddressLocationFields.js +41 -0
  104. package/dist/features/addresses/hooks/usePostalLookup.d.ts +17 -0
  105. package/dist/features/addresses/hooks/usePostalLookup.js +133 -0
  106. package/dist/features/addresses/schemas/address-form.d.ts +423 -0
  107. package/dist/features/addresses/schemas/address-form.js +212 -0
  108. package/dist/features/addresses/schemas/index.d.ts +1 -0
  109. package/dist/features/addresses/schemas/index.js +8 -0
  110. package/dist/features/admin/actions/admin-read-actions.d.ts +1 -1
  111. package/dist/features/admin/actions/index.d.ts +0 -1
  112. package/dist/features/admin/actions/index.js +0 -1
  113. package/dist/features/admin/actions/notification-actions.js +1 -41
  114. package/dist/features/admin/components/AdminActionIndexView.d.ts +32 -0
  115. package/dist/features/admin/components/AdminActionIndexView.js +128 -0
  116. package/dist/features/admin/components/AdminAdEditorView.js +159 -163
  117. package/dist/features/admin/components/AdminAddressEditorView.js +97 -55
  118. package/dist/features/admin/components/AdminBrandEditorView.d.ts +15 -0
  119. package/dist/features/admin/components/AdminBrandEditorView.js +113 -77
  120. package/dist/features/admin/components/AdminBundleEditorView.d.ts +19 -9
  121. package/dist/features/admin/components/AdminBundleEditorView.js +136 -168
  122. package/dist/features/admin/components/AdminCategoryEditorView.js +120 -43
  123. package/dist/features/admin/components/AdminCouponEditorView.js +145 -109
  124. package/dist/features/admin/components/AdminEmployeeEditorView.js +125 -56
  125. package/dist/features/admin/components/AdminFaqEditorView.js +112 -58
  126. package/dist/features/admin/components/AdminFeatureEditorView.js +108 -68
  127. package/dist/features/admin/components/AdminNavEditorView.js +69 -44
  128. package/dist/features/admin/components/AdminOrderEditorView.js +47 -10
  129. package/dist/features/admin/components/AdminPrizeDrawsView.js +16 -2
  130. package/dist/features/admin/components/AdminSidebar.d.ts +11 -7
  131. package/dist/features/admin/components/AdminSiteSettingsView.js +37 -37
  132. package/dist/features/admin/components/AdminStoreEditorView.js +75 -52
  133. package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +71 -30
  134. package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +95 -65
  135. package/dist/features/admin/components/BrandQuickCreateForm.d.ts +10 -0
  136. package/dist/features/admin/components/BrandQuickCreateForm.js +49 -26
  137. package/dist/features/admin/components/CategoryQuickCreateForm.d.ts +11 -0
  138. package/dist/features/admin/components/CategoryQuickCreateForm.js +48 -26
  139. package/dist/features/admin/components/index.d.ts +2 -2
  140. package/dist/features/admin/components/index.js +1 -1
  141. package/dist/features/admin/constants/site-settings-tabs.d.ts +120 -0
  142. package/dist/features/admin/constants/site-settings-tabs.js +152 -0
  143. package/dist/features/admin/index.d.ts +0 -3
  144. package/dist/features/admin/index.js +0 -2
  145. package/dist/features/admin/repository/analytics-rollup.repository.d.ts +35 -0
  146. package/dist/features/admin/repository/analytics-rollup.repository.js +30 -0
  147. package/dist/features/admin/schemas/admin-editor-forms.d.ts +56 -57
  148. package/dist/features/admin/schemas/admin-editor-forms.js +97 -54
  149. package/dist/features/admin/schemas/admin-ops-forms.d.ts +57 -5
  150. package/dist/features/admin/schemas/admin-ops-forms.js +104 -20
  151. package/dist/features/admin/schemas/admin-user-form.js +7 -0
  152. package/dist/features/admin/schemas/firestore.d.ts +48 -95
  153. package/dist/features/admin/schemas/firestore.js +15 -151
  154. package/dist/features/admin/schemas/nav-item-form.js +9 -2
  155. package/dist/features/admin/server.d.ts +0 -4
  156. package/dist/features/admin/server.js +0 -2
  157. package/dist/features/admin/types/index.d.ts +0 -7
  158. package/dist/features/analytics/repository/page-views.repository.d.ts +14 -0
  159. package/dist/features/analytics/repository/page-views.repository.js +24 -0
  160. package/dist/features/auth/actions/index.d.ts +0 -1
  161. package/dist/features/auth/actions/index.js +0 -1
  162. package/dist/features/auth/actions/realtime-token-actions.js +1 -12
  163. package/dist/features/auth/auth-helpers.js +5 -7
  164. package/dist/features/auth/components/Guards.js +2 -1
  165. package/dist/features/auth/components/LoginForm.d.ts +0 -2
  166. package/dist/features/auth/components/LoginForm.js +1 -3
  167. package/dist/features/auth/components/RegisterForm.js +62 -19
  168. package/dist/features/auth/permissions/constants.d.ts +27 -1
  169. package/dist/features/auth/role-predicates.d.ts +75 -7
  170. package/dist/features/auth/role-predicates.js +28 -2
  171. package/dist/features/auth/schemas/firestore.d.ts +22 -0
  172. package/dist/features/auth/schemas/index.d.ts +1 -1
  173. package/dist/features/auth/schemas/index.js +44 -8
  174. package/dist/features/auth/types/index.d.ts +14 -1
  175. package/dist/features/blog/api/route.js +10 -2
  176. package/dist/features/blog/components/BlogIndexListing.js +1 -1
  177. package/dist/features/blog/hooks/useBlog.js +2 -2
  178. package/dist/features/blog/schemas/index.d.ts +4 -0
  179. package/dist/features/blog/schemas/index.js +2 -0
  180. package/dist/features/blog/types/index.d.ts +1 -1
  181. package/dist/features/catalogue/components/CatalogueItemEditorView.d.ts +22 -0
  182. package/dist/features/catalogue/components/CatalogueItemEditorView.js +112 -52
  183. package/dist/features/catalogue/schemas/validation.d.ts +4 -4
  184. package/dist/features/catalogue/schemas/validation.js +47 -10
  185. package/dist/features/categories/schemas/bundle-form.d.ts +142 -0
  186. package/dist/features/categories/schemas/bundle-form.js +149 -0
  187. package/dist/features/classified/components/ClassifiedDetailPageView.d.ts +5 -4
  188. package/dist/features/classified/components/ClassifiedDetailPageView.js +37 -16
  189. package/dist/features/consultation/components/ConsultationForm.js +1 -1
  190. package/dist/features/consultation/schemas/index.js +34 -10
  191. package/dist/features/corporate/schemas/index.js +36 -15
  192. package/dist/features/filters/FilterPanel.js +3 -3
  193. package/dist/features/filters/RangeFilter.d.ts +10 -0
  194. package/dist/features/filters/RangeFilter.js +41 -4
  195. package/dist/features/homepage/components/MarketplaceHomepageView.js +24 -5
  196. package/dist/features/homepage/hooks/useBlogArticles.js +1 -1
  197. package/dist/features/layout/BottomActions.js +84 -11
  198. package/dist/features/layout/BottomActionsContext.d.ts +1 -6
  199. package/dist/features/layout/BottomActionsContext.js +1 -1
  200. package/dist/features/layout/hooks/useBottomActions.js +2 -2
  201. package/dist/features/layout/hooks/useFormBottomActions.d.ts +12 -1
  202. package/dist/features/layout/hooks/useFormBottomActions.js +12 -6
  203. package/dist/features/lottery/schemas/config-write.d.ts +9 -5
  204. package/dist/features/lottery/schemas/config-write.js +69 -9
  205. package/dist/features/orders/schemas/buyer-forms.d.ts +6 -3
  206. package/dist/features/orders/schemas/buyer-forms.js +31 -3
  207. package/dist/features/orders/schemas/index.d.ts +2 -2
  208. package/dist/features/products/components/GroupSettingsPanel.js +3 -1
  209. package/dist/features/products/components/MakeOfferButton.d.ts +13 -2
  210. package/dist/features/products/components/MakeOfferButton.js +33 -11
  211. package/dist/features/products/components/ProductDetailActions.js +0 -1
  212. package/dist/features/products/components/ProductDetailPageView.d.ts +7 -3
  213. package/dist/features/products/components/ProductDetailPageView.js +8 -5
  214. package/dist/features/products/constants/action-defs.d.ts +1 -2
  215. package/dist/features/products/constants/action-defs.js +40 -43
  216. package/dist/features/products/repository/products.repository.d.ts +5 -1
  217. package/dist/features/products/repository/products.repository.js +23 -1
  218. package/dist/features/products/schemas/firestore.d.ts +7 -3
  219. package/dist/features/products/schemas/product-features.validators.js +3 -0
  220. package/dist/features/promotions/components/CouponCard.js +8 -7
  221. package/dist/features/scams/schemas/firestore.d.ts +15 -1
  222. package/dist/features/scams/schemas/report-form.d.ts +5 -5
  223. package/dist/features/scams/schemas/report-form.js +18 -3
  224. package/dist/features/search/action-index/derive.d.ts +49 -0
  225. package/dist/features/search/action-index/derive.js +121 -0
  226. package/dist/features/search/action-index/repository.d.ts +42 -0
  227. package/dist/features/search/action-index/repository.js +100 -0
  228. package/dist/features/search/action-index/settings-entries.d.ts +41 -0
  229. package/dist/features/search/action-index/settings-entries.js +268 -0
  230. package/dist/features/search/action-index/types.d.ts +81 -0
  231. package/dist/features/search/action-index/types.js +102 -0
  232. package/dist/features/search/action-index/validate-entry.d.ts +34 -0
  233. package/dist/features/search/action-index/validate-entry.js +114 -0
  234. package/dist/features/search/hooks/useNavSuggestions.d.ts +5 -1
  235. package/dist/features/search/hooks/useNavSuggestions.js +23 -3
  236. package/dist/features/search/repository/search.repository.js +3 -1
  237. package/dist/features/seller/actions/offer-actions.js +43 -24
  238. package/dist/features/seller/actions/seller-actions.js +29 -5
  239. package/dist/features/seller/components/CategoryChipPicker.d.ts +9 -0
  240. package/dist/features/seller/components/CategoryChipPicker.js +12 -0
  241. package/dist/features/seller/components/SellerAddressesView.js +5 -1
  242. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +83 -43
  243. package/dist/features/seller/components/SellerArtView.d.ts +14 -7
  244. package/dist/features/seller/components/SellerArtView.js +4 -119
  245. package/dist/features/seller/components/SellerClassifiedView.d.ts +14 -7
  246. package/dist/features/seller/components/SellerClassifiedView.js +4 -124
  247. package/dist/features/seller/components/SellerCouponEditorView.d.ts +22 -1
  248. package/dist/features/seller/components/SellerCouponEditorView.js +91 -45
  249. package/dist/features/seller/components/SellerDigitalCodesView.d.ts +14 -7
  250. package/dist/features/seller/components/SellerDigitalCodesView.js +4 -126
  251. package/dist/features/seller/components/SellerListingTypeView.d.ts +66 -0
  252. package/dist/features/seller/components/SellerListingTypeView.js +277 -0
  253. package/dist/features/seller/components/SellerLiveView.d.ts +14 -7
  254. package/dist/features/seller/components/SellerLiveView.js +4 -126
  255. package/dist/features/seller/components/SellerSidebar.d.ts +3 -6
  256. package/dist/features/seller/components/SellerStickersView.d.ts +14 -7
  257. package/dist/features/seller/components/SellerStickersView.js +4 -119
  258. package/dist/features/seller/components/SellerSupportView.d.ts +31 -0
  259. package/dist/features/seller/components/SellerSupportView.js +100 -0
  260. package/dist/features/seller/components/index.d.ts +4 -8
  261. package/dist/features/seller/components/index.js +2 -4
  262. package/dist/features/seller/index.d.ts +0 -1
  263. package/dist/features/seller/index.js +0 -1
  264. package/dist/features/seller/schemas/coupon-form.d.ts +214 -0
  265. package/dist/features/seller/schemas/coupon-form.js +155 -23
  266. package/dist/features/seller/schemas/offer-forms.d.ts +16 -7
  267. package/dist/features/seller/schemas/offer-forms.js +28 -12
  268. package/dist/features/shell/FormShell.js +2 -2
  269. package/dist/features/shell/QuickFormDrawer.js +2 -2
  270. package/dist/features/shell/SectionForm.d.ts +19 -1
  271. package/dist/features/shell/SectionForm.js +104 -17
  272. package/dist/features/shell/build-sections.d.ts +23 -0
  273. package/dist/features/shell/build-sections.js +106 -8
  274. package/dist/features/shell/field-ui-meta.d.ts +44 -0
  275. package/dist/features/shell/field-ui-meta.js +14 -7
  276. package/dist/features/shell/form-sections.d.ts +46 -0
  277. package/dist/features/shell/form-sections.js +114 -0
  278. package/dist/features/shipments/components/AdminShipmentEditorView.js +87 -64
  279. package/dist/features/shipments/schemas/validation.d.ts +2 -2
  280. package/dist/features/shipments/schemas/validation.js +44 -13
  281. package/dist/features/store-extensions/schemas/custom-role-form.js +6 -2
  282. package/dist/features/store-extensions/schemas/listing-template-form.d.ts +7 -7
  283. package/dist/features/store-extensions/schemas/payout-method-form.js +14 -0
  284. package/dist/features/stores/actions/store-query-actions.js +3 -2
  285. package/dist/features/stores/api/[storeSlug]/auctions/route.js +3 -1
  286. package/dist/features/stores/api/[storeSlug]/products/route.js +3 -1
  287. package/dist/features/support/repository/support.repository.d.ts +21 -0
  288. package/dist/features/support/repository/support.repository.js +63 -0
  289. package/dist/features/support/schemas/firestore.d.ts +27 -2
  290. package/dist/features/support/schemas/firestore.js +2 -0
  291. package/dist/features/support/schemas/ticket-create-form.js +7 -0
  292. package/dist/features/tester/schemas/checklist-item-form.d.ts +43 -0
  293. package/dist/features/tester/schemas/checklist-item-form.js +109 -0
  294. package/dist/features/tester/seed-data/blog-tester-seed-data.js +2 -2
  295. package/dist/features/tester/seed-data/categories-tester-seed-data.js +2 -2
  296. package/dist/features/tester/seed-data/events-tester-seed-data.js +5 -5
  297. package/dist/features/tester/seed-data/orders-tester-seed-data.js +2 -2
  298. package/dist/features/tester/seed-data/products-tester-seed-data.js +48 -49
  299. package/dist/features/tester/seed-data/stores-tester-seed-data.js +3 -3
  300. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +275 -29
  301. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +48 -12
  302. package/dist/http/api-handler.js +2 -1
  303. package/dist/index.d.ts +24 -103
  304. package/dist/index.js +33 -141
  305. package/dist/jobs.d.ts +1 -1
  306. package/dist/jobs.js +1 -1
  307. package/dist/monitoring/browser.d.ts +0 -2
  308. package/dist/monitoring/browser.js +12 -1
  309. package/dist/monitoring/index.d.ts +0 -2
  310. package/dist/monitoring/index.js +0 -1
  311. package/dist/next/api/routeHandler.js +6 -3
  312. package/dist/next/index.d.ts +7 -2
  313. package/dist/next/index.js +36 -1
  314. package/dist/next/middleware/index.d.ts +0 -2
  315. package/dist/next/middleware/index.js +0 -1
  316. package/dist/next/routing/route-map.d.ts +8 -7
  317. package/dist/next/routing/route-map.js +10 -3
  318. package/dist/providers/db-firebase/admin.js +15 -1
  319. package/dist/providers/db-firebase/sieve.d.ts +22 -0
  320. package/dist/react/hooks/useListingTypeFlags.d.ts +1 -1
  321. package/dist/react/hooks/useListingTypeFlags.js +1 -1
  322. package/dist/react/hooks/usePendingFilters.js +5 -3
  323. package/dist/react/hooks/usePendingTable.js +15 -3
  324. package/dist/repositories/index.d.ts +0 -1
  325. package/dist/repositories/index.js +0 -1
  326. package/dist/schemas/registry.d.ts +197 -232
  327. package/dist/schemas/registry.js +5 -6
  328. package/dist/schemas/webhooks/razorpay.d.ts +50 -50
  329. package/dist/security/authorization.d.ts +9 -0
  330. package/dist/security/authorization.js +16 -2
  331. package/dist/security/browser.d.ts +1 -3
  332. package/dist/security/browser.js +5 -2
  333. package/dist/security/index.d.ts +1 -3
  334. package/dist/security/index.js +19 -2
  335. package/dist/security/pii-encrypt.js +16 -0
  336. package/dist/security/pii-schemas.d.ts +0 -14
  337. package/dist/security/pii-schemas.js +7 -15
  338. package/dist/seed/_helpers/category-forest.js +2 -2
  339. package/dist/seed/_helpers/media.d.ts +19 -0
  340. package/dist/seed/_helpers/media.js +75 -0
  341. package/dist/seed/blog-posts-seed-data.js +17 -17
  342. package/dist/seed/carousel-slides-seed-data.js +7 -7
  343. package/dist/seed/cart-seed-data.js +13 -13
  344. package/dist/seed/categories-seed-data.js +10 -10
  345. package/dist/seed/events-seed-data.js +24 -24
  346. package/dist/seed/factories/product.factory.js +4 -4
  347. package/dist/seed/factories/store.factory.js +3 -3
  348. package/dist/seed/factories/user.factory.js +2 -2
  349. package/dist/seed/grouped-listings-seed-data.js +11 -11
  350. package/dist/seed/homepage-sections-seed-data.js +2 -2
  351. package/dist/seed/index.d.ts +0 -1
  352. package/dist/seed/index.js +0 -1
  353. package/dist/seed/manifest.d.ts +1 -1
  354. package/dist/seed/manifest.js +0 -5
  355. package/dist/seed/notifications-seed-data.js +20 -1
  356. package/dist/seed/orders-seed-data.js +2 -2
  357. package/dist/seed/products-art-seed-data.js +16 -16
  358. package/dist/seed/products-auctions-seed-data.js +34 -34
  359. package/dist/seed/products-classifieds-seed-data.js +35 -37
  360. package/dist/seed/products-digital-codes-seed-data.js +20 -20
  361. package/dist/seed/products-live-items-seed-data.js +11 -11
  362. package/dist/seed/products-preorders-seed-data.js +13 -13
  363. package/dist/seed/products-prize-draws-seed-data.js +43 -43
  364. package/dist/seed/products-standard-seed-data.js +54 -54
  365. package/dist/seed/products-stickers-seed-data.js +15 -15
  366. package/dist/seed/reviews-seed-data.js +4 -4
  367. package/dist/seed/scammers-seed-data.js +3 -3
  368. package/dist/seed/site-settings-seed-data.js +11 -24
  369. package/dist/seed/store-extensions-seed-data.js +10 -10
  370. package/dist/seed/stores-seed-data.js +11 -11
  371. package/dist/seed/support-tickets-seed-data.d.ts +14 -0
  372. package/dist/seed/support-tickets-seed-data.js +20 -1
  373. package/dist/seed/users-seed-data.js +20 -20
  374. package/dist/server-entry.d.ts +0 -1
  375. package/dist/server-entry.js +0 -1
  376. package/dist/server.d.ts +9 -6
  377. package/dist/server.js +7 -13
  378. package/dist/styles.css +248 -68
  379. package/dist/tailwind-utilities.css +1 -1
  380. package/dist/tokens/themes/default-dark.js +2 -2
  381. package/dist/tokens/themes/default-light.js +2 -2
  382. package/dist/tokens/tokens.css +24 -4
  383. package/dist/ui/components/ActionRow.d.ts +50 -0
  384. package/dist/ui/components/ActionRow.js +53 -0
  385. package/dist/ui/components/ActionRow.style.css +145 -0
  386. package/dist/ui/components/Drawer.d.ts +10 -0
  387. package/dist/ui/components/Drawer.js +2 -1
  388. package/dist/ui/components/FilterDrawer.js +4 -1
  389. package/dist/ui/components/FilterDrawer.style.css +6 -12
  390. package/dist/ui/components/FormActionBar.js +14 -1
  391. package/dist/ui/components/FormActionBar.style.css +12 -11
  392. package/dist/ui/components/ListingLayout.js +8 -1
  393. package/dist/ui/components/ListingLayout.style.css +6 -13
  394. package/dist/ui/components/Modal.d.ts +15 -1
  395. package/dist/ui/components/Modal.js +17 -3
  396. package/dist/ui/components/QuickCreateModal.js +2 -2
  397. package/dist/ui/components/QuickCreateModal.style.css +78 -84
  398. package/dist/ui/components/RecordDetailModal.js +2 -2
  399. package/dist/ui/components/SideDrawer.d.ts +8 -0
  400. package/dist/ui/components/SideDrawer.js +2 -1
  401. package/dist/ui/components/SideDrawer.style.css +6 -3
  402. package/dist/ui/components/SideModal.d.ts +11 -1
  403. package/dist/ui/components/SideModal.js +3 -2
  404. package/dist/ui/components/SideModal.style.css +14 -0
  405. package/dist/ui/components/Typography.style.css +33 -24
  406. package/dist/ui/components/index.style.css +1 -0
  407. package/dist/ui/forms/FormShell.style.css +260 -311
  408. package/dist/ui/index.d.ts +2 -0
  409. package/dist/ui/index.js +1 -0
  410. package/dist/utils/index.d.ts +0 -1
  411. package/dist/utils/index.js +0 -1
  412. package/dist/validation/index.d.ts +1 -1
  413. package/dist/validation/index.js +1 -1
  414. package/dist/validation/schemas.d.ts +0 -19
  415. package/dist/validation/schemas.js +8 -19
  416. package/package.json +1 -1
  417. package/scripts/seed-cli.mjs +3 -6
@@ -31,21 +31,37 @@ export const counterOfferFormSchema = z.object({
31
31
  /**
32
32
  * Buyer-side Make-an-Offer form.
33
33
  *
34
- * A factory rather than a constant because both bounds depend on the listing:
35
- * the floor is `minOfferAmount(listedPrice, minOfferPercent)` and the ceiling is
36
- * the listed price itself. Both are re-checked server-side in `makeOffer` — this
37
- * exists so the buyer is told before submitting, using the SAME
38
- * `minOfferAmount()` the server uses, rather than a second rounding rule that
39
- * could disagree by a rupee.
34
+ * A factory rather than a constant because both bounds depend on the listing.
35
+ * It takes the already-resolved `OfferBounds` rather than recomputing them:
36
+ * `resolveOfferBounds()` is what `makeOffer` enforces, so anything this schema
37
+ * accepts, the server accepts. Deriving a second floor here is exactly how the
38
+ * forms came to advertise a 50% minimum the server rejected at 70 (see the
39
+ * offers config header).
40
+ *
41
+ * Three shapes, all from the same bounds object:
42
+ *
43
+ * - **Buy request** (`isBuyRequest`) — no cart on this type and no haggling
44
+ * opted in, so `min === max` and the only legal amount is the asking price.
45
+ * - **Offer, no cart** — floor to asking price *inclusive*: with no Add to Cart
46
+ * button, refusing a full-price offer would leave the buyer nowhere to go.
47
+ * - **Offer, cartable** — the original rule, ceiling exclusive of the price.
40
48
  */
41
49
  export function makeOfferFormSchema(opts) {
42
- const { listedPrice, minOffer, formatAmount } = opts;
50
+ const { listedPrice, bounds, formatAmount } = opts;
51
+ const amount = z.coerce
52
+ .number({ invalid_type_error: "Enter an offer amount." })
53
+ .positive("Offer amount must be greater than zero.");
54
+ const bounded = bounds.isBuyRequest
55
+ ? amount
56
+ .min(bounds.min, `This seller is not taking lower offers — the price is ${formatAmount(bounds.min)}.`)
57
+ .max(bounds.max, `This seller is not taking lower offers — the price is ${formatAmount(bounds.max)}.`)
58
+ : amount
59
+ .min(bounds.min, `Minimum offer is ${formatAmount(bounds.min)}.`)
60
+ .max(bounds.max, bounds.max >= listedPrice
61
+ ? `Your offer cannot exceed the asking price of ${formatAmount(listedPrice)}.`
62
+ : `Offer must be below the listed price of ${formatAmount(listedPrice)} — use Add to Cart to buy at that price.`);
43
63
  return z.object({
44
- offerAmount: z.coerce
45
- .number({ invalid_type_error: "Enter an offer amount." })
46
- .positive("Offer amount must be greater than zero.")
47
- .min(minOffer, `Minimum offer is ${formatAmount(minOffer)}.`)
48
- .max(listedPrice - 0.01, `Offer must be below the listed price of ${formatAmount(listedPrice)} — use Add to Cart to buy at that price.`),
64
+ offerAmount: bounded,
49
65
  buyerNote: z.string().max(300, "Keep your note under 300 characters.").optional(),
50
66
  });
51
67
  }
@@ -6,7 +6,7 @@ import { Button } from "../../ui/components/Button";
6
6
  import { IconButton } from "../../ui/components/IconButton";
7
7
  import { classNames } from "../../ui/style.helper";
8
8
  import { FORM_ACTION_META, FORM_ACTION_ID } from "../products/constants/action-defs";
9
- import { Div, Row, Span, Stack, Text } from "../../ui";
9
+ import { ActionRow, Div, Row, Span, Stack, Text } from "../../ui";
10
10
  /**
11
11
  * Carries the `schema` prop passed to `<FormShell>` down to any nested
12
12
  * `<SectionForm>` that doesn't declare its own `schema`/`SectionDef.schema` —
@@ -125,5 +125,5 @@ export function FormShell({ isOpen, onClose, title, breadcrumb, schema, isDirty
125
125
  return (_jsxs(FormSchemaContext.Provider, { value: schema, children: [_jsx(Div, { surface: "overlay-sm", className: "fixed inset-0 backdrop-blur-[2px]", style: { zIndex: "calc(var(--appkit-z-modal) - 1)" }, "aria-hidden": "true", onClick: attemptClose }), _jsxs(Div, { layout: "flex", ref: panelRef, role: "dialog", "aria-modal": "true", "aria-label": title, className: "fixed top-0 bottom-0 left-0 right-0 lg:left-[10%] lg:right-[10%] [flex-direction:column] bg-[var(--appkit-color-surface)]", shadow: "2xl", style: { zIndex: "var(--appkit-z-modal)" }, children: [_jsxs(Row, { gap: "sm", paddingX: "x-5", className: "flex-shrink-0 sticky top-0 z-10 border-b border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", padding: "y-sm", children: [previewMode ? (_jsxs(Button, { variant: "ghost", type: "button", onClick: () => setPreviewMode(false), "aria-label": "Back to edit", gap: "xs", rounded: "lg", textSize: "sm", textColor: "muted", className: "flex-shrink-0", children: [_jsx(ArrowLeft, { className: "w-4 h-4" }), _jsx(Span, { className: "hidden sm:inline", children: "Back to Edit" })] })) : (_jsx(IconButton, { type: "button", onClick: attemptClose, "aria-label": "Close", variant: "ghost", rounded: "lg", className: "text-[var(--appkit-color-text-muted)] flex-shrink-0", children: _jsx(X, { className: "w-5 h-5" }) })), _jsxs(Div, { className: "flex-1 min-w-0", children: [breadcrumb && (_jsx(Text, { className: "text-[var(--appkit-color-text-muted)] truncate mb-0.5", size: "xs", children: breadcrumb })), _jsx(Text, { className: "text-[var(--appkit-color-text)] truncate", size: "sm", weight: "semibold", children: previewMode ? `Preview — ${title}` : title })] }), _jsx(Row, { gap: "xs", className: "flex-shrink-0", children: previewMode ? null : (_jsxs(_Fragment, { children: [previewSlot && (_jsxs(Button, { variant: "outline", type: "button", onClick: () => setPreviewMode(true), gap: "xs", rounded: "lg", paddingX: "sm", textSize: "sm", textColor: "muted", children: [_jsx(Eye, { className: "w-4 h-4" }), _jsx(Span, { className: "hidden sm:inline", children: "Preview" })] })), onSaveDraft && (_jsx(Button, { variant: "outline", size: "sm", onClick: handleSaveDraft, disabled: isLoading || savingDraft || publishing, isLoading: savingDraft, children: saveLabel })), onPublish && (_jsx(Button, { variant: "primary", size: "sm", onClick: handlePublish, disabled: isLoading || savingDraft || publishing, isLoading: publishing, children: publishLabel }))] })) })] }), _jsxs(Div, { layout: "flex", className: `flex-1 ${__O.hidden}`, children: [sections && sections.length > 0 && !previewMode && (_jsx("nav", { "aria-label": "Form sections", className: "hidden lg:flex flex-col flex-shrink-0 w-48 border-r border-[var(--appkit-color-border)] py-[var(--appkit-space-4)] px-[var(--appkit-space-3)] gap-[var(--appkit-space-1)] overflow-y-auto", children: sections.map((sec) => (_jsx(Button, { variant: "ghost", type: "button", onClick: () => scrollToSection(sec.id), rounded: "lg", paddingX: "sm", paddingY: "xs", textSize: "sm", textColor: "muted", justify: "start", className: "text-left", children: sec.label }, sec.id))) })), sections && sections.length > 0 && !previewMode && (_jsx(Row, { gap: "px", className: `lg:hidden fixed top-[var(--form-shell-topbar-h,57px)] left-0 right-0 z-10 ${__O.xAuto} px-[1.25rem] bg-[var(--appkit-color-surface)] border-b border-[var(--appkit-color-border)]`, padding: "y-xs", children: sections.map((sec) => (_jsx(Button, { variant: "outline", type: "button", onClick: () => scrollToSection(sec.id), rounded: "full", paddingX: "sm", paddingY: "xs", textSize: "xs", textColor: "muted", className: "flex-shrink-0 hover:border-primary hover:text-primary whitespace-nowrap", children: sec.label }, sec.id))) })), _jsx(Div, { ref: bodyRef, className: classNames("flex-1 overflow-y-auto", sections && sections.length > 0 && !previewMode ? "pt-[var(--appkit-space-0)] lg:pt-[var(--appkit-space-0)]" : ""), children: previewMode && previewSlot ? (_jsxs(_Fragment, { children: [_jsxs(Row, { textSize: "sm", gap: "xs", className: "sticky top-0 z-10 bg-[var(--appkit-color-warning-surface)] border-b border-[var(--appkit-color-warning)] text-[var(--appkit-color-warning-text,var(--appkit-color-warning))]", paddingY: "y-xs", paddingX: "x-md", children: [_jsx(Eye, { className: "w-4 h-4 flex-shrink-0" }), _jsx(Span, { children: "Preview \u2014 not visible to buyers until published" })] }), _jsx(Div, { padding: "y-md", children: previewSlot() })] })) : splitPreview && previewSlot ? (
126
126
  // S-STORE-3-D — desktop 60/40 split: form left, preview right.
127
127
  // Below `lg`, falls back to single-column form (preview-as-modal via existing toggle).
128
- _jsxs(Div, { className: "lg:grid lg:grid-cols-[3fr_2fr] lg:gap-[1.5rem] lg:px-[1.5rem] lg:py-[1.5rem] lg:max-w-[1400px] lg:mx-auto", children: [_jsx(Div, { className: "max-w-3xl mx-auto px-[1.25rem] sm:px-[1.5rem] lg:max-w-none lg:px-[0] lg:py-[0]", padding: "y-lg", children: children }), _jsxs(Div, { className: `hidden lg:block sticky lg:top-4 lg:self-start lg:max-h-[calc(100vh-6rem)] ${__O.yAuto} border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-raised)] ${__P.p4}`, rounded: "lg", children: [_jsxs(Row, { textWeight: "semibold", textSize: "xs", gap: "xs", className: "mb-3 uppercase tracking-wide text-[var(--appkit-color-text-muted)]", children: [_jsx(Eye, { className: "w-3.5 h-3.5" }), _jsx(Span, { children: "Live preview" })] }), previewSlot()] })] })) : (_jsx(Div, { className: "max-w-3xl mx-auto px-[1.25rem] sm:px-[1.5rem]", padding: "y-lg", children: children })) })] }), !previewMode && renderBottomBar ? (renderBottomBar()) : !previewMode && (onSaveDraft || onPublish) ? (_jsxs(Stack, { gap: "none", className: "flex-shrink-0 sticky bottom-0 z-10 border-t border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", children: [footerTopSlot && (_jsx(Div, { paddingX: "x-5", padding: "t-sm", children: footerTopSlot })), _jsxs(Row, { justify: "between", paddingX: "x-5", padding: "y-sm", style: { paddingBottom: "calc(var(--appkit-space-3) + var(--keyboard-inset-height, 0px))" }, children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: attemptClose, disabled: isLoading, children: FORM_ACTION_META[FORM_ACTION_ID.DISCARD].label }), _jsxs(Row, { gap: "xs", children: [onSaveDraft && (_jsx(Button, { variant: "outline", size: "sm", onClick: handleSaveDraft, disabled: isLoading || savingDraft || publishing, isLoading: savingDraft, children: saveLabel })), onPublish && (_jsxs(Button, { variant: "primary", size: "sm", onClick: handlePublish, disabled: isLoading || savingDraft || publishing, isLoading: publishing, children: [publishLabel, " \u2192"] }))] })] })] })) : null] }), showUnsaved && (_jsxs(_Fragment, { children: [_jsx(Div, { surface: "overlay-lg", className: "fixed inset-0", style: { zIndex: "var(--appkit-z-confirm)" }, onClick: () => setShowUnsaved(false) }), _jsxs(Div, { className: "fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[90vw] max-w-sm bg-[var(--appkit-color-surface)]", shadow: "2xl", rounded: "xl", padding: "lg", style: { zIndex: "var(--appkit-z-confirm)" }, children: [_jsxs(Row, { gap: "sm", align: "start", className: "mb-4", children: [_jsx(Span, { layout: "flex-center", className: "flex-shrink-0 w-10 h-10 bg-[var(--appkit-color-warning-surface)]", rounded: "full", children: _jsx(AlertTriangle, { className: "w-5 h-5 text-[var(--appkit-color-warning)]" }) }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "text-[var(--appkit-color-text)]", weight: "semibold", children: "Unsaved changes" }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "You have unsaved changes. Leave without saving?" })] })] }), _jsxs(Row, { gap: "xs", justify: "end", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: () => setShowUnsaved(false), children: "Stay" }), _jsx(Button, { variant: "danger", size: "sm", onClick: () => { setShowUnsaved(false); onClose(); }, children: "Leave" })] })] })] }))] }));
128
+ _jsxs(Div, { className: "lg:grid lg:grid-cols-[3fr_2fr] lg:gap-[1.5rem] lg:px-[1.5rem] lg:py-[1.5rem] lg:max-w-[1400px] lg:mx-auto", children: [_jsx(Div, { className: "max-w-3xl mx-auto px-[1.25rem] sm:px-[1.5rem] lg:max-w-none lg:px-[0] lg:py-[0]", padding: "y-lg", children: children }), _jsxs(Div, { className: `hidden lg:block sticky lg:top-4 lg:self-start lg:max-h-[calc(100vh-6rem)] ${__O.yAuto} border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-raised)] ${__P.p4}`, rounded: "lg", children: [_jsxs(Row, { textWeight: "semibold", textSize: "xs", gap: "xs", className: "mb-3 uppercase tracking-wide text-[var(--appkit-color-text-muted)]", children: [_jsx(Eye, { className: "w-3.5 h-3.5" }), _jsx(Span, { children: "Live preview" })] }), previewSlot()] })] })) : (_jsx(Div, { className: "max-w-3xl mx-auto px-[1.25rem] sm:px-[1.5rem]", padding: "y-lg", children: children })) })] }), !previewMode && renderBottomBar ? (renderBottomBar()) : !previewMode && (onSaveDraft || onPublish) ? (_jsxs(Stack, { gap: "none", className: "flex-shrink-0 sticky bottom-0 z-10 border-t border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", children: [footerTopSlot && (_jsx(Div, { paddingX: "x-5", padding: "t-sm", children: footerTopSlot })), _jsxs(ActionRow, { align: "end", paddingX: "x-5", padding: "y-sm", style: { paddingBottom: "calc(var(--appkit-space-3) + var(--keyboard-inset-height, 0px))" }, anchor: _jsx(Button, { variant: "ghost", size: "sm", onClick: attemptClose, disabled: isLoading, children: FORM_ACTION_META[FORM_ACTION_ID.DISCARD].label }), children: [onSaveDraft && (_jsx(Button, { variant: "outline", size: "sm", onClick: handleSaveDraft, disabled: isLoading || savingDraft || publishing, isLoading: savingDraft, children: saveLabel })), onPublish && (_jsxs(Button, { variant: "primary", size: "sm", onClick: handlePublish, disabled: isLoading || savingDraft || publishing, isLoading: publishing, children: [publishLabel, " \u2192"] }))] })] })) : null] }), showUnsaved && (_jsxs(_Fragment, { children: [_jsx(Div, { surface: "overlay-lg", className: "fixed inset-0", style: { zIndex: "var(--appkit-z-confirm)" }, onClick: () => setShowUnsaved(false) }), _jsxs(Div, { className: "fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[90vw] max-w-sm bg-[var(--appkit-color-surface)]", shadow: "2xl", rounded: "xl", padding: "lg", style: { zIndex: "var(--appkit-z-confirm)" }, children: [_jsxs(Row, { gap: "sm", align: "start", className: "mb-4", children: [_jsx(Span, { layout: "flex-center", className: "flex-shrink-0 w-10 h-10 bg-[var(--appkit-color-warning-surface)]", rounded: "full", children: _jsx(AlertTriangle, { className: "w-5 h-5 text-[var(--appkit-color-warning)]" }) }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "text-[var(--appkit-color-text)]", weight: "semibold", children: "Unsaved changes" }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "You have unsaved changes. Leave without saving?" })] })] }), _jsxs(Row, { gap: "xs", justify: "end", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: () => setShowUnsaved(false), children: "Stay" }), _jsx(Button, { variant: "danger", size: "sm", onClick: () => { setShowUnsaved(false); onClose(); }, children: "Leave" })] })] })] }))] }));
129
129
  }
@@ -6,7 +6,7 @@ import { Button } from "../../ui/components/Button";
6
6
  import { IconButton } from "../../ui/components/IconButton";
7
7
  import { FormField } from "../../ui/components/FormField";
8
8
  import { Toggle } from "../../ui/components/Toggle";
9
- import { Div, Form, Row, Stack, Text } from "../../ui";
9
+ import { ActionRow, Div, Form, Row, Stack, Text } from "../../ui";
10
10
  import { FormShellContext, FormErrorSummary } from "../../ui/forms";
11
11
  import { useHandMode } from "../../_internal/client/hand-mode";
12
12
  function initValues(fields, defaults = {}) {
@@ -203,5 +203,5 @@ export function QuickFormDrawer({ isOpen, onClose, title, fields, defaultValues,
203
203
  : field.type === "select"
204
204
  ? "select"
205
205
  : "text", value: String(value ?? ""), onChange: (v) => set(field.name, field.type === "number" ? Number(v) : v), options: field.options, placeholder: field.placeholder, disabled: busy, error: errors[field.name] }), field.helperText && !errors[field.name] && (_jsx(Text, { className: "mt-1 text-[var(--appkit-color-text-muted)]", size: "xs", children: field.helperText }))] }, field.name));
206
- }), renderExtra?.(values, set)] }), _jsxs(Div, { className: "flex-shrink-0 border-t border-[var(--appkit-color-border)] bg-[var(--appkit-color-bg)]", children: [_jsx(Div, { paddingX: "x-md", padding: "t-sm", children: _jsx(FormErrorSummary, {}) }), _jsxs(Row, { padding: "md", align: "center", justify: "between", gap: "sm", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: handleClose, disabled: busy, children: cancelLabel }), _jsxs(Button, { variant: "primary", size: "sm", onClick: () => void handleSubmit(), disabled: busy, isLoading: busy, children: [submitLabel, " \u2192"] })] })] })] })] }) }));
206
+ }), renderExtra?.(values, set)] }), _jsxs(Div, { className: "flex-shrink-0 border-t border-[var(--appkit-color-border)] bg-[var(--appkit-color-bg)]", children: [_jsx(Div, { paddingX: "x-md", padding: "t-sm", children: _jsx(FormErrorSummary, {}) }), _jsxs(ActionRow, { padding: "md", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: handleClose, disabled: busy, children: cancelLabel }), _jsxs(Button, { variant: "primary", size: "sm", onClick: () => void handleSubmit(), disabled: busy, isLoading: busy, children: [submitLabel, " \u2192"] })] })] })] })] }) }));
207
207
  }
@@ -110,6 +110,14 @@ export interface SectionFormProps<T extends object = Record<string, JsonValue>>
110
110
  export declare function orderSections<T extends object>(sections: SectionDef<T>[]): SectionDef<T>[];
111
111
  /** `fieldName -> index into the ORDERED section list`. */
112
112
  export declare function buildFieldToSectionIndex<T extends object>(ordered: SectionDef<T>[]): Record<string, number>;
113
+ export interface UseSectionFormNavOptions {
114
+ /**
115
+ * `"{portal}:{surface}"` — persists this form's open set to the user's
116
+ * profile. Omit for in-memory-only, which is the default and what every
117
+ * pre-existing call site gets.
118
+ */
119
+ scope?: string;
120
+ }
113
121
  export interface UseSectionFormNavResult<T extends object> {
114
122
  /** Sections in render order — required first. */
115
123
  ordered: SectionDef<T>[];
@@ -126,8 +134,18 @@ export interface UseSectionFormNavResult<T extends object> {
126
134
  * `fieldToSectionIndex` into `useFormShellState(schema, nav)` and `openIds`
127
135
  * back into `<SectionForm>`.
128
136
  */
129
- export declare function useSectionFormNav<T extends object = Record<string, JsonValue>>(sections: SectionDef<T>[], values?: T): UseSectionFormNavResult<T>;
137
+ export declare function useSectionFormNav<T extends object = Record<string, JsonValue>>(sections: SectionDef<T>[], values?: T, opts?: UseSectionFormNavOptions): UseSectionFormNavResult<T>;
130
138
  /** DOM id for a section panel — kept in one place so nav and render agree. */
131
139
  export declare function sectionAnchorId(id: string): string;
140
+ /**
141
+ * Scroll a section into view and focus the first invalid control inside it.
142
+ *
143
+ * Deliberately module-level and shared by `goToSection` and `SectionForm`'s own
144
+ * submit handler: the deferred frame is the load-bearing part and is easy to
145
+ * drop when copying. The panel must MOUNT before it can be scrolled to —
146
+ * expanding is a state update, so without the deferral the scroll lands on a
147
+ * zero-height collapsed header and the offending field stays off-screen.
148
+ */
149
+ export declare function scrollToSection(id: string): void;
132
150
  export declare function SectionForm<T extends object = Record<string, JsonValue>>({ sections, values, onChange, onSubmit, submitLabel, isLoading, schema, openIds: controlledOpenIds, onOpenChange, expandMode, hideActions, destructiveAction, onValidationChange, onCancel, cancelLabel, bottomBar, }: SectionFormProps<T>): import("react").JSX.Element;
133
151
  export default SectionForm;
@@ -7,6 +7,7 @@ import { Badge } from "../../ui/components/Badge";
7
7
  import { Div, Row, Span, Stack } from "../../ui";
8
8
  import { FormShellContext } from "../../ui/forms/FormShell";
9
9
  import { useFormBottomActions } from "../layout/hooks/useFormBottomActions";
10
+ import { useSectionState } from "../account/hooks/useCollapsedSections";
10
11
  import { FormSchemaContext } from "./FormShell";
11
12
  /** Sections in render order: required first, original order preserved within each group. */
12
13
  export function orderSections(sections) {
@@ -25,36 +26,62 @@ export function buildFieldToSectionIndex(ordered) {
25
26
  * `fieldToSectionIndex` into `useFormShellState(schema, nav)` and `openIds`
26
27
  * back into `<SectionForm>`.
27
28
  */
28
- export function useSectionFormNav(sections, values) {
29
+ export function useSectionFormNav(sections, values, opts) {
29
30
  const ordered = useMemo(() => {
30
31
  const visible = values
31
32
  ? sections.filter((s) => (s.when ? s.when(values) : true))
32
33
  : sections;
33
34
  return orderSections(visible);
34
35
  }, [sections, values]);
35
- const [openIds, setOpenIds] = useState(() => orderSections(sections).filter((s) => s.required).map((s) => s.id));
36
+ const sectionIds = useMemo(() => ordered.map((s) => s.id), [ordered]);
37
+ const defaultOpen = useMemo(() => ordered.filter((s) => s.required).map((s) => s.id), [ordered]);
38
+ /*
39
+ * The open set is `useSectionState`, not a local `useState` — which is what
40
+ * makes "expand section 3, reload, find it still expanded" true. `scope: ""`
41
+ * (the default) is EPHEMERAL: identical in-memory behaviour to the previous
42
+ * `useState`, so the seven existing call sites are unchanged until each names
43
+ * a durable scope. Without that opt-out every unnamed form would share one
44
+ * `sectionState[""]` entry and fight over it.
45
+ */
46
+ const { openIds, setOpenIds, open } = useSectionState({
47
+ scope: opts?.scope ?? "",
48
+ sectionIds,
49
+ defaultOpen,
50
+ mode: "multi",
51
+ });
36
52
  const fieldToSectionIndex = useMemo(() => buildFieldToSectionIndex(ordered), [ordered]);
37
53
  const sectionMeta = useMemo(() => ordered.map((s) => ({ id: s.id, label: s.label, requiredFields: s.fields })), [ordered]);
38
54
  const goToSection = useCallback((index) => {
39
55
  const target = ordered[index];
40
56
  if (!target)
41
57
  return;
42
- setOpenIds((prev) => (prev.includes(target.id) ? prev : [...prev, target.id]));
43
- // The panel must MOUNT before it can be scrolled to — expanding is a state
44
- // update, so defer a frame. Without this the scroll lands on a zero-height
45
- // collapsed header and the offending field stays off-screen.
46
- requestAnimationFrame(() => {
47
- const el = document.getElementById(sectionAnchorId(target.id));
48
- el?.scrollIntoView({ behavior: "smooth", block: "start" });
49
- el?.querySelector("[aria-invalid='true']")?.focus();
50
- });
51
- }, [ordered]);
58
+ open(target.id);
59
+ scrollToSection(target.id);
60
+ }, [ordered, open]);
52
61
  return { ordered, openIds, setOpenIds, goToSection, fieldToSectionIndex, sectionMeta };
53
62
  }
54
63
  /** DOM id for a section panel — kept in one place so nav and render agree. */
55
64
  export function sectionAnchorId(id) {
56
65
  return `section-${id}`;
57
66
  }
67
+ /**
68
+ * Scroll a section into view and focus the first invalid control inside it.
69
+ *
70
+ * Deliberately module-level and shared by `goToSection` and `SectionForm`'s own
71
+ * submit handler: the deferred frame is the load-bearing part and is easy to
72
+ * drop when copying. The panel must MOUNT before it can be scrolled to —
73
+ * expanding is a state update, so without the deferral the scroll lands on a
74
+ * zero-height collapsed header and the offending field stays off-screen.
75
+ */
76
+ export function scrollToSection(id) {
77
+ if (typeof document === "undefined")
78
+ return;
79
+ requestAnimationFrame(() => {
80
+ const el = document.getElementById(sectionAnchorId(id));
81
+ el?.scrollIntoView({ behavior: "smooth", block: "start" });
82
+ el?.querySelector("[aria-invalid='true']")?.focus();
83
+ });
84
+ }
58
85
  export function SectionForm({ sections, values, onChange, onSubmit, submitLabel = "Save", isLoading = false, schema, openIds: controlledOpenIds, onOpenChange, expandMode = "multi", hideActions = false, destructiveAction, onValidationChange, onCancel, cancelLabel, bottomBar = true, }) {
59
86
  const inheritedSchema = useContext(FormSchemaContext);
60
87
  /*
@@ -66,6 +93,18 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
66
93
  const shellCtx = useContext(FormShellContext);
67
94
  const [fieldErrors, setFieldErrors] = useState({});
68
95
  const [uncontrolledOpenIds, setUncontrolledOpenIds] = useState(() => orderSections(sections).filter((s) => s.required).map((s) => s.id));
96
+ /*
97
+ * Sections held open because they hold a validation error, kept SEPARATE
98
+ * from `openIds`.
99
+ *
100
+ * Under `expandMode: "single"` the open set is an accordion — opening any
101
+ * other section replaces it — so a section revealed by a failed submit would
102
+ * be collapsed again by the user's very next click, hiding the error they
103
+ * were sent to fix. This set is unioned into the rendered state and is pruned
104
+ * per-section as each one's errors clear (below), so it can never wedge a
105
+ * section open once it is valid.
106
+ */
107
+ const [forceOpenIds, setForceOpenIds] = useState([]);
69
108
  const ordered = useMemo(() => orderSections(sections.filter((s) => (s.when ? s.when(values) : true))), [sections, values]);
70
109
  const openIds = controlledOpenIds ?? uncontrolledOpenIds;
71
110
  const setOpenIds = useCallback((ids) => {
@@ -79,6 +118,15 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
79
118
  // validator on every render and re-fire the effect below in a loop.
80
119
  const onValidationChangeRef = useRef(onValidationChange);
81
120
  onValidationChangeRef.current = onValidationChange;
121
+ /** Section ids holding at least one error, in render order. */
122
+ const erroringSectionIds = useCallback((errs) => {
123
+ const keys = Object.keys(errs);
124
+ if (keys.length === 0)
125
+ return [];
126
+ return ordered
127
+ .filter((s) => s.fields?.some((f) => keys.some((k) => k === f || k.startsWith(`${f}.`))))
128
+ .map((s) => s.id);
129
+ }, [ordered]);
82
130
  const runValidation = useCallback((nextValues) => {
83
131
  const activeSchema = schema ?? inheritedSchema;
84
132
  if (!activeSchema)
@@ -89,6 +137,7 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
89
137
  const parsed = activeSchema.safeParse(nextValues);
90
138
  if (parsed.success) {
91
139
  setFieldErrors({});
140
+ setForceOpenIds([]);
92
141
  onValidationChangeRef.current?.({}, fieldToSectionIndex);
93
142
  return;
94
143
  }
@@ -99,8 +148,16 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
99
148
  next[issue.path.map(String).join(".")] = issue.message;
100
149
  }
101
150
  setFieldErrors(next);
151
+ // Release a force-opened section as soon as ITS OWN errors clear, rather
152
+ // than waiting for the whole form to become valid — otherwise fixing
153
+ // section 1 leaves it pinned open while the user works through section 5.
154
+ const stillBad = new Set(erroringSectionIds(next));
155
+ setForceOpenIds((prev) => {
156
+ const kept = prev.filter((id) => stillBad.has(id));
157
+ return kept.length === prev.length ? prev : kept;
158
+ });
102
159
  onValidationChangeRef.current?.(next, fieldToSectionIndex);
103
- }, [schema, inheritedSchema, fieldToSectionIndex]);
160
+ }, [schema, inheritedSchema, fieldToSectionIndex, erroringSectionIds]);
104
161
  useEffect(() => {
105
162
  runValidation(values);
106
163
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -116,8 +173,27 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
116
173
  */
117
174
  const handleSubmit = useCallback(() => {
118
175
  shellCtx?.markSubmitAttempted();
176
+ /*
177
+ * Reveal the FIRST offending section — only the first. Expanding all five
178
+ * at once loses the user's place, and the per-section error badge already
179
+ * points at the rest.
180
+ *
181
+ * This is additive: `onSubmit` still fires exactly as before, so a caller
182
+ * that does its own parse or lets the server decide is unaffected. Blocking
183
+ * here would change behaviour for all thirteen call sites, and `fieldErrors`
184
+ * is only ever populated when a schema is present — a schema-less form
185
+ * finds nothing to expand and falls straight through.
186
+ */
187
+ const [firstBadId] = erroringSectionIds(fieldErrors);
188
+ if (firstBadId) {
189
+ setForceOpenIds((prev) => (prev.includes(firstBadId) ? prev : [...prev, firstBadId]));
190
+ if (!openIds.includes(firstBadId)) {
191
+ setOpenIds(expandMode === "single" ? [firstBadId] : [...openIds, firstBadId]);
192
+ }
193
+ scrollToSection(firstBadId);
194
+ }
119
195
  void onSubmit();
120
- }, [shellCtx, onSubmit]);
196
+ }, [shellCtx, onSubmit, erroringSectionIds, fieldErrors, openIds, setOpenIds, expandMode]);
121
197
  useFormBottomActions({
122
198
  onSubmit: handleSubmit,
123
199
  onCancel,
@@ -128,13 +204,22 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
128
204
  enabled: bottomBar && !hideActions,
129
205
  });
130
206
  const toggle = useCallback((id) => {
131
- const isOpen = openIds.includes(id);
207
+ /*
208
+ * A DIRECT click on the header always wins. `forceOpenIds` exists to
209
+ * survive an INDIRECT collapse — the accordion closing this section because
210
+ * a sibling was opened — not to make the header unresponsive. Leaving the
211
+ * id in the set here would mean clicking a section holding an error does
212
+ * visibly nothing, which reads as a broken control and is worse than the
213
+ * hidden error it was protecting against.
214
+ */
215
+ setForceOpenIds((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : prev));
216
+ const isOpen = openIds.includes(id) || forceOpenIds.includes(id);
132
217
  if (expandMode === "single") {
133
218
  setOpenIds(isOpen ? [] : [id]);
134
219
  return;
135
220
  }
136
221
  setOpenIds(isOpen ? openIds.filter((x) => x !== id) : [...openIds, id]);
137
- }, [openIds, expandMode, setOpenIds]);
222
+ }, [openIds, forceOpenIds, expandMode, setOpenIds]);
138
223
  const errorCountFor = useCallback((section) => {
139
224
  if (!section.fields?.length)
140
225
  return 0;
@@ -144,7 +229,9 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
144
229
  const isRequired = section.required === true;
145
230
  // A required section is always open — it holds the fields without
146
231
  // which the record cannot be saved, so hiding it is never useful.
147
- const isCollapsed = isRequired ? false : !openIds.includes(section.id);
232
+ const isCollapsed = isRequired
233
+ ? false
234
+ : !(openIds.includes(section.id) || forceOpenIds.includes(section.id));
148
235
  const errorCount = errorCountFor(section);
149
236
  return (_jsx(CollapsibleSection, { id: sectionAnchorId(section.id), title: _jsxs(Row, { align: "center", gap: "xs", children: [_jsx(Span, { weight: "semibold", size: "sm", children: section.heading ?? section.label }), isRequired && _jsx(Span, { size: "xs", color: "muted", children: "Required" })] }), isCollapsed: isCollapsed, onToggle: () => { if (!isRequired)
150
237
  toggle(section.id); }, keepMounted: section.keepMounted, renderHeaderExtra: errorCount > 0
@@ -53,6 +53,29 @@ export declare function groupFieldsBySection(fields: ResolvedField[]): Map<strin
53
53
  * make a clean 2-up, a `pair` next to a `quarter` makes neither.
54
54
  */
55
55
  export declare function packRows(fields: ResolvedField[]): ResolvedField[][];
56
+ /**
57
+ * The draft, minus every field whose `when` predicate says it is not on screen.
58
+ *
59
+ * ## Why this exists, and why it is not optional
60
+ *
61
+ * A `when` predicate hides a CONTROL; it does not clear the value behind it.
62
+ * So a form that renders conditionally and submits `{...draft}` sends whatever
63
+ * the user typed before they switched the option away — the control is gone
64
+ * from the screen and its value is still in the payload.
65
+ *
66
+ * That is not hypothetical. Four of the twenty dead controls found in the W3c
67
+ * sweep were exactly this: a cap typed on a percentage coupon persisted after
68
+ * switching to free-shipping; a lottery slot price written under uniform
69
+ * pricing; an AdSense slot shipped with a manually-authored ad. Each looked
70
+ * right on screen and saved something else.
71
+ *
72
+ * Build the payload from THIS, not from the draft, and hidden-implies-not-sent
73
+ * holds by construction rather than by remembering.
74
+ *
75
+ * Fields with no `when` are always kept, so a schema with no conditionals gets
76
+ * its draft back unchanged and the call is free.
77
+ */
78
+ export declare function visibleValues<T extends object>(schema: ZodTypeAny, values: T): Partial<T>;
56
79
  /**
57
80
  * Build the `SectionDef[]` a `<SectionForm>` renders.
58
81
  *
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  /*
4
4
  * WHY: A `SectionDef[]` written by hand is a second source of truth that has to
5
5
  * stay aligned with the schema that validates it. Nothing enforces the
@@ -34,14 +34,18 @@ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
34
34
  * @tag consumers:SectionForm callsites
35
35
  * @tag sideEffects:none
36
36
  */
37
+ import { groupRank, resolveSectionGroup } from "./form-sections";
37
38
  import { Fragment } from "react";
38
39
  import { FormGroup } from "../../ui/components/Form";
40
+ import { PaginatedSelect } from "../../ui/components/PaginatedSelect";
41
+ import { Stack } from "../../ui/components/Layout";
42
+ import { Text } from "../../ui/components/Typography";
39
43
  import { FieldInput } from "../../ui/forms/FieldInput";
40
44
  import { FieldSelect } from "../../ui/forms/FieldSelect";
41
45
  import { FieldTextarea } from "../../ui/forms/FieldTextarea";
42
46
  import { FieldCheckbox } from "../../ui/forms/FieldCheckbox";
43
47
  import { humaniseFieldName } from "./entity-form";
44
- import { ROW_COLUMNS, inferFieldUiMeta, objectShape, schemaEnumOptions, schemaIsOptional, ui, } from "./field-ui-meta";
48
+ import { ROW_COLUMNS, inferFieldUiMeta, objectShape, schemaEnumOptions, schemaIsOptional, ui, PAGINATED_SELECT_THRESHOLD, } from "./field-ui-meta";
45
49
  /**
46
50
  * Enum members as `<FieldSelect>` options.
47
51
  *
@@ -80,8 +84,21 @@ export function resolveFields(schema) {
80
84
  export function groupFieldsBySection(fields) {
81
85
  const bySection = new Map();
82
86
  for (const field of fields) {
83
- if (field.meta.derived)
84
- continue; // server-owned — never an input
87
+ // A derived field is never a bare input, but "never an input" and "never
88
+ // shown" are different questions and the tier answers them differently:
89
+ //
90
+ // t2-server / t3-mirror / t4-aggregate — the client cannot know the value
91
+ // at edit time (an id, a timestamp, a rollup). Omitted entirely; a blank
92
+ // read-only box for a value that does not exist yet is noise.
93
+ // t1-derive — the client CAN compute it, live, from a field on this same
94
+ // form (slug from title). Kept, and rendered read-only by renderField,
95
+ // so the user can see what will be saved and cannot desync it from its
96
+ // source by typing.
97
+ //
98
+ // Absent `tier` keeps the original behaviour of dropping the field: that is
99
+ // what every existing `derived: true` annotation was written against.
100
+ if (field.meta.derived && field.meta.tier !== "t1-derive")
101
+ continue;
85
102
  const key = field.meta.section || "advanced";
86
103
  const bucket = bySection.get(key);
87
104
  if (bucket)
@@ -128,6 +145,42 @@ export function packRows(fields) {
128
145
  flush();
129
146
  return rows;
130
147
  }
148
+ /**
149
+ * The draft, minus every field whose `when` predicate says it is not on screen.
150
+ *
151
+ * ## Why this exists, and why it is not optional
152
+ *
153
+ * A `when` predicate hides a CONTROL; it does not clear the value behind it.
154
+ * So a form that renders conditionally and submits `{...draft}` sends whatever
155
+ * the user typed before they switched the option away — the control is gone
156
+ * from the screen and its value is still in the payload.
157
+ *
158
+ * That is not hypothetical. Four of the twenty dead controls found in the W3c
159
+ * sweep were exactly this: a cap typed on a percentage coupon persisted after
160
+ * switching to free-shipping; a lottery slot price written under uniform
161
+ * pricing; an AdSense slot shipped with a manually-authored ad. Each looked
162
+ * right on screen and saved something else.
163
+ *
164
+ * Build the payload from THIS, not from the draft, and hidden-implies-not-sent
165
+ * holds by construction rather than by remembering.
166
+ *
167
+ * Fields with no `when` are always kept, so a schema with no conditionals gets
168
+ * its draft back unchanged and the call is free.
169
+ */
170
+ export function visibleValues(schema, values) {
171
+ const fields = resolveFields(schema);
172
+ if (!fields.some((f) => f.meta.when))
173
+ return values;
174
+ const out = {};
175
+ for (const field of fields) {
176
+ if (field.meta.when && !field.meta.when(values))
177
+ continue;
178
+ const key = field.name;
179
+ if (key in values)
180
+ out[key] = values[key];
181
+ }
182
+ return out;
183
+ }
131
184
  /**
132
185
  * Build the `SectionDef[]` a `<SectionForm>` renders.
133
186
  *
@@ -140,12 +193,27 @@ export function buildSectionsFromSchema(schema, opts = {}) {
140
193
  const bySection = groupFieldsBySection(fields);
141
194
  const declarationOrder = [...bySection.keys()];
142
195
  const explicitOrder = opts.sectionOrder ?? [];
196
+ /**
197
+ * Order is: required first, then an explicit `sectionOrder` if the caller
198
+ * gave one, then the section's GROUP band, then declaration order.
199
+ *
200
+ * The group band is what makes "required → additional → … → least important
201
+ * last" hold without every form maintaining its own `sectionOrder`. It cannot
202
+ * come from the section id, because ids are free-form and form-specific —
203
+ * see the note on `FieldUiMeta.group`. An explicit `sectionOrder` still wins,
204
+ * so a form with a genuine bespoke sequence keeps it.
205
+ */
143
206
  const rank = (id) => {
144
207
  const bucket = bySection.get(id) ?? [];
145
208
  if (bucket.some((f) => f.meta.sectionRequired))
146
- return -1;
209
+ return -1000;
147
210
  const i = explicitOrder.indexOf(id);
148
- return i === -1 ? explicitOrder.length + declarationOrder.indexOf(id) : i;
211
+ if (i !== -1)
212
+ return i;
213
+ const group = resolveSectionGroup(id, bucket.find((f) => f.meta.group)?.meta.group);
214
+ // Band first, declaration order inside the band. The multiplier just has to
215
+ // exceed any plausible section count in one form.
216
+ return explicitOrder.length + groupRank(group) * 1000 + declarationOrder.indexOf(id);
149
217
  };
150
218
  return declarationOrder
151
219
  .slice()
@@ -189,6 +257,21 @@ function renderField(field, ctx, opts) {
189
257
  const label = meta.label ?? humaniseFieldName(name);
190
258
  const value = ctx.values[name];
191
259
  const set = (v) => ctx.onChange({ [name]: v });
260
+ /*
261
+ * A t1-derive field — a slug computed from a title — is a live PREVIEW of
262
+ * what will be saved, not an input.
263
+ *
264
+ * Read-only rather than merely discouraged, because the two are only equal
265
+ * while the user behaves: an editable slug beside its source lets them be
266
+ * desynced, and slug immutability after creation is this codebase's
267
+ * established convention (categories, brands, bundles — Root Cause #39).
268
+ * `required` is deliberately NOT passed on: the user cannot fill it, so
269
+ * marking it required would put a permanent asterisk on a field they are not
270
+ * allowed to touch, and the error summary would name it.
271
+ */
272
+ if (meta.derived) {
273
+ return (_jsx(FieldInput, { name: name, label: label, hint: meta.help ?? "Generated automatically — not editable.", readOnly: true, value: typeof value === "string" ? value : value == null ? "" : String(value), onChange: () => { } }, name));
274
+ }
192
275
  switch (meta.kind) {
193
276
  case "toggle":
194
277
  return (_jsx(FieldCheckbox, { name: name, label: label, hint: meta.help, checked: Boolean(value), onChange: set }, name));
@@ -196,7 +279,18 @@ function renderField(field, ctx, opts) {
196
279
  return (_jsx(FieldTextarea, { name: name, label: label, hint: meta.help, required: required, value: typeof value === "string" ? value : "", onChange: set }, name));
197
280
  case "select": {
198
281
  const options = opts.options?.[name] ?? selectOptions(field.schema) ?? [];
199
- return (_jsx(FieldSelect, { name: name, label: label, hint: meta.help === "paginated" ? undefined : meta.help, required: required, value: typeof value === "string" ? value : "", options: options, onChange: set }, name));
282
+ /*
283
+ * More than five options and a native dropdown is unscannable — the
284
+ * standing rule in CLAUDE.md § UI Primitive Rules. Decided here, by
285
+ * counting, so it holds for a caller-supplied `opts.options` list too:
286
+ * inference can only see a Zod enum, and the long lists (categories,
287
+ * brands, stores) all arrive through `opts.options`, which is exactly the
288
+ * case that most needs searching.
289
+ */
290
+ if (options.length > PAGINATED_SELECT_THRESHOLD) {
291
+ return (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "sm", weight: "medium", color: "muted", children: [label, required ? " *" : ""] }), _jsx(PaginatedSelect, { value: typeof value === "string" ? value : null, onChange: (v) => set(v ?? ""), options: options, ariaLabel: label, placeholder: `Select ${label.toLowerCase()}…` }), meta.help && _jsx(Text, { size: "xs", color: "muted", children: meta.help }), ctx.errors[name] && (_jsx(Text, { size: "xs", color: "error", role: "alert", children: ctx.errors[name] }))] }, name));
292
+ }
293
+ return (_jsx(FieldSelect, { name: name, label: label, hint: meta.help, required: required, value: typeof value === "string" ? value : "", options: options, onChange: set }, name));
200
294
  }
201
295
  case "number":
202
296
  return (_jsx(FieldInput, { name: name, type: "number", label: label, hint: meta.help, required: required, value: value == null ? "" : String(value), onChange: (v) => set(v === "" ? undefined : Number(v)) }, name));
@@ -211,6 +305,10 @@ function renderField(field, ctx, opts) {
211
305
  case "list":
212
306
  return (_jsx(FieldInput, { name: name, label: label, hint: meta.help ?? `Provide a renderer for "${name}" — this field needs a custom control.`, required: required, disabled: true, value: "" }, name));
213
307
  default:
214
- return (_jsx(FieldInput, { name: name, label: label, hint: meta.help, required: required, value: typeof value === "string" ? value : value == null ? "" : String(value), onChange: set }, name));
308
+ return (_jsx(FieldInput, { name: name,
309
+ // Defaults to "text". Without the passthrough a password field
310
+ // renders in PLAINTEXT, and an email field gets the wrong mobile
311
+ // keyboard and no autofill — see FieldUiMeta.inputType.
312
+ type: meta.inputType ?? "text", label: label, hint: meta.help, required: required, value: typeof value === "string" ? value : value == null ? "" : String(value), onChange: set }, name));
215
313
  }
216
314
  }
@@ -1,3 +1,4 @@
1
+ import type { FormSectionGroup } from "./form-sections";
1
2
  import type { ZodTypeAny } from "zod";
2
3
  import type { FormValues } from "../../schemas/types";
3
4
  /** How much of a row a field claims on desktop. Everything is 1-up on mobile. */
@@ -20,6 +21,22 @@ export interface FieldUiMeta {
20
21
  * handful of schemas and quietly mis-group the rest.
21
22
  */
22
23
  section: string;
24
+ /**
25
+ * Priority band this section belongs to — drives ORDER and default-open,
26
+ * never identity.
27
+ *
28
+ * Deliberately separate from `section`, which stays free-form. Collapsing the
29
+ * 58 section ids in use onto a closed 12-value set was measured first and
30
+ * would have MERGED sections in 7 files: the scam report's `who` + `what` +
31
+ * `declare` into one blob, coupons' `limits` + `validity`, blog's `content` +
32
+ * `seo`. The id is the rendered unit and supplies the visible heading (144 of
33
+ * 228 annotations have no `sectionLabel`, so `humaniseFieldName(id)` IS their
34
+ * heading); the group is only "how early does this appear".
35
+ *
36
+ * Omit it and `SECTION_GROUP_DEFAULTS` resolves one from the id. A brand-new
37
+ * id with no default must declare its own.
38
+ */
39
+ group?: FormSectionGroup;
23
40
  /** Human label for the section. The first field to declare it wins. */
24
41
  sectionLabel?: string;
25
42
  /** Marks its section required-first, open on mount, non-collapsible. */
@@ -64,6 +81,19 @@ export interface FieldUiMeta {
64
81
  tier?: CalculatedTier;
65
82
  label?: string;
66
83
  help?: string;
84
+ /**
85
+ * The native `<input type>` for a `kind: "text"` field.
86
+ *
87
+ * `FieldKind` deliberately stays a small set of CONTROL kinds — it picks
88
+ * which primitive renders, and `email`/`password`/`tel`/`url` all render the
89
+ * same `<FieldInput>`. What they change is the browser's keyboard, autofill
90
+ * and masking, which is a property of the field rather than of the control.
91
+ *
92
+ * 🛑 Without this the generator emits a bare `<FieldInput>` with no `type`,
93
+ * so a password field renders **in plaintext**. Any schema with a password,
94
+ * an email or a phone number must set it.
95
+ */
96
+ inputType?: "text" | "email" | "password" | "tel" | "url" | "search";
67
97
  }
68
98
  export declare const ui: {
69
99
  /** Attach metadata to a schema instance. Returns the SAME instance. */
@@ -126,6 +156,20 @@ export declare function objectShape(schema: ZodTypeAny): Record<string, ZodTypeA
126
156
  * `.register(ui, { section: "…" })` calls, not 13 full metadata objects.
127
157
  */
128
158
  export declare function inferFieldUiMeta(name: string, schema: ZodTypeAny): Partial<FieldUiMeta>;
159
+ /**
160
+ * Above this many options a select becomes a searchable `<PaginatedSelect>`.
161
+ * The standing UI rule (CLAUDE.md § UI Primitive Rules): more than five options
162
+ * and a plain dropdown is unscannable.
163
+ *
164
+ * 🛑 This is the WHOLE mechanism — there is deliberately no `paginated` flag on
165
+ * `FieldUiMeta`. It used to be smuggled through `help` as the literal string
166
+ * `"paginated"`, which broke in both directions: the assignment was
167
+ * `meta.help ?? "paginated"`, so a select that had real help text never became
168
+ * paginated; and the renderer had to blank `help` to avoid printing the
169
+ * sentinel, so one that DID become paginated could never show help. Counting
170
+ * the options at render time needs neither.
171
+ */
172
+ export declare const PAGINATED_SELECT_THRESHOLD = 5;
129
173
  /**
130
174
  * The field's final metadata: inference first, explicit registration on top.
131
175
  *