@mohasinac/appkit 4.26.0 → 4.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/dist/_internal/client/features/lottery/LotteryAdminEditView.d.ts +16 -16
  2. package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +29 -6
  3. package/dist/_internal/client/features/lottery/LotteryListView.js +13 -1
  4. package/dist/_internal/server/features/bundles/data.d.ts +19 -0
  5. package/dist/_internal/server/features/bundles/data.js +32 -0
  6. package/dist/_internal/server/features/bundles/index.d.ts +1 -1
  7. package/dist/_internal/server/features/bundles/index.js +1 -1
  8. package/dist/_internal/server/features/checkout/actions.js +6 -4
  9. package/dist/_internal/server/features/classified/metadata.js +3 -1
  10. package/dist/_internal/server/features/digital-code/metadata.js +3 -1
  11. package/dist/_internal/server/features/live/metadata.js +3 -1
  12. package/dist/_internal/server/features/refunds/actions.js +3 -2
  13. package/dist/_internal/server/features/reviews/actions.js +3 -2
  14. package/dist/_internal/server/features/seo/sitemap.js +189 -40
  15. package/dist/_internal/server/jobs/core/listingProcessor.d.ts +0 -1
  16. package/dist/_internal/server/jobs/core/listingProcessor.js +19 -1
  17. package/dist/_internal/shared/actions/action-registry.js +14 -0
  18. package/dist/_internal/shared/checkout/order-math.d.ts +25 -3
  19. package/dist/_internal/shared/features/checkout/config.d.ts +3 -3
  20. package/dist/_internal/shared/features/checkout/config.js +6 -3
  21. package/dist/client.d.ts +32 -2
  22. package/dist/client.js +37 -2
  23. package/dist/constants/api-endpoints.d.ts +21 -3
  24. package/dist/constants/api-endpoints.js +14 -1
  25. package/dist/constants/field-names.d.ts +2 -2
  26. package/dist/constants/field-names.js +2 -2
  27. package/dist/core/newsletter.repository.js +14 -1
  28. package/dist/features/about/components/FAQPageView.js +1 -1
  29. package/dist/features/about/components/FeesView.js +1 -1
  30. package/dist/features/account/components/AddressForm.d.ts +2 -0
  31. package/dist/features/account/components/AddressForm.js +5 -1
  32. package/dist/features/account/components/UserBidsView.js +1 -1
  33. package/dist/features/account/components/UserSidebar.js +1 -1
  34. package/dist/features/account/components/UserSupportView.js +24 -2
  35. package/dist/features/account/hooks/useAddresses.d.ts +29 -1
  36. package/dist/features/account/hooks/useAddresses.js +4 -1
  37. package/dist/features/account/hooks/useProfile.d.ts +1 -1
  38. package/dist/features/account/hooks/useProfile.js +1 -1
  39. package/dist/features/addresses/repository/addresses.repository.d.ts +13 -0
  40. package/dist/features/addresses/repository/addresses.repository.js +19 -0
  41. package/dist/features/admin/actions/chat-actions.js +5 -1
  42. package/dist/features/admin/components/AdminAdsView.js +1 -1
  43. package/dist/features/admin/components/AdminBidsView.js +6 -1
  44. package/dist/features/admin/components/AdminCarouselGroupEditorView.d.ts +9 -1
  45. package/dist/features/admin/components/AdminCarouselGroupEditorView.js +42 -10
  46. package/dist/features/admin/components/AdminCarouselView.js +1 -1
  47. package/dist/features/admin/components/AdminFeatureEditorView.js +1 -1
  48. package/dist/features/admin/components/AdminGroupedListingsView.js +14 -2
  49. package/dist/features/admin/components/AdminSidebar.js +1 -1
  50. package/dist/features/admin/components/AdminViewCards.js +2 -2
  51. package/dist/features/admin/components/DataTable.js +2 -2
  52. package/dist/features/admin/schemas/firestore.js +9 -3
  53. package/dist/features/auctions/actions/bid-actions.js +45 -4
  54. package/dist/features/auctions/components/AuctionBottomActions.d.ts +21 -6
  55. package/dist/features/auctions/components/AuctionBottomActions.js +42 -11
  56. package/dist/features/auctions/components/AuctionDetailPageView.d.ts +8 -3
  57. package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
  58. package/dist/features/auctions/components/MarketplaceAuctionCard.js +4 -5
  59. package/dist/features/auctions/components/PlaceBidFormClient.d.ts +27 -2
  60. package/dist/features/auctions/components/PlaceBidFormClient.js +42 -20
  61. package/dist/features/auth/components/SocialAuthButtons.js +1 -1
  62. package/dist/features/auth/hooks/useAuth.d.ts +23 -1
  63. package/dist/features/auth/hooks/useAuth.js +27 -1
  64. package/dist/features/auth/permissions/constants.d.ts +1 -1
  65. package/dist/features/auth/permissions/constants.js +3 -0
  66. package/dist/features/auth/repository/session.repository.d.ts +16 -1
  67. package/dist/features/auth/repository/session.repository.js +23 -0
  68. package/dist/features/auth/repository/user.repository.js +13 -2
  69. package/dist/features/blog/components/BlogListView.js +2 -2
  70. package/dist/features/cart/components/CartPriceBreakdown.d.ts +11 -1
  71. package/dist/features/cart/components/CartPriceBreakdown.js +3 -3
  72. package/dist/features/cart/components/StoreAddonsPicker.d.ts +9 -0
  73. package/dist/features/cart/components/StoreAddonsPicker.js +15 -5
  74. package/dist/features/cart/components/index.d.ts +1 -1
  75. package/dist/features/cart/components/index.js +1 -1
  76. package/dist/features/cart/hooks/useGuestCartMerge.d.ts +8 -1
  77. package/dist/features/cart/hooks/useGuestCartMerge.js +2 -1
  78. package/dist/features/cart/utils/line-total.d.ts +25 -0
  79. package/dist/features/cart/utils/line-total.js +26 -0
  80. package/dist/features/catalogue/repository/catalogue.repository.d.ts +12 -0
  81. package/dist/features/catalogue/repository/catalogue.repository.js +15 -0
  82. package/dist/features/categories/components/CategoryTree.js +2 -2
  83. package/dist/features/contact/components/ContactForm.js +1 -1
  84. package/dist/features/copilot/hooks/useCopilotFeedback.d.ts +1 -1
  85. package/dist/features/copilot/hooks/useCopilotFeedback.js +1 -1
  86. package/dist/features/events/components/AdminEventEditorView.js +16 -9
  87. package/dist/features/events/components/AdminEventEntriesView.js +7 -13
  88. package/dist/features/events/components/EventRafflesSection.js +14 -2
  89. package/dist/features/events/components/EventStatusBadge.js +1 -1
  90. package/dist/features/events/components/event-entry-detail-fields.d.ts +24 -0
  91. package/dist/features/events/components/event-entry-detail-fields.js +13 -0
  92. package/dist/features/faq/components/ContactCTA.js +1 -1
  93. package/dist/features/faq/components/FAQHelpfulButtons.js +2 -2
  94. package/dist/features/faq/repository/faqs.repository.d.ts +16 -1
  95. package/dist/features/faq/repository/faqs.repository.js +64 -54
  96. package/dist/features/faq/schemas/firestore.d.ts +3 -2
  97. package/dist/features/faq/schemas/firestore.js +2 -2
  98. package/dist/features/faq/schemas/index.d.ts +3 -3
  99. package/dist/features/faq/schemas/index.js +1 -1
  100. package/dist/features/filters/FilterFacetSection.js +2 -2
  101. package/dist/features/forms/Toggle.js +1 -1
  102. package/dist/features/grouped/components/GroupedListingEditorView.d.ts +18 -0
  103. package/dist/features/grouped/components/GroupedListingEditorView.js +192 -0
  104. package/dist/features/homepage/components/AdvertisementBanner.js +2 -2
  105. package/dist/features/homepage/components/BrandsSection.js +1 -1
  106. package/dist/features/homepage/components/CTABannerSection.js +1 -1
  107. package/dist/features/homepage/components/HomepageCustomerReviewsSection.js +1 -1
  108. package/dist/features/homepage/components/HomepageSectionBoundary.d.ts +39 -0
  109. package/dist/features/homepage/components/HomepageSectionBoundary.js +49 -0
  110. package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
  111. package/dist/features/homepage/components/WelcomeSection.js +1 -1
  112. package/dist/features/homepage/lib/section-renderer.js +2 -1
  113. package/dist/features/layout/AppLayoutShell.js +5 -5
  114. package/dist/features/layout/BottomActions.js +5 -5
  115. package/dist/features/layout/BottomSheet.js +1 -1
  116. package/dist/features/layout/MainNavbar.js +1 -1
  117. package/dist/features/layout/NavbarLayout.js +1 -1
  118. package/dist/features/layout/TitleBarLayout.js +3 -3
  119. package/dist/features/layout/hooks/useFormBottomActions.d.ts +11 -1
  120. package/dist/features/layout/hooks/useFormBottomActions.js +16 -2
  121. package/dist/features/lottery/schemas/config-write.d.ts +141 -0
  122. package/dist/features/lottery/schemas/config-write.js +139 -0
  123. package/dist/features/media/MediaPickerModal.js +3 -1
  124. package/dist/features/media/MediaVideo.js +4 -2
  125. package/dist/features/media/components/MediaSlider.js +1 -1
  126. package/dist/features/orders/components/OrdersList.js +2 -2
  127. package/dist/features/payments/repository/payout.repository.d.ts +11 -1
  128. package/dist/features/payments/repository/payout.repository.js +14 -6
  129. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -2
  130. package/dist/features/pre-orders/components/PreOrderBadge.js +1 -1
  131. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -1
  132. package/dist/features/products/columns/productTableColumns.js +3 -3
  133. package/dist/features/products/components/CompareOverlay.js +1 -1
  134. package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
  135. package/dist/features/products/components/PrizeDrawEntryActions.js +2 -2
  136. package/dist/features/products/components/PrizeDrawsSection.js +5 -1
  137. package/dist/features/products/components/ProductDetailActions.js +11 -7
  138. package/dist/features/products/components/ProductDetailPageView.js +3 -3
  139. package/dist/features/products/components/ProductFeatureBadges.js +2 -2
  140. package/dist/features/products/components/ProductFeaturesSelector.js +1 -1
  141. package/dist/features/products/components/ProductFilters.js +15 -8
  142. package/dist/features/products/components/ProductGrid.js +8 -17
  143. package/dist/features/products/components/ProductTabs.js +7 -4
  144. package/dist/features/products/components/ProductTabsShell.js +2 -7
  145. package/dist/features/products/constants/action-defs.d.ts +21 -4
  146. package/dist/features/products/constants/action-defs.js +7 -3
  147. package/dist/features/products/hooks/useRelatedProducts.js +2 -1
  148. package/dist/features/products/repository/products.repository.d.ts +1 -1
  149. package/dist/features/products/repository/products.repository.js +27 -13
  150. package/dist/features/products/schemas/firestore.d.ts +3 -2
  151. package/dist/features/products/schemas/firestore.js +1 -1
  152. package/dist/features/reviews/actions/review-actions.d.ts +8 -1
  153. package/dist/features/reviews/actions/review-actions.js +14 -3
  154. package/dist/features/reviews/components/ReviewDetailShell.js +10 -4
  155. package/dist/features/reviews/components/ReviewModal.js +1 -1
  156. package/dist/features/reviews/repository/reviews.repository.d.ts +25 -1
  157. package/dist/features/reviews/repository/reviews.repository.js +53 -13
  158. package/dist/features/reviews/schemas/firestore.d.ts +6 -0
  159. package/dist/features/scams/components/ScamRegistryView.js +2 -2
  160. package/dist/features/search/components/Search.js +3 -3
  161. package/dist/features/search/components/SearchResultsSection.js +1 -1
  162. package/dist/features/seller/components/SellerAddressesView.js +3 -1
  163. package/dist/features/seller/components/SellerBidsView.js +6 -1
  164. package/dist/features/seller/components/SellerSidebar.js +1 -1
  165. package/dist/features/shell/SectionForm.d.ts +21 -1
  166. package/dist/features/shell/SectionForm.js +15 -3
  167. package/dist/features/shell/build-sections.js +10 -1
  168. package/dist/features/shell/field-ui-meta.d.ts +21 -0
  169. package/dist/features/shipments/components/AdminShipmentLotItemsView.js +23 -6
  170. package/dist/features/site-settings/components/ActionPermissionsManager.js +1 -1
  171. package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +21 -4
  172. package/dist/features/store-extensions/repository/store-extensions.repositories.js +24 -4
  173. package/dist/features/store-extensions/schemas/shipping-config-form.js +13 -0
  174. package/dist/features/store-extensions/schemas/store-category-form.js +1 -1
  175. package/dist/features/stores/actions/store-query-actions.js +25 -1
  176. package/dist/features/stores/components/InteractiveStoreCard.js +1 -1
  177. package/dist/features/stores/components/StoreHeader.js +1 -1
  178. package/dist/features/stores/components/StoreNavTabs.js +20 -8
  179. package/dist/features/support/repository/support.repository.d.ts +12 -0
  180. package/dist/features/support/repository/support.repository.js +15 -0
  181. package/dist/features/support/schemas/ticket-create-form.d.ts +33 -0
  182. package/dist/features/support/schemas/ticket-create-form.js +82 -0
  183. package/dist/features/tester/seed-data/products-tester-seed-data.js +108 -2
  184. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +235 -13
  185. package/dist/features/wishlist/components/WishlistHeartButton.d.ts +55 -0
  186. package/dist/features/wishlist/components/WishlistHeartButton.js +52 -0
  187. package/dist/features/wishlist/components/index.d.ts +2 -1
  188. package/dist/features/wishlist/components/index.js +1 -1
  189. package/dist/index.d.ts +10 -3
  190. package/dist/index.js +19 -4
  191. package/dist/monitoring/error-tracking.js +39 -5
  192. package/dist/monitoring/server-logger.js +70 -32
  193. package/dist/next/components/ErrorView.js +1 -1
  194. package/dist/next/routing/route-map.d.ts +39 -7
  195. package/dist/next/routing/route-map.js +34 -4
  196. package/dist/providers/db-firebase/base.repository.d.ts +30 -0
  197. package/dist/providers/db-firebase/base.repository.js +56 -9
  198. package/dist/react/ErrorBoundary.js +9 -1
  199. package/dist/react/contexts/SessionContext.js +6 -2
  200. package/dist/react/hooks/useAuthGate.d.ts +22 -0
  201. package/dist/react/hooks/useAuthGate.js +86 -7
  202. package/dist/schemas/registry.d.ts +3 -3
  203. package/dist/security/index.d.ts +1 -1
  204. package/dist/security/index.js +1 -1
  205. package/dist/security/pii-encrypt.d.ts +13 -5
  206. package/dist/security/pii-encrypt.js +132 -6
  207. package/dist/security/pii-schemas.d.ts +20 -4
  208. package/dist/security/pii-schemas.js +23 -5
  209. package/dist/seed/_helpers/product-search-txt.d.ts +18 -0
  210. package/dist/seed/_helpers/product-search-txt.js +33 -0
  211. package/dist/seed/faq-seed-data.d.ts +16 -0
  212. package/dist/seed/faq-seed-data.js +22 -1
  213. package/dist/seed/products-art-seed-data.js +49 -7
  214. package/dist/seed/products-auctions-seed-data.js +12 -4
  215. package/dist/seed/products-classifieds-seed-data.d.ts +5 -0
  216. package/dist/seed/products-classifieds-seed-data.js +65 -9
  217. package/dist/seed/products-digital-codes-seed-data.d.ts +5 -0
  218. package/dist/seed/products-digital-codes-seed-data.js +65 -9
  219. package/dist/seed/products-live-items-seed-data.d.ts +5 -0
  220. package/dist/seed/products-live-items-seed-data.js +33 -5
  221. package/dist/seed/products-preorders-seed-data.d.ts +1 -1
  222. package/dist/seed/products-preorders-seed-data.js +6 -2
  223. package/dist/seed/products-prize-draws-seed-data.d.ts +5 -0
  224. package/dist/seed/products-prize-draws-seed-data.js +57 -8
  225. package/dist/seed/products-standard-seed-data.js +6 -2
  226. package/dist/seed/products-stickers-seed-data.js +49 -7
  227. package/dist/seed/site-settings-seed-data.js +5 -3
  228. package/dist/seo/json-ld.d.ts +44 -2
  229. package/dist/seo/json-ld.js +57 -11
  230. package/dist/server.d.ts +1 -1
  231. package/dist/server.js +1 -1
  232. package/dist/styles.css +368 -90
  233. package/dist/tailwind-utilities.css +1 -1
  234. package/dist/tokens/index.d.ts +1 -1
  235. package/dist/tokens/index.js +1 -1
  236. package/dist/tokens/themes/default-dark.js +6 -0
  237. package/dist/tokens/themes/default-light.js +5 -0
  238. package/dist/tokens/tokens.css +84 -0
  239. package/dist/ui/DataTable.js +1 -1
  240. package/dist/ui/DataTable.style.css +3 -3
  241. package/dist/ui/components/BulkActionBar.style.css +1 -3
  242. package/dist/ui/components/Button.js +15 -6
  243. package/dist/ui/components/Dropdown.style.css +1 -4
  244. package/dist/ui/components/FilterChipGroup.js +1 -1
  245. package/dist/ui/components/FilterDrawer.style.css +1 -1
  246. package/dist/ui/components/Icon.d.ts +53 -0
  247. package/dist/ui/components/Icon.js +41 -0
  248. package/dist/ui/components/IconButton.d.ts +13 -0
  249. package/dist/ui/components/IconButton.js +13 -0
  250. package/dist/ui/components/IconButton.style.css +18 -10
  251. package/dist/ui/components/ListingLayout.style.css +2 -10
  252. package/dist/ui/components/ListingToolbar.js +6 -6
  253. package/dist/ui/components/Modal.js +1 -1
  254. package/dist/ui/components/PaginatedSelect.style.css +3 -3
  255. package/dist/ui/components/Pagination.style.css +1 -6
  256. package/dist/ui/components/RichTextEditor.js +1 -1
  257. package/dist/ui/components/RowActionMenu.style.css +1 -1
  258. package/dist/ui/components/SideDrawer.style.css +1 -1
  259. package/dist/ui/components/Slider.js +1 -1
  260. package/dist/ui/components/TabBarShell.d.ts +78 -0
  261. package/dist/ui/components/TabBarShell.js +95 -0
  262. package/dist/ui/components/Tabs.js +12 -8
  263. package/dist/ui/components/Tabs.style.css +249 -41
  264. package/dist/ui/components/ViewToggle.style.css +1 -5
  265. package/dist/ui/components/tabs-overflow.d.ts +17 -0
  266. package/dist/ui/components/tabs-overflow.js +142 -0
  267. package/dist/ui/icons/icon-registry.d.ts +38 -0
  268. package/dist/ui/icons/icon-registry.js +37 -0
  269. package/dist/ui/index.d.ts +6 -0
  270. package/dist/ui/index.js +3 -0
  271. package/dist/utils/index.d.ts +1 -1
  272. package/dist/utils/index.js +1 -1
  273. package/dist/utils/media-url.d.ts +27 -4
  274. package/dist/utils/media-url.js +37 -6
  275. package/dist/utils/search-txt.d.ts +71 -0
  276. package/dist/utils/search-txt.js +118 -0
  277. package/package.json +1 -1
@@ -0,0 +1,142 @@
1
+ "use client";
2
+ /*
3
+ * WHY: A tab strip that overflows its container needs a visible affordance,
4
+ * and a thin scrollbar is not one on a touch device or a trackpad. The
5
+ * arrows this hook drives are in-flow flex siblings of the tablist, which
6
+ * is what makes the hysteresis below mandatory rather than a nicety.
7
+ *
8
+ * WHAT: Measures whether the tab list overflows, whether it is scrolled to
9
+ * either end, and exposes a paging scroll. Consumed only by
10
+ * <TabBarShell>.
11
+ *
12
+ * EXPORTS: useTabsOverflow, TabsOverflowState
13
+ *
14
+ * @tag domain:ui
15
+ * @tag layer:ui
16
+ * @tag pattern:hook
17
+ * @tag access:client
18
+ * @tag consumers:TabBarShell
19
+ * @tag sideEffects:dom-measurement
20
+ */
21
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
22
+ /**
23
+ * Slack used by the hysteresis rule below, in px. Matches
24
+ * 2 x --appkit-tabs-arrow-size (2rem) — the width the two in-flow arrows
25
+ * reclaim from the list when they unmount.
26
+ */
27
+ const ARROW_SLACK_PX = 64;
28
+ /** Sub-pixel tolerance: scrollWidth/clientWidth can differ by <1px at some zooms. */
29
+ const EPSILON = 1;
30
+ const INITIAL = { hasArrows: false, atStart: true, atEnd: true };
31
+ function sameState(a, b) {
32
+ return a.hasArrows === b.hasArrows && a.atStart === b.atStart && a.atEnd === b.atEnd;
33
+ }
34
+ /**
35
+ * Drives the scroll-arrow affordances on a horizontally scrollable tab list.
36
+ *
37
+ * `activeKey` identifies the currently-selected tab. It exists solely to key
38
+ * the scroll-into-view effect, so the strip repositions when the selection
39
+ * changes and stays put while the user scrolls.
40
+ */
41
+ export function useTabsOverflow(listRef, activeKey) {
42
+ // Starts false and can only become true inside an effect, so the server HTML
43
+ // never contains arrows and always matches the first client render.
44
+ const [state, setState] = useState(INITIAL);
45
+ const stateRef = useRef(state);
46
+ stateRef.current = state;
47
+ const measure = useCallback(() => {
48
+ const el = listRef.current;
49
+ if (!el)
50
+ return;
51
+ const overflow = el.scrollWidth - el.clientWidth;
52
+ const previous = stateRef.current.hasArrows;
53
+ /*
54
+ * 🛑 HYSTERESIS — do not collapse this to a single threshold.
55
+ *
56
+ * The arrows are IN-FLOW, so mounting them shrinks the list's clientWidth
57
+ * by ~ARROW_SLACK_PX, which INCREASES the measured overflow. With one
58
+ * threshold a strip 10px wider than its container oscillates forever:
59
+ * hidden -> overflow 10 -> show -> overflow 74 -> still overflowing
60
+ * -> ... -> hide -> overflow -54 -> hide -> widen -> overflow 10 -> show
61
+ *
62
+ * Once the arrows are up they may only come down if the content would
63
+ * STILL fit after reclaiming their width, which by construction leaves
64
+ * overflow <= 0 afterwards. That makes the rule provably stable.
65
+ */
66
+ const hasArrows = previous ? overflow > ARROW_SLACK_PX : overflow > EPSILON;
67
+ const max = el.scrollWidth - el.clientWidth;
68
+ // Math.abs: scrollLeft is negative (or inverted) in RTL depending on engine.
69
+ const position = Math.abs(el.scrollLeft);
70
+ const next = {
71
+ hasArrows,
72
+ atStart: position <= EPSILON,
73
+ atEnd: max <= EPSILON || position >= max - EPSILON,
74
+ };
75
+ setState((current) => (sameState(current, next) ? current : next));
76
+ }, [listRef]);
77
+ /*
78
+ * Content width — a badge appearing, a locale swapping every label, a tab
79
+ * being added. Deliberately dependency-FREE so it runs after every render:
80
+ * none of those changes is expressible as a dep, and a ResizeObserver on a
81
+ * scroll container fires only for its own box, never for its content. The
82
+ * `sameState` guard in `measure` is what makes this safe — it only calls
83
+ * setState when a value actually changed, so there is no render loop. The
84
+ * cost is two layout READS per render (no writes, so no forced reflow).
85
+ */
86
+ useLayoutEffect(() => {
87
+ measure();
88
+ });
89
+ // Container width: viewport resize, sidebar collapse, drawer open.
90
+ useEffect(() => {
91
+ const el = listRef.current;
92
+ // jsdom does not implement ResizeObserver; appkit/tests/setup.ts stubs it,
93
+ // but guard anyway so the hook is safe in any non-DOM runtime.
94
+ if (!el || typeof ResizeObserver === "undefined")
95
+ return;
96
+ const observer = new ResizeObserver(() => measure());
97
+ observer.observe(el);
98
+ return () => observer.disconnect();
99
+ }, [listRef, measure]);
100
+ // atStart / atEnd. Passive: this listener never calls preventDefault.
101
+ useEffect(() => {
102
+ const el = listRef.current;
103
+ if (!el)
104
+ return;
105
+ const onScroll = () => measure();
106
+ el.addEventListener("scroll", onScroll, { passive: true });
107
+ return () => el.removeEventListener("scroll", onScroll);
108
+ }, [listRef, measure]);
109
+ // A web-font swap changes every label's width after first paint; without
110
+ // this the initial measurement is taken against the fallback font.
111
+ useEffect(() => {
112
+ let cancelled = false;
113
+ const fonts = document.fonts;
114
+ fonts?.ready?.then(() => {
115
+ if (!cancelled)
116
+ measure();
117
+ });
118
+ return () => {
119
+ cancelled = true;
120
+ };
121
+ }, [measure]);
122
+ /*
123
+ * Keep the selected tab reachable when it starts off-screen in the strip.
124
+ * Keyed on `activeKey` and NOT run every render — an
125
+ * unconditional scrollIntoView would yank the strip back while the user is
126
+ * mid-scroll, fighting their own input.
127
+ */
128
+ useEffect(() => {
129
+ const active = listRef.current?.querySelector('[aria-selected="true"]');
130
+ // `block: "nearest"` is essential — the default "start" scrolls the whole
131
+ // PAGE vertically whenever a tab bar sits below the fold.
132
+ active?.scrollIntoView?.({ block: "nearest", inline: "nearest" });
133
+ }, [listRef, activeKey]);
134
+ const scrollByPage = useCallback((direction) => {
135
+ const el = listRef.current;
136
+ // Smoothness comes from `scroll-behavior: smooth` in Tabs.style.css,
137
+ // which already has a prefers-reduced-motion override — no JS branch.
138
+ // `?.` because jsdom implements neither scrollBy nor scrollIntoView.
139
+ el?.scrollBy?.({ left: direction * el.clientWidth * 0.8 });
140
+ }, [listRef]);
141
+ return { ...state, scrollByPage };
142
+ }
@@ -53,6 +53,44 @@ export declare const ICONS: {
53
53
  };
54
54
  /** Every valid `iconKey`. A typo is a compile error, not a silent blank. */
55
55
  export type IconKey = keyof typeof ICONS;
56
+ /**
57
+ * The ONLY sanctioned glyph sizes.
58
+ *
59
+ * WHY: a sweep of `appkit/src` + `src` found ~217 icon sites each hand-picking
60
+ * a size, in two spellings with no canonical order (`h-4 w-4` ×104,
61
+ * `w-5 h-5` ×64, `h-3 w-3` ×23, `h-3.5 w-3.5` ×26) across 105 lucide-importing
62
+ * files and 63 with an inline `<svg>`. Nothing related a glyph's size to the
63
+ * control containing it, so a 14px heart could sit in a 44px button and read as
64
+ * broken — which is exactly the "the wishlist icon is very small" report.
65
+ *
66
+ * Pick by ROLE, not by eye:
67
+ *
68
+ * | key | px | use for |
69
+ * |------|----|-----------------------------------------------------|
70
+ * | `xs` | 12 | dense meta, inside a badge or a caption row |
71
+ * | `sm` | 14 | chrome inside a `size="sm"` control |
72
+ * | `md` | 16 | inline with body text; a `size="md"` control |
73
+ * | `lg` | 20 | a standalone icon-only control's glyph |
74
+ * | `xl` | 24 | hero, empty-state, feature tile |
75
+ * | `2xl`| 28 | chrome floating on an overlay (lightbox, carousel) |
76
+ *
77
+ * `2xl` exists because ten lightbox/carousel controls had independently and
78
+ * unanimously picked 28px — that agreement is a real tier, not ten mistakes.
79
+ * Overlay chrome sits over arbitrary imagery and needs more presence than
80
+ * inline UI.
81
+ *
82
+ * `<Button>` and `<IconButton>` derive their own icon size from these — prefer
83
+ * letting the control choose over passing a size by hand.
84
+ */
85
+ export declare const ICON_SIZE: {
86
+ readonly xs: "h-3 w-3";
87
+ readonly sm: "h-3.5 w-3.5";
88
+ readonly md: "h-4 w-4";
89
+ readonly lg: "h-5 w-5";
90
+ readonly xl: "h-6 w-6";
91
+ readonly "2xl": "h-7 w-7";
92
+ };
93
+ export type IconSizeKey = keyof typeof ICON_SIZE;
56
94
  /** Narrow an unknown string — for data that crosses a boundary the type cannot. */
57
95
  export declare function isIconKey(value: unknown): value is IconKey;
58
96
  /**
@@ -99,6 +99,43 @@ export const ICONS = {
99
99
  expand: ChevronDown,
100
100
  user: User,
101
101
  };
102
+ /**
103
+ * The ONLY sanctioned glyph sizes.
104
+ *
105
+ * WHY: a sweep of `appkit/src` + `src` found ~217 icon sites each hand-picking
106
+ * a size, in two spellings with no canonical order (`h-4 w-4` ×104,
107
+ * `w-5 h-5` ×64, `h-3 w-3` ×23, `h-3.5 w-3.5` ×26) across 105 lucide-importing
108
+ * files and 63 with an inline `<svg>`. Nothing related a glyph's size to the
109
+ * control containing it, so a 14px heart could sit in a 44px button and read as
110
+ * broken — which is exactly the "the wishlist icon is very small" report.
111
+ *
112
+ * Pick by ROLE, not by eye:
113
+ *
114
+ * | key | px | use for |
115
+ * |------|----|-----------------------------------------------------|
116
+ * | `xs` | 12 | dense meta, inside a badge or a caption row |
117
+ * | `sm` | 14 | chrome inside a `size="sm"` control |
118
+ * | `md` | 16 | inline with body text; a `size="md"` control |
119
+ * | `lg` | 20 | a standalone icon-only control's glyph |
120
+ * | `xl` | 24 | hero, empty-state, feature tile |
121
+ * | `2xl`| 28 | chrome floating on an overlay (lightbox, carousel) |
122
+ *
123
+ * `2xl` exists because ten lightbox/carousel controls had independently and
124
+ * unanimously picked 28px — that agreement is a real tier, not ten mistakes.
125
+ * Overlay chrome sits over arbitrary imagery and needs more presence than
126
+ * inline UI.
127
+ *
128
+ * `<Button>` and `<IconButton>` derive their own icon size from these — prefer
129
+ * letting the control choose over passing a size by hand.
130
+ */
131
+ export const ICON_SIZE = {
132
+ xs: "h-3 w-3",
133
+ sm: "h-3.5 w-3.5",
134
+ md: "h-4 w-4",
135
+ lg: "h-5 w-5",
136
+ xl: "h-6 w-6",
137
+ "2xl": "h-7 w-7",
138
+ };
102
139
  /** Narrow an unknown string — for data that crosses a boundary the type cannot. */
103
140
  export function isIconKey(value) {
104
141
  return typeof value === "string" && value in ICONS;
@@ -35,6 +35,10 @@ export type { ButtonProps } from "./components/Button";
35
35
  export { Button } from "./components/Button";
36
36
  export type { IconButtonProps } from "./components/IconButton";
37
37
  export { IconButton } from "./components/IconButton";
38
+ export type { IconProps, IconTone } from "./components/Icon";
39
+ export { Icon } from "./components/Icon";
40
+ export type { IconKey, IconSizeKey } from "./icons/icon-registry";
41
+ export { ICONS, ICON_SIZE, isIconKey, resolveIcon, } from "./icons/icon-registry";
38
42
  export type { TextLinkProps } from "./components/TextLink";
39
43
  export { TextLink } from "./components/TextLink";
40
44
  export type { TooltipProps } from "./components/Tooltip";
@@ -153,6 +157,8 @@ export type { NavigationLoaderProps } from "./components/NavigationLoader";
153
157
  export { NavigationLoader } from "./components/NavigationLoader";
154
158
  export type { TabsProps, TabsListProps, TabsTriggerProps, TabsContentProps, } from "./components/Tabs";
155
159
  export { Tabs, TabsList, TabsTrigger, TabsContent } from "./components/Tabs";
160
+ export type { TabBarShellProps, TabBarButtonProps, TabsNavSelectProps, } from "./components/TabBarShell";
161
+ export { TabBarShell, TabBarButton, TabsNavSelect } from "./components/TabBarShell";
156
162
  export type { FilterDrawerProps } from "./components/FilterDrawer";
157
163
  export { FilterDrawer } from "./components/FilterDrawer";
158
164
  export type { RowAction, RowActionMenuProps } from "./components/RowActionMenu";
package/dist/ui/index.js CHANGED
@@ -20,6 +20,8 @@ export { SiteMark } from "./components/SiteMark";
20
20
  export { Skeleton } from "./components/Skeleton";
21
21
  export { Button } from "./components/Button";
22
22
  export { IconButton } from "./components/IconButton";
23
+ export { Icon } from "./components/Icon";
24
+ export { ICONS, ICON_SIZE, isIconKey, resolveIcon, } from "./icons/icon-registry";
23
25
  export { TextLink } from "./components/TextLink";
24
26
  export { Tooltip } from "./components/Tooltip";
25
27
  export { Accordion, AccordionItem } from "./components/Accordion";
@@ -79,6 +81,7 @@ export { RoleBadge } from "./components/RoleBadge";
79
81
  export { SkipToMain } from "./components/SkipToMain";
80
82
  export { NavigationLoader } from "./components/NavigationLoader";
81
83
  export { Tabs, TabsList, TabsTrigger, TabsContent } from "./components/Tabs";
84
+ export { TabBarShell, TabBarButton, TabsNavSelect } from "./components/TabBarShell";
82
85
  export { FilterDrawer } from "./components/FilterDrawer";
83
86
  export { RowActionMenu } from "./components/RowActionMenu";
84
87
  export { PasswordStrengthIndicator } from "./components/PasswordStrengthIndicator";
@@ -17,5 +17,5 @@ export * from "./business-day";
17
17
  export * from "./schema-ui";
18
18
  export * from "./listing-params";
19
19
  export * from "./action-response";
20
- export * from "./search-tokens";
20
+ export * from "./search-txt";
21
21
  export { splitCartIntoOrderGroups, type OrderType, type OrderGroup, } from "../features/orders/utils/order-splitter";
@@ -17,6 +17,6 @@ export * from "./business-day";
17
17
  export * from "./schema-ui";
18
18
  export * from "./listing-params";
19
19
  export * from "./action-response";
20
- export * from "./search-tokens";
20
+ export * from "./search-txt";
21
21
  // Re-export order utilities for checkout flows
22
22
  export { splitCartIntoOrderGroups, } from "../features/orders/utils/order-splitter";
@@ -7,17 +7,40 @@
7
7
  export declare const FIREBASE_STORAGE_HOST = "firebasestorage.googleapis.com";
8
8
  /** Google Cloud Storage host (used by public-asset URLs). */
9
9
  export declare const GCS_HOST = "storage.googleapis.com";
10
+ export interface ResolveMediaUrlOptions {
11
+ /**
12
+ * When false, a URL we do not own is returned UNCHANGED instead of being sent
13
+ * to `/api/media/ext`. That proxy is image-only (it 400s on `video/mp4`), so
14
+ * every non-image caller must pass false — see `resolveVideoUrl`.
15
+ * @default true
16
+ */
17
+ externalProxy?: boolean;
18
+ }
10
19
  /**
11
20
  * Normalise any image URL so it goes through the watermark proxy:
12
- * - /media/<path> → return as-is (already proxied via Firebase Storage)
21
+ * - /media<path> → return as-is (already proxied via Firebase Storage)
13
22
  * - blob: / data: URI → return as-is (local-only, never fetchable server-side)
14
23
  * - Firebase Storage URL → extract /o/ path → /media/<path>
15
- * - GCS (storage.googleapis.com) URL → drop the bucket segment → /media/<path>
16
- * - Any other absolute URL → /api/media/ext?url=<encoded> (ext watermark proxy)
24
+ * - GCS URL for OUR bucket → drop the bucket segment → /media/<path>
25
+ * - Any other absolute URL → /api/media/ext?url=<encoded> (ext watermark proxy),
26
+ * or unchanged when `externalProxy: false`
17
27
  * - Relative URI → return as-is
18
28
  * - Falsy → undefined
19
29
  */
20
- export declare function resolveMediaUrl(url: string | null | undefined): string | undefined;
30
+ export declare function resolveMediaUrl(url: string | null | undefined, opts?: ResolveMediaUrlOptions): string | undefined;
31
+ /**
32
+ * Resolve a VIDEO src.
33
+ *
34
+ * Same rules as `resolveMediaUrl` for media we own — a Firebase Storage or
35
+ * own-bucket GCS URL still becomes `/media/<path>`, and the `[...slug]` proxy
36
+ * raw-pipes non-images — but a third-party URL is returned untouched rather
37
+ * than routed through `/api/media/ext`, which is image-only and 400s on
38
+ * `video/mp4` (Root Cause #27).
39
+ *
40
+ * Use this for anything that ends up as a `<video src>`. Posters and thumbnails
41
+ * are images and stay on `resolveMediaUrl`.
42
+ */
43
+ export declare function resolveVideoUrl(url: string | null | undefined): string | undefined;
21
44
  /**
22
45
  * Extracts the 11-char YouTube video ID from a watch/share/embed URL, or
23
46
  * `null` when `url` isn't a recognized YouTube URL. A video field can be
@@ -10,17 +10,25 @@ export const FIREBASE_STORAGE_HOST = "firebasestorage.googleapis.com";
10
10
  /** Google Cloud Storage host (used by public-asset URLs). */
11
11
  export const GCS_HOST = "storage.googleapis.com";
12
12
  const PROXY_PREFIX = "/media/";
13
+ /**
14
+ * Our own Storage bucket, as it appears as the first path segment of a
15
+ * `storage.googleapis.com` URL. Only URLs pointing at THIS bucket may have that
16
+ * segment stripped and be rewritten to `/media/<path>` — see the GCS branch
17
+ * below for why assuming every GCS URL is ours is a live 404.
18
+ */
19
+ const OWN_STORAGE_BUCKET = process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET ?? "";
13
20
  /**
14
21
  * Normalise any image URL so it goes through the watermark proxy:
15
- * - /media/<path> → return as-is (already proxied via Firebase Storage)
22
+ * - /media<path> → return as-is (already proxied via Firebase Storage)
16
23
  * - blob: / data: URI → return as-is (local-only, never fetchable server-side)
17
24
  * - Firebase Storage URL → extract /o/ path → /media/<path>
18
- * - GCS (storage.googleapis.com) URL → drop the bucket segment → /media/<path>
19
- * - Any other absolute URL → /api/media/ext?url=<encoded> (ext watermark proxy)
25
+ * - GCS URL for OUR bucket → drop the bucket segment → /media/<path>
26
+ * - Any other absolute URL → /api/media/ext?url=<encoded> (ext watermark proxy),
27
+ * or unchanged when `externalProxy: false`
20
28
  * - Relative URI → return as-is
21
29
  * - Falsy → undefined
22
30
  */
23
- export function resolveMediaUrl(url) {
31
+ export function resolveMediaUrl(url, opts) {
24
32
  if (!url)
25
33
  return undefined;
26
34
  if (url.startsWith(PROXY_PREFIX))
@@ -43,19 +51,42 @@ export function resolveMediaUrl(url) {
43
51
  // Path shape: /<bucket>/<object-path...> — drop the bucket segment,
44
52
  // proxy the rest like a Firebase Storage URL (consistent watermarking
45
53
  // + caching instead of falling through to the external-URL proxy).
54
+ //
55
+ // ONLY for our own bucket. This branch used to strip the first segment of
56
+ // ANY storage.googleapis.com URL, so Google's public sample bucket
57
+ // https://storage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4
58
+ // became /media/sample/BigBuckBunny.mp4 — an object that has never existed
59
+ // in our bucket — and 404'd on the homepage hero carousel.
46
60
  const parts = parsed.pathname.replace(/^\//, "").split("/");
47
- if (parts.length > 1) {
61
+ if (parts.length > 1 && OWN_STORAGE_BUCKET && parts[0] === OWN_STORAGE_BUCKET) {
48
62
  const objectPath = parts.slice(1).join("/");
49
63
  return `${PROXY_PREFIX}${decodeURIComponent(objectPath)}`;
50
64
  }
51
65
  }
52
- return MEDIA_ENDPOINTS.EXT_URL(url);
66
+ // Not ours. The ext proxy watermarks IMAGES only and 400s on anything else,
67
+ // so a non-image caller opts out and gets the original URL back.
68
+ return opts?.externalProxy === false ? url : MEDIA_ENDPOINTS.EXT_URL(url);
53
69
  }
54
70
  catch (_err) {
55
71
  void normalizeError(_err); // URL constructor throws for non-URL strings (e.g., relative paths) — return original
56
72
  return url;
57
73
  }
58
74
  }
75
+ /**
76
+ * Resolve a VIDEO src.
77
+ *
78
+ * Same rules as `resolveMediaUrl` for media we own — a Firebase Storage or
79
+ * own-bucket GCS URL still becomes `/media/<path>`, and the `[...slug]` proxy
80
+ * raw-pipes non-images — but a third-party URL is returned untouched rather
81
+ * than routed through `/api/media/ext`, which is image-only and 400s on
82
+ * `video/mp4` (Root Cause #27).
83
+ *
84
+ * Use this for anything that ends up as a `<video src>`. Posters and thumbnails
85
+ * are images and stay on `resolveMediaUrl`.
86
+ */
87
+ export function resolveVideoUrl(url) {
88
+ return resolveMediaUrl(url, { externalProxy: false });
89
+ }
59
90
  const YOUTUBE_HOSTS = new Set([
60
91
  "youtube.com",
61
92
  "m.youtube.com",
@@ -0,0 +1,71 @@
1
+ /**
2
+ * `searchTxt` — one normalized, partial-match search field per entity.
3
+ *
4
+ * ## Why an array and not a string
5
+ *
6
+ * Firestore has no LIKE/substring operator. A string field supports `==` and a
7
+ * range, and a range on a string is a **prefix anchored at the start of the whole
8
+ * value** — so `"pokemon charizard psa 9"` would match a search for `"pok"` and
9
+ * never for `"charizard"`. A single text column cannot do partial match here.
10
+ *
11
+ * An array of normalized *edge n-grams* can, in one `array-contains` clause:
12
+ *
13
+ * "Pokémon Charizard PSA 9"
14
+ * → normalize → "pokemon charizard psa 9"
15
+ * → ["po","pok",…,"pokemon","ch","cha","char","chari",…,"charizard","ps","psa"]
16
+ *
17
+ * A query for "chari" is then a single equality-shaped clause against one index.
18
+ * No OR-group, no query-time case folding, no per-sort composite index.
19
+ *
20
+ * ## Deliberate limits
21
+ *
22
+ * - **Word-prefix, not mid-word.** "ariza" will not match "charizard". True
23
+ * mid-word needs trigrams, which multiply index entries several-fold for a
24
+ * case users rarely type.
25
+ * - **One `array-contains` per Firestore query**, so a multi-word search uses the
26
+ * most selective term as the clause and AND-refines the rest server-side.
27
+ * - **Never put PII in here.** These are readable fragments of the source text —
28
+ * a `searchTxt` for a person's name IS that name, re-encoded. Encrypted fields
29
+ * stay searchable only through their HMAC blind index (exact match).
30
+ */
31
+ export interface SearchTxtOptions {
32
+ /**
33
+ * Emit prefixes for these sources. Use for short, high-signal text (title,
34
+ * tags, category). Long body text should stay whole-word or it dominates the
35
+ * token budget.
36
+ */
37
+ prefix?: boolean;
38
+ maxPrefixLength?: number;
39
+ maxTokens?: number;
40
+ }
41
+ type Source = string | string[] | undefined | null;
42
+ /**
43
+ * Lowercase, strip accents, collapse everything non-alphanumeric to spaces.
44
+ *
45
+ * Accent folding is what makes "Pokemon" find "Pokémon" — a plain lowercase
46
+ * would leave those as different tokens.
47
+ */
48
+ export declare function normalizeSearchText(input: string): string;
49
+ /**
50
+ * Build the stored token array.
51
+ *
52
+ * @param sources any mix of strings and string arrays; nullish entries ignored
53
+ */
54
+ export declare function buildSearchTxt(sources: Source[], options?: SearchTxtOptions): string[];
55
+ /**
56
+ * Tokenize a user's query into candidate clause values, most selective first.
57
+ *
58
+ * Longest term first because only one may become the `array-contains` clause —
59
+ * the longest is the cheapest proxy for "narrows the result set most" without
60
+ * keeping per-term cardinality statistics.
61
+ *
62
+ * Uses the SAME normalizer as the write side; if the two ever diverge, a query
63
+ * silently stops matching what was indexed.
64
+ */
65
+ export declare function parseSearchTxtQuery(query: string): string[];
66
+ /**
67
+ * Does a document's stored tokens satisfy every term? Used for the AND-refine
68
+ * pass after the single `array-contains` clause has narrowed the candidates.
69
+ */
70
+ export declare function matchesAllSearchTerms(searchTxt: readonly string[] | undefined, terms: readonly string[]): boolean;
71
+ export {};
@@ -0,0 +1,118 @@
1
+ /**
2
+ * `searchTxt` — one normalized, partial-match search field per entity.
3
+ *
4
+ * ## Why an array and not a string
5
+ *
6
+ * Firestore has no LIKE/substring operator. A string field supports `==` and a
7
+ * range, and a range on a string is a **prefix anchored at the start of the whole
8
+ * value** — so `"pokemon charizard psa 9"` would match a search for `"pok"` and
9
+ * never for `"charizard"`. A single text column cannot do partial match here.
10
+ *
11
+ * An array of normalized *edge n-grams* can, in one `array-contains` clause:
12
+ *
13
+ * "Pokémon Charizard PSA 9"
14
+ * → normalize → "pokemon charizard psa 9"
15
+ * → ["po","pok",…,"pokemon","ch","cha","char","chari",…,"charizard","ps","psa"]
16
+ *
17
+ * A query for "chari" is then a single equality-shaped clause against one index.
18
+ * No OR-group, no query-time case folding, no per-sort composite index.
19
+ *
20
+ * ## Deliberate limits
21
+ *
22
+ * - **Word-prefix, not mid-word.** "ariza" will not match "charizard". True
23
+ * mid-word needs trigrams, which multiply index entries several-fold for a
24
+ * case users rarely type.
25
+ * - **One `array-contains` per Firestore query**, so a multi-word search uses the
26
+ * most selective term as the clause and AND-refines the rest server-side.
27
+ * - **Never put PII in here.** These are readable fragments of the source text —
28
+ * a `searchTxt` for a person's name IS that name, re-encoded. Encrypted fields
29
+ * stay searchable only through their HMAC blind index (exact match).
30
+ */
31
+ /** Longest prefix emitted. Nobody types more than this before results narrow. */
32
+ const MAX_PREFIX_LENGTH = 12;
33
+ /** Shortest prefix emitted. 1 keeps single-character searches working. */
34
+ const MIN_PREFIX_LENGTH = 1;
35
+ /**
36
+ * Ceiling on tokens per document. Firestore allows 40k index entries per doc and
37
+ * each array element costs one per index containing the field, so a long body
38
+ * left unbounded is a real limit to hit.
39
+ */
40
+ const MAX_TOKENS = 600;
41
+ /** Words longer than this contribute whole-word only — no prefix expansion. */
42
+ const LONG_WORD_LIMIT = 24;
43
+ /**
44
+ * Lowercase, strip accents, collapse everything non-alphanumeric to spaces.
45
+ *
46
+ * Accent folding is what makes "Pokemon" find "Pokémon" — a plain lowercase
47
+ * would leave those as different tokens.
48
+ */
49
+ export function normalizeSearchText(input) {
50
+ return input
51
+ .normalize("NFD")
52
+ .replace(/[̀-ͯ]/g, "")
53
+ .toLowerCase()
54
+ .replace(/[^a-z0-9]+/g, " ")
55
+ .trim();
56
+ }
57
+ function words(sources) {
58
+ const text = sources
59
+ .flat()
60
+ .filter((v) => typeof v === "string" && v.length > 0)
61
+ .join(" ");
62
+ const normalized = normalizeSearchText(text);
63
+ return normalized ? normalized.split(" ").filter(Boolean) : [];
64
+ }
65
+ /**
66
+ * Build the stored token array.
67
+ *
68
+ * @param sources any mix of strings and string arrays; nullish entries ignored
69
+ */
70
+ export function buildSearchTxt(sources, options = {}) {
71
+ const { prefix = true, maxPrefixLength = MAX_PREFIX_LENGTH, maxTokens = MAX_TOKENS, } = options;
72
+ const tokens = new Set();
73
+ for (const word of words(sources)) {
74
+ tokens.add(word);
75
+ if (!prefix || word.length > LONG_WORD_LIMIT)
76
+ continue;
77
+ // Inclusive: maxPrefixLength=5 must emit the 5-character prefix. When the
78
+ // bound equals the word length the last slice is the whole word, which the
79
+ // Set already holds.
80
+ const upper = Math.min(word.length, maxPrefixLength);
81
+ for (let n = MIN_PREFIX_LENGTH; n <= upper; n++) {
82
+ tokens.add(word.slice(0, n));
83
+ }
84
+ if (tokens.size >= maxTokens)
85
+ break;
86
+ }
87
+ return Array.from(tokens).slice(0, maxTokens);
88
+ }
89
+ /**
90
+ * Tokenize a user's query into candidate clause values, most selective first.
91
+ *
92
+ * Longest term first because only one may become the `array-contains` clause —
93
+ * the longest is the cheapest proxy for "narrows the result set most" without
94
+ * keeping per-term cardinality statistics.
95
+ *
96
+ * Uses the SAME normalizer as the write side; if the two ever diverge, a query
97
+ * silently stops matching what was indexed.
98
+ */
99
+ export function parseSearchTxtQuery(query) {
100
+ const normalized = normalizeSearchText(query ?? "");
101
+ if (!normalized)
102
+ return [];
103
+ return Array.from(new Set(normalized.split(" ").filter(Boolean)))
104
+ .sort((a, b) => b.length - a.length)
105
+ .slice(0, 10);
106
+ }
107
+ /**
108
+ * Does a document's stored tokens satisfy every term? Used for the AND-refine
109
+ * pass after the single `array-contains` clause has narrowed the candidates.
110
+ */
111
+ export function matchesAllSearchTerms(searchTxt, terms) {
112
+ if (terms.length === 0)
113
+ return true;
114
+ if (!searchTxt || searchTxt.length === 0)
115
+ return false;
116
+ const set = new Set(searchTxt);
117
+ return terms.every((t) => set.has(t));
118
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.26.0",
3
+ "version": "4.28.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"