@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.
- package/dist/_internal/client/features/layout/useSidebarSearch.d.ts +21 -5
- package/dist/_internal/client/features/layout/useSidebarSearch.js +27 -6
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.d.ts +23 -3
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +106 -87
- package/dist/_internal/client/features/lottery/LotteryPullForm.d.ts +16 -5
- package/dist/_internal/client/features/lottery/LotteryPullForm.js +65 -40
- package/dist/_internal/server/features/auth/permissions.d.ts +2 -0
- package/dist/_internal/server/features/auth/permissions.js +3 -2
- package/dist/_internal/server/features/categories/data.js +15 -4
- package/dist/_internal/server/features/classified/index.d.ts +0 -1
- package/dist/_internal/server/features/classified/index.js +0 -1
- package/dist/_internal/server/features/digital-code/index.d.ts +0 -1
- package/dist/_internal/server/features/digital-code/index.js +0 -1
- package/dist/_internal/server/features/live/index.d.ts +0 -1
- package/dist/_internal/server/features/live/index.js +0 -1
- package/dist/_internal/server/features/products/data.js +15 -5
- package/dist/_internal/server/features/products/list-public.d.ts +9 -0
- package/dist/_internal/server/features/products/list-public.js +95 -7
- package/dist/_internal/server/features/site-settings/adapters.d.ts +2 -2
- package/dist/_internal/server/features/site-settings/adapters.js +8 -5
- package/dist/_internal/server/features/tester/visibility.d.ts +2 -1
- package/dist/_internal/server/features/tester/visibility.js +2 -2
- package/dist/_internal/server/functions/scheduled.d.ts +3 -1
- package/dist/_internal/server/functions/scheduled.js +27 -1
- package/dist/_internal/server/jobs/core/hardBanCascade.js +4 -0
- package/dist/_internal/server/jobs/core/index.d.ts +2 -0
- package/dist/_internal/server/jobs/core/index.js +2 -0
- package/dist/_internal/server/jobs/core/pageViewPrune.d.ts +10 -0
- package/dist/_internal/server/jobs/core/pageViewPrune.js +44 -0
- package/dist/_internal/server/jobs/core/pageViewRollup.d.ts +2 -0
- package/dist/_internal/server/jobs/core/pageViewRollup.js +87 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/index.js +2 -0
- package/dist/_internal/server/jobs/handlers/pageViewPrune.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/pageViewPrune.js +2 -0
- package/dist/_internal/server/jobs/handlers/pageViewRollup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/pageViewRollup.js +2 -0
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +69 -22
- package/dist/_internal/shared/checkout/rules/classified.rule.d.ts +9 -4
- package/dist/_internal/shared/checkout/rules/classified.rule.js +7 -2
- package/dist/_internal/shared/features/layout/matchesNavQuery.d.ts +14 -0
- package/dist/_internal/shared/features/layout/matchesNavQuery.js +72 -0
- package/dist/_internal/shared/features/layout/types.d.ts +37 -1
- package/dist/_internal/shared/features/layout/types.js +19 -1
- package/dist/_internal/shared/features/notifications/action-url.d.ts +9 -1
- package/dist/_internal/shared/features/notifications/action-url.js +47 -0
- package/dist/_internal/shared/features/offers/config.d.ts +65 -0
- package/dist/_internal/shared/features/offers/config.js +73 -2
- package/dist/_internal/shared/features/orders/schema.d.ts +19 -1
- package/dist/_internal/shared/features/orders/schema.js +22 -2
- package/dist/_internal/shared/features/site-settings/types.d.ts +12 -3
- package/dist/_internal/shared/listing-types/capabilities.d.ts +8 -2
- package/dist/_internal/shared/listing-types/capabilities.js +6 -1
- package/dist/_internal/shared/listing-types/classified/ctas.js +6 -1
- package/dist/_internal/shared/listing-types/feature-flags.d.ts +10 -5
- package/dist/_internal/shared/listing-types/feature-flags.js +11 -6
- package/dist/client.d.ts +37 -20
- package/dist/client.js +36 -14
- package/dist/configs/next.js +10 -2
- package/dist/constants/api-endpoints.d.ts +18 -54
- package/dist/constants/api-endpoints.js +6 -26
- package/dist/constants/field-names.d.ts +6 -24
- package/dist/constants/field-names.js +10 -27
- package/dist/constants/geo/countries.d.ts +50 -0
- package/dist/constants/geo/countries.js +129 -0
- package/dist/constants/geo/subdivisions.d.ts +13 -0
- package/dist/constants/geo/subdivisions.js +104 -0
- package/dist/constants/index.d.ts +2 -2
- package/dist/constants/index.js +2 -2
- package/dist/constants/table-keys.d.ts +24 -0
- package/dist/constants/table-keys.js +28 -0
- package/dist/contracts/extend.d.ts +0 -9
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/realtime-session.js +7 -0
- package/dist/core/browser.d.ts +1 -1
- package/dist/core/browser.js +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/site-config.d.ts +0 -3
- package/dist/core/site-config.js +0 -3
- package/dist/errors/messages.d.ts +0 -8
- package/dist/errors/messages.js +0 -9
- package/dist/features/about/schemas/index.d.ts +8 -8
- package/dist/features/account/components/AddressFilters.js +32 -11
- package/dist/features/account/components/AddressForm.d.ts +26 -25
- package/dist/features/account/components/AddressForm.js +81 -103
- package/dist/features/account/components/AddressesIndexListing.js +8 -4
- package/dist/features/account/components/UserSidebar.d.ts +3 -4
- package/dist/features/account/components/UserSupportView.js +89 -58
- package/dist/features/account/components/index.d.ts +1 -7
- package/dist/features/account/components/index.js +0 -3
- package/dist/features/account/hooks/useAddresses.d.ts +4 -3
- package/dist/features/account/hooks/useAddresses.js +4 -6
- package/dist/features/account/hooks/useCollapsedSections.d.ts +51 -10
- package/dist/features/account/hooks/useCollapsedSections.js +211 -38
- package/dist/features/account/hooks/useProfile.d.ts +8 -0
- package/dist/features/account/index.d.ts +0 -3
- package/dist/features/account/index.js +0 -3
- package/dist/features/account/schemas/index.d.ts +0 -92
- package/dist/features/account/schemas/index.js +42 -20
- package/dist/features/addresses/components/AddressLocationFields.d.ts +13 -0
- package/dist/features/addresses/components/AddressLocationFields.js +41 -0
- package/dist/features/addresses/hooks/usePostalLookup.d.ts +17 -0
- package/dist/features/addresses/hooks/usePostalLookup.js +133 -0
- package/dist/features/addresses/schemas/address-form.d.ts +423 -0
- package/dist/features/addresses/schemas/address-form.js +212 -0
- package/dist/features/addresses/schemas/index.d.ts +1 -0
- package/dist/features/addresses/schemas/index.js +8 -0
- package/dist/features/admin/actions/admin-read-actions.d.ts +1 -1
- package/dist/features/admin/actions/index.d.ts +0 -1
- package/dist/features/admin/actions/index.js +0 -1
- package/dist/features/admin/actions/notification-actions.js +1 -41
- package/dist/features/admin/components/AdminActionIndexView.d.ts +32 -0
- package/dist/features/admin/components/AdminActionIndexView.js +128 -0
- package/dist/features/admin/components/AdminAdEditorView.js +159 -163
- package/dist/features/admin/components/AdminAddressEditorView.js +97 -55
- package/dist/features/admin/components/AdminBrandEditorView.d.ts +15 -0
- package/dist/features/admin/components/AdminBrandEditorView.js +113 -77
- package/dist/features/admin/components/AdminBundleEditorView.d.ts +19 -9
- package/dist/features/admin/components/AdminBundleEditorView.js +136 -168
- package/dist/features/admin/components/AdminCategoryEditorView.js +120 -43
- package/dist/features/admin/components/AdminCouponEditorView.js +145 -109
- package/dist/features/admin/components/AdminEmployeeEditorView.js +125 -56
- package/dist/features/admin/components/AdminFaqEditorView.js +112 -58
- package/dist/features/admin/components/AdminFeatureEditorView.js +108 -68
- package/dist/features/admin/components/AdminNavEditorView.js +69 -44
- package/dist/features/admin/components/AdminOrderEditorView.js +47 -10
- package/dist/features/admin/components/AdminPrizeDrawsView.js +16 -2
- package/dist/features/admin/components/AdminSidebar.d.ts +11 -7
- package/dist/features/admin/components/AdminSiteSettingsView.js +37 -37
- package/dist/features/admin/components/AdminStoreEditorView.js +75 -52
- package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +71 -30
- package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +95 -65
- package/dist/features/admin/components/BrandQuickCreateForm.d.ts +10 -0
- package/dist/features/admin/components/BrandQuickCreateForm.js +49 -26
- package/dist/features/admin/components/CategoryQuickCreateForm.d.ts +11 -0
- package/dist/features/admin/components/CategoryQuickCreateForm.js +48 -26
- package/dist/features/admin/components/index.d.ts +2 -2
- package/dist/features/admin/components/index.js +1 -1
- package/dist/features/admin/constants/site-settings-tabs.d.ts +120 -0
- package/dist/features/admin/constants/site-settings-tabs.js +152 -0
- package/dist/features/admin/index.d.ts +0 -3
- package/dist/features/admin/index.js +0 -2
- package/dist/features/admin/repository/analytics-rollup.repository.d.ts +35 -0
- package/dist/features/admin/repository/analytics-rollup.repository.js +30 -0
- package/dist/features/admin/schemas/admin-editor-forms.d.ts +56 -57
- package/dist/features/admin/schemas/admin-editor-forms.js +97 -54
- package/dist/features/admin/schemas/admin-ops-forms.d.ts +57 -5
- package/dist/features/admin/schemas/admin-ops-forms.js +104 -20
- package/dist/features/admin/schemas/admin-user-form.js +7 -0
- package/dist/features/admin/schemas/firestore.d.ts +48 -95
- package/dist/features/admin/schemas/firestore.js +15 -151
- package/dist/features/admin/schemas/nav-item-form.js +9 -2
- package/dist/features/admin/server.d.ts +0 -4
- package/dist/features/admin/server.js +0 -2
- package/dist/features/admin/types/index.d.ts +0 -7
- package/dist/features/analytics/repository/page-views.repository.d.ts +14 -0
- package/dist/features/analytics/repository/page-views.repository.js +24 -0
- package/dist/features/auth/actions/index.d.ts +0 -1
- package/dist/features/auth/actions/index.js +0 -1
- package/dist/features/auth/actions/realtime-token-actions.js +1 -12
- package/dist/features/auth/auth-helpers.js +5 -7
- package/dist/features/auth/components/Guards.js +2 -1
- package/dist/features/auth/components/LoginForm.d.ts +0 -2
- package/dist/features/auth/components/LoginForm.js +1 -3
- package/dist/features/auth/components/RegisterForm.js +62 -19
- package/dist/features/auth/permissions/constants.d.ts +27 -1
- package/dist/features/auth/role-predicates.d.ts +75 -7
- package/dist/features/auth/role-predicates.js +28 -2
- package/dist/features/auth/schemas/firestore.d.ts +22 -0
- package/dist/features/auth/schemas/index.d.ts +1 -1
- package/dist/features/auth/schemas/index.js +44 -8
- package/dist/features/auth/types/index.d.ts +14 -1
- package/dist/features/blog/api/route.js +10 -2
- package/dist/features/blog/components/BlogIndexListing.js +1 -1
- package/dist/features/blog/hooks/useBlog.js +2 -2
- package/dist/features/blog/schemas/index.d.ts +4 -0
- package/dist/features/blog/schemas/index.js +2 -0
- package/dist/features/blog/types/index.d.ts +1 -1
- package/dist/features/catalogue/components/CatalogueItemEditorView.d.ts +22 -0
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +112 -52
- package/dist/features/catalogue/schemas/validation.d.ts +4 -4
- package/dist/features/catalogue/schemas/validation.js +47 -10
- package/dist/features/categories/schemas/bundle-form.d.ts +142 -0
- package/dist/features/categories/schemas/bundle-form.js +149 -0
- package/dist/features/classified/components/ClassifiedDetailPageView.d.ts +5 -4
- package/dist/features/classified/components/ClassifiedDetailPageView.js +37 -16
- package/dist/features/consultation/components/ConsultationForm.js +1 -1
- package/dist/features/consultation/schemas/index.js +34 -10
- package/dist/features/corporate/schemas/index.js +36 -15
- package/dist/features/filters/FilterPanel.js +3 -3
- package/dist/features/filters/RangeFilter.d.ts +10 -0
- package/dist/features/filters/RangeFilter.js +41 -4
- package/dist/features/homepage/components/MarketplaceHomepageView.js +24 -5
- package/dist/features/homepage/hooks/useBlogArticles.js +1 -1
- package/dist/features/layout/BottomActions.js +84 -11
- package/dist/features/layout/BottomActionsContext.d.ts +1 -6
- package/dist/features/layout/BottomActionsContext.js +1 -1
- package/dist/features/layout/hooks/useBottomActions.js +2 -2
- package/dist/features/layout/hooks/useFormBottomActions.d.ts +12 -1
- package/dist/features/layout/hooks/useFormBottomActions.js +12 -6
- package/dist/features/lottery/schemas/config-write.d.ts +9 -5
- package/dist/features/lottery/schemas/config-write.js +69 -9
- package/dist/features/orders/schemas/buyer-forms.d.ts +6 -3
- package/dist/features/orders/schemas/buyer-forms.js +31 -3
- package/dist/features/orders/schemas/index.d.ts +2 -2
- package/dist/features/products/components/GroupSettingsPanel.js +3 -1
- package/dist/features/products/components/MakeOfferButton.d.ts +13 -2
- package/dist/features/products/components/MakeOfferButton.js +33 -11
- package/dist/features/products/components/ProductDetailActions.js +0 -1
- package/dist/features/products/components/ProductDetailPageView.d.ts +7 -3
- package/dist/features/products/components/ProductDetailPageView.js +8 -5
- package/dist/features/products/constants/action-defs.d.ts +1 -2
- package/dist/features/products/constants/action-defs.js +40 -43
- package/dist/features/products/repository/products.repository.d.ts +5 -1
- package/dist/features/products/repository/products.repository.js +23 -1
- package/dist/features/products/schemas/firestore.d.ts +7 -3
- package/dist/features/products/schemas/product-features.validators.js +3 -0
- package/dist/features/promotions/components/CouponCard.js +8 -7
- package/dist/features/scams/schemas/firestore.d.ts +15 -1
- package/dist/features/scams/schemas/report-form.d.ts +5 -5
- package/dist/features/scams/schemas/report-form.js +18 -3
- package/dist/features/search/action-index/derive.d.ts +49 -0
- package/dist/features/search/action-index/derive.js +121 -0
- package/dist/features/search/action-index/repository.d.ts +42 -0
- package/dist/features/search/action-index/repository.js +100 -0
- package/dist/features/search/action-index/settings-entries.d.ts +41 -0
- package/dist/features/search/action-index/settings-entries.js +268 -0
- package/dist/features/search/action-index/types.d.ts +81 -0
- package/dist/features/search/action-index/types.js +102 -0
- package/dist/features/search/action-index/validate-entry.d.ts +34 -0
- package/dist/features/search/action-index/validate-entry.js +114 -0
- package/dist/features/search/hooks/useNavSuggestions.d.ts +5 -1
- package/dist/features/search/hooks/useNavSuggestions.js +23 -3
- package/dist/features/search/repository/search.repository.js +3 -1
- package/dist/features/seller/actions/offer-actions.js +43 -24
- package/dist/features/seller/actions/seller-actions.js +29 -5
- package/dist/features/seller/components/CategoryChipPicker.d.ts +9 -0
- package/dist/features/seller/components/CategoryChipPicker.js +12 -0
- package/dist/features/seller/components/SellerAddressesView.js +5 -1
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +83 -43
- package/dist/features/seller/components/SellerArtView.d.ts +14 -7
- package/dist/features/seller/components/SellerArtView.js +4 -119
- package/dist/features/seller/components/SellerClassifiedView.d.ts +14 -7
- package/dist/features/seller/components/SellerClassifiedView.js +4 -124
- package/dist/features/seller/components/SellerCouponEditorView.d.ts +22 -1
- package/dist/features/seller/components/SellerCouponEditorView.js +91 -45
- package/dist/features/seller/components/SellerDigitalCodesView.d.ts +14 -7
- package/dist/features/seller/components/SellerDigitalCodesView.js +4 -126
- package/dist/features/seller/components/SellerListingTypeView.d.ts +66 -0
- package/dist/features/seller/components/SellerListingTypeView.js +277 -0
- package/dist/features/seller/components/SellerLiveView.d.ts +14 -7
- package/dist/features/seller/components/SellerLiveView.js +4 -126
- package/dist/features/seller/components/SellerSidebar.d.ts +3 -6
- package/dist/features/seller/components/SellerStickersView.d.ts +14 -7
- package/dist/features/seller/components/SellerStickersView.js +4 -119
- package/dist/features/seller/components/SellerSupportView.d.ts +31 -0
- package/dist/features/seller/components/SellerSupportView.js +100 -0
- package/dist/features/seller/components/index.d.ts +4 -8
- package/dist/features/seller/components/index.js +2 -4
- package/dist/features/seller/index.d.ts +0 -1
- package/dist/features/seller/index.js +0 -1
- package/dist/features/seller/schemas/coupon-form.d.ts +214 -0
- package/dist/features/seller/schemas/coupon-form.js +155 -23
- package/dist/features/seller/schemas/offer-forms.d.ts +16 -7
- package/dist/features/seller/schemas/offer-forms.js +28 -12
- package/dist/features/shell/FormShell.js +2 -2
- package/dist/features/shell/QuickFormDrawer.js +2 -2
- package/dist/features/shell/SectionForm.d.ts +19 -1
- package/dist/features/shell/SectionForm.js +104 -17
- package/dist/features/shell/build-sections.d.ts +23 -0
- package/dist/features/shell/build-sections.js +106 -8
- package/dist/features/shell/field-ui-meta.d.ts +44 -0
- package/dist/features/shell/field-ui-meta.js +14 -7
- package/dist/features/shell/form-sections.d.ts +46 -0
- package/dist/features/shell/form-sections.js +114 -0
- package/dist/features/shipments/components/AdminShipmentEditorView.js +87 -64
- package/dist/features/shipments/schemas/validation.d.ts +2 -2
- package/dist/features/shipments/schemas/validation.js +44 -13
- package/dist/features/store-extensions/schemas/custom-role-form.js +6 -2
- package/dist/features/store-extensions/schemas/listing-template-form.d.ts +7 -7
- package/dist/features/store-extensions/schemas/payout-method-form.js +14 -0
- package/dist/features/stores/actions/store-query-actions.js +3 -2
- package/dist/features/stores/api/[storeSlug]/auctions/route.js +3 -1
- package/dist/features/stores/api/[storeSlug]/products/route.js +3 -1
- package/dist/features/support/repository/support.repository.d.ts +21 -0
- package/dist/features/support/repository/support.repository.js +63 -0
- package/dist/features/support/schemas/firestore.d.ts +27 -2
- package/dist/features/support/schemas/firestore.js +2 -0
- package/dist/features/support/schemas/ticket-create-form.js +7 -0
- package/dist/features/tester/schemas/checklist-item-form.d.ts +43 -0
- package/dist/features/tester/schemas/checklist-item-form.js +109 -0
- package/dist/features/tester/seed-data/blog-tester-seed-data.js +2 -2
- package/dist/features/tester/seed-data/categories-tester-seed-data.js +2 -2
- package/dist/features/tester/seed-data/events-tester-seed-data.js +5 -5
- package/dist/features/tester/seed-data/orders-tester-seed-data.js +2 -2
- package/dist/features/tester/seed-data/products-tester-seed-data.js +48 -49
- package/dist/features/tester/seed-data/stores-tester-seed-data.js +3 -3
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +275 -29
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +48 -12
- package/dist/http/api-handler.js +2 -1
- package/dist/index.d.ts +24 -103
- package/dist/index.js +33 -141
- package/dist/jobs.d.ts +1 -1
- package/dist/jobs.js +1 -1
- package/dist/monitoring/browser.d.ts +0 -2
- package/dist/monitoring/browser.js +12 -1
- package/dist/monitoring/index.d.ts +0 -2
- package/dist/monitoring/index.js +0 -1
- package/dist/next/api/routeHandler.js +6 -3
- package/dist/next/index.d.ts +7 -2
- package/dist/next/index.js +36 -1
- package/dist/next/middleware/index.d.ts +0 -2
- package/dist/next/middleware/index.js +0 -1
- package/dist/next/routing/route-map.d.ts +8 -7
- package/dist/next/routing/route-map.js +10 -3
- package/dist/providers/db-firebase/admin.js +15 -1
- package/dist/providers/db-firebase/sieve.d.ts +22 -0
- package/dist/react/hooks/useListingTypeFlags.d.ts +1 -1
- package/dist/react/hooks/useListingTypeFlags.js +1 -1
- package/dist/react/hooks/usePendingFilters.js +5 -3
- package/dist/react/hooks/usePendingTable.js +15 -3
- package/dist/repositories/index.d.ts +0 -1
- package/dist/repositories/index.js +0 -1
- package/dist/schemas/registry.d.ts +197 -232
- package/dist/schemas/registry.js +5 -6
- package/dist/schemas/webhooks/razorpay.d.ts +50 -50
- package/dist/security/authorization.d.ts +9 -0
- package/dist/security/authorization.js +16 -2
- package/dist/security/browser.d.ts +1 -3
- package/dist/security/browser.js +5 -2
- package/dist/security/index.d.ts +1 -3
- package/dist/security/index.js +19 -2
- package/dist/security/pii-encrypt.js +16 -0
- package/dist/security/pii-schemas.d.ts +0 -14
- package/dist/security/pii-schemas.js +7 -15
- package/dist/seed/_helpers/category-forest.js +2 -2
- package/dist/seed/_helpers/media.d.ts +19 -0
- package/dist/seed/_helpers/media.js +75 -0
- package/dist/seed/blog-posts-seed-data.js +17 -17
- package/dist/seed/carousel-slides-seed-data.js +7 -7
- package/dist/seed/cart-seed-data.js +13 -13
- package/dist/seed/categories-seed-data.js +10 -10
- package/dist/seed/events-seed-data.js +24 -24
- package/dist/seed/factories/product.factory.js +4 -4
- package/dist/seed/factories/store.factory.js +3 -3
- package/dist/seed/factories/user.factory.js +2 -2
- package/dist/seed/grouped-listings-seed-data.js +11 -11
- package/dist/seed/homepage-sections-seed-data.js +2 -2
- package/dist/seed/index.d.ts +0 -1
- package/dist/seed/index.js +0 -1
- package/dist/seed/manifest.d.ts +1 -1
- package/dist/seed/manifest.js +0 -5
- package/dist/seed/notifications-seed-data.js +20 -1
- package/dist/seed/orders-seed-data.js +2 -2
- package/dist/seed/products-art-seed-data.js +16 -16
- package/dist/seed/products-auctions-seed-data.js +34 -34
- package/dist/seed/products-classifieds-seed-data.js +35 -37
- package/dist/seed/products-digital-codes-seed-data.js +20 -20
- package/dist/seed/products-live-items-seed-data.js +11 -11
- package/dist/seed/products-preorders-seed-data.js +13 -13
- package/dist/seed/products-prize-draws-seed-data.js +43 -43
- package/dist/seed/products-standard-seed-data.js +54 -54
- package/dist/seed/products-stickers-seed-data.js +15 -15
- package/dist/seed/reviews-seed-data.js +4 -4
- package/dist/seed/scammers-seed-data.js +3 -3
- package/dist/seed/site-settings-seed-data.js +11 -24
- package/dist/seed/store-extensions-seed-data.js +10 -10
- package/dist/seed/stores-seed-data.js +11 -11
- package/dist/seed/support-tickets-seed-data.d.ts +14 -0
- package/dist/seed/support-tickets-seed-data.js +20 -1
- package/dist/seed/users-seed-data.js +20 -20
- package/dist/server-entry.d.ts +0 -1
- package/dist/server-entry.js +0 -1
- package/dist/server.d.ts +9 -6
- package/dist/server.js +7 -13
- package/dist/styles.css +248 -68
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/default-dark.js +2 -2
- package/dist/tokens/themes/default-light.js +2 -2
- package/dist/tokens/tokens.css +24 -4
- package/dist/ui/components/ActionRow.d.ts +50 -0
- package/dist/ui/components/ActionRow.js +53 -0
- package/dist/ui/components/ActionRow.style.css +145 -0
- package/dist/ui/components/Drawer.d.ts +10 -0
- package/dist/ui/components/Drawer.js +2 -1
- package/dist/ui/components/FilterDrawer.js +4 -1
- package/dist/ui/components/FilterDrawer.style.css +6 -12
- package/dist/ui/components/FormActionBar.js +14 -1
- package/dist/ui/components/FormActionBar.style.css +12 -11
- package/dist/ui/components/ListingLayout.js +8 -1
- package/dist/ui/components/ListingLayout.style.css +6 -13
- package/dist/ui/components/Modal.d.ts +15 -1
- package/dist/ui/components/Modal.js +17 -3
- package/dist/ui/components/QuickCreateModal.js +2 -2
- package/dist/ui/components/QuickCreateModal.style.css +78 -84
- package/dist/ui/components/RecordDetailModal.js +2 -2
- package/dist/ui/components/SideDrawer.d.ts +8 -0
- package/dist/ui/components/SideDrawer.js +2 -1
- package/dist/ui/components/SideDrawer.style.css +6 -3
- package/dist/ui/components/SideModal.d.ts +11 -1
- package/dist/ui/components/SideModal.js +3 -2
- package/dist/ui/components/SideModal.style.css +14 -0
- package/dist/ui/components/Typography.style.css +33 -24
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/forms/FormShell.style.css +260 -311
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +1 -0
- package/dist/utils/index.d.ts +0 -1
- package/dist/utils/index.js +0 -1
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +1 -1
- package/dist/validation/schemas.d.ts +0 -19
- package/dist/validation/schemas.js +8 -19
- package/package.json +1 -1
- 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
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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,
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
84
|
-
|
|
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 -
|
|
209
|
+
return -1000;
|
|
147
210
|
const i = explicitOrder.indexOf(id);
|
|
148
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
*
|