@fire-spark/rn-ui 0.4.1 → 0.6.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.
package/dist/index.d.ts CHANGED
@@ -4,5 +4,5 @@ import { Category, CategoryContent, CategoryDescription, CategoryEmpty, Category
4
4
  import { Checkout, CheckoutAside, CheckoutBack, CheckoutBanner, CheckoutColumn, CheckoutDescription, CheckoutFeeInfo, CheckoutFeeLabel, CheckoutFeeList, CheckoutFeeRow, CheckoutFeeValue, CheckoutFooter, CheckoutFulfillment, CheckoutFulfillmentOption, CheckoutHeader, CheckoutMain, CheckoutNav, CheckoutNavBrand, CheckoutOption, CheckoutOptionControl, CheckoutOptionLabel, CheckoutPayment, CheckoutPaymentOption, CheckoutPaymentPanel, CheckoutPaymentPanelHeader, CheckoutPromo, CheckoutPromoChevron, CheckoutPromoIcon, CheckoutPromoLabel, CheckoutRow, CheckoutRowAside, CheckoutRowChevron, CheckoutRowContent, CheckoutRowDescription, CheckoutRowMedia, CheckoutRowTitle, CheckoutSection, CheckoutSectionAction, CheckoutSectionHeader, CheckoutSectionTitle, CheckoutStore, CheckoutStoreContent, CheckoutStoreDescription, CheckoutStoreImage, CheckoutStoreMedia, CheckoutStoreTitle, CheckoutSubmit, CheckoutTip, CheckoutTipCustom, CheckoutTipList, CheckoutTipOption, CheckoutTitle } from "./checkout.js";
5
5
  import { Menu, MenuEmpty, MenuEmptyContent, MenuEmptyDescription, MenuEmptyHeader, MenuEmptyMedia, MenuEmptyTitle } from "./menu.js";
6
6
  import { ProductCard, ProductCardAddButton, ProductCardAnchorPrice, ProductCardBadge, ProductCardContent, ProductCardDescription, ProductCardDiscount, ProductCardFooter, ProductCardHeader, ProductCardImage, ProductCardImageSlot, ProductCardMedia, ProductCardMeta, ProductCardPrice, ProductCardRating, ProductCardTitle } from "./product-card.js";
7
- import { UseActiveCategoryOptions, useActiveCategory } from "./use-active-category.js";
8
- export { Cart, CartAction, CartBanner, CartBody, CartDescription, CartEmpty, CartEmptyDescription, CartEmptyTitle, CartFooter, CartHeader, CartItem, CartItemAction, CartItemActions, CartItemAnchorPrice, CartItemAside, CartItemContent, CartItemDescription, CartItemHeader, CartItemImage, CartItemImageSlot, CartItemMedia, CartItemMeta, CartItemPrice, CartItemTitle, CartList, CartOption, CartOptionControl, CartOptionLabel, CartQuantity, CartQuantityButton, CartQuantityValue, CartScroller, CartSection, CartSectionHeader, CartSectionTitle, CartStore, CartStoreContent, CartStoreDescription, CartStoreImage, CartStoreMedia, CartStoreTitle, CartSubmit, CartSummary, CartSummaryLabel, CartSummaryRow, CartSummaryValue, CartTitle, Category, CategoryContent, CategoryDescription, CategoryEmpty, CategoryEmptyContent, CategoryEmptyDescription, CategoryEmptyHeader, CategoryEmptyMedia, CategoryEmptyTitle, CategoryGrid, CategoryGridItem, CategoryHeader, CategoryList, CategoryListItem, CategoryNav, CategoryNavExtra, CategoryNavItem, CategoryNavLink, CategoryNavList, CategoryNavOrientation, CategoryTitle, Checkout, CheckoutAside, CheckoutBack, CheckoutBanner, CheckoutColumn, CheckoutDescription, CheckoutFeeInfo, CheckoutFeeLabel, CheckoutFeeList, CheckoutFeeRow, CheckoutFeeValue, CheckoutFooter, CheckoutFulfillment, CheckoutFulfillmentOption, CheckoutHeader, CheckoutMain, CheckoutNav, CheckoutNavBrand, CheckoutOption, CheckoutOptionControl, CheckoutOptionLabel, CheckoutPayment, CheckoutPaymentOption, CheckoutPaymentPanel, CheckoutPaymentPanelHeader, CheckoutPromo, CheckoutPromoChevron, CheckoutPromoIcon, CheckoutPromoLabel, CheckoutRow, CheckoutRowAside, CheckoutRowChevron, CheckoutRowContent, CheckoutRowDescription, CheckoutRowMedia, CheckoutRowTitle, CheckoutSection, CheckoutSectionAction, CheckoutSectionHeader, CheckoutSectionTitle, CheckoutStore, CheckoutStoreContent, CheckoutStoreDescription, CheckoutStoreImage, CheckoutStoreMedia, CheckoutStoreTitle, CheckoutSubmit, CheckoutTip, CheckoutTipCustom, CheckoutTipList, CheckoutTipOption, CheckoutTitle, Menu, MenuEmpty, MenuEmptyContent, MenuEmptyDescription, MenuEmptyHeader, MenuEmptyMedia, MenuEmptyTitle, ProductCard, ProductCardAddButton, ProductCardAnchorPrice, ProductCardBadge, ProductCardContent, ProductCardDescription, ProductCardDiscount, ProductCardFooter, ProductCardHeader, ProductCardImage, ProductCardImageSlot, ProductCardMedia, ProductCardMeta, ProductCardPrice, ProductCardRating, ProductCardTitle, UseActiveCategoryOptions, useActiveCategory };
7
+ import { ActiveCategoryViewToken, ActiveCategoryViewableInfo, ScrollableList, UseActiveCategoryIndexOptions, UseActiveCategoryOffsetOptions, UseActiveCategoryOptions, useActiveCategory } from "./use-active-category.js";
8
+ export { ActiveCategoryViewToken, ActiveCategoryViewableInfo, Cart, CartAction, CartBanner, CartBody, CartDescription, CartEmpty, CartEmptyDescription, CartEmptyTitle, CartFooter, CartHeader, CartItem, CartItemAction, CartItemActions, CartItemAnchorPrice, CartItemAside, CartItemContent, CartItemDescription, CartItemHeader, CartItemImage, CartItemImageSlot, CartItemMedia, CartItemMeta, CartItemPrice, CartItemTitle, CartList, CartOption, CartOptionControl, CartOptionLabel, CartQuantity, CartQuantityButton, CartQuantityValue, CartScroller, CartSection, CartSectionHeader, CartSectionTitle, CartStore, CartStoreContent, CartStoreDescription, CartStoreImage, CartStoreMedia, CartStoreTitle, CartSubmit, CartSummary, CartSummaryLabel, CartSummaryRow, CartSummaryValue, CartTitle, Category, CategoryContent, CategoryDescription, CategoryEmpty, CategoryEmptyContent, CategoryEmptyDescription, CategoryEmptyHeader, CategoryEmptyMedia, CategoryEmptyTitle, CategoryGrid, CategoryGridItem, CategoryHeader, CategoryList, CategoryListItem, CategoryNav, CategoryNavExtra, CategoryNavItem, CategoryNavLink, CategoryNavList, CategoryNavOrientation, CategoryTitle, Checkout, CheckoutAside, CheckoutBack, CheckoutBanner, CheckoutColumn, CheckoutDescription, CheckoutFeeInfo, CheckoutFeeLabel, CheckoutFeeList, CheckoutFeeRow, CheckoutFeeValue, CheckoutFooter, CheckoutFulfillment, CheckoutFulfillmentOption, CheckoutHeader, CheckoutMain, CheckoutNav, CheckoutNavBrand, CheckoutOption, CheckoutOptionControl, CheckoutOptionLabel, CheckoutPayment, CheckoutPaymentOption, CheckoutPaymentPanel, CheckoutPaymentPanelHeader, CheckoutPromo, CheckoutPromoChevron, CheckoutPromoIcon, CheckoutPromoLabel, CheckoutRow, CheckoutRowAside, CheckoutRowChevron, CheckoutRowContent, CheckoutRowDescription, CheckoutRowMedia, CheckoutRowTitle, CheckoutSection, CheckoutSectionAction, CheckoutSectionHeader, CheckoutSectionTitle, CheckoutStore, CheckoutStoreContent, CheckoutStoreDescription, CheckoutStoreImage, CheckoutStoreMedia, CheckoutStoreTitle, CheckoutSubmit, CheckoutTip, CheckoutTipCustom, CheckoutTipList, CheckoutTipOption, CheckoutTitle, Menu, MenuEmpty, MenuEmptyContent, MenuEmptyDescription, MenuEmptyHeader, MenuEmptyMedia, MenuEmptyTitle, ProductCard, ProductCardAddButton, ProductCardAnchorPrice, ProductCardBadge, ProductCardContent, ProductCardDescription, ProductCardDiscount, ProductCardFooter, ProductCardHeader, ProductCardImage, ProductCardImageSlot, ProductCardMedia, ProductCardMeta, ProductCardPrice, ProductCardRating, ProductCardTitle, ScrollableList, UseActiveCategoryIndexOptions, UseActiveCategoryOffsetOptions, UseActiveCategoryOptions, useActiveCategory };
@@ -1,29 +1,59 @@
1
1
  import { LayoutChangeEvent } from "react-native";
2
2
  import { RefObject } from "react";
3
3
  //#region src/use-active-category.d.ts
4
- type Scrollable = {
4
+ type OffsetScrollable = {
5
5
  scrollTo: (opts: {
6
6
  y: number;
7
7
  animated?: boolean;
8
8
  }) => void;
9
9
  };
10
- type UseActiveCategoryOptions = {
10
+ /** List ref shared by FlashList, FlatList, and LegendList. No list package. */
11
+ type ScrollableList = {
12
+ scrollToIndex: (params: {
13
+ index: number;
14
+ animated?: boolean;
15
+ viewOffset?: number;
16
+ }) => unknown;
17
+ getFirstVisibleIndex?: () => number;
18
+ };
19
+ type ActiveCategoryViewToken = {
20
+ index: number | null;
21
+ isViewable?: boolean;
22
+ };
23
+ type ActiveCategoryViewableInfo = {
24
+ viewableItems: readonly ActiveCategoryViewToken[];
25
+ };
26
+ type ScrollOffsetEvent = {
27
+ nativeEvent: {
28
+ contentOffset: {
29
+ y: number;
30
+ };
31
+ };
32
+ };
33
+ type UseActiveCategoryOffsetOptions = {
11
34
  categoryIds: readonly string[];
12
35
  offset?: number;
13
- scrollRef?: RefObject<Scrollable | null>;
36
+ scrollRef?: RefObject<OffsetScrollable | null>;
14
37
  };
38
+ type UseActiveCategoryIndexOptions = {
39
+ /** Section id for every list row (row i belongs to rowSectionIds[i]). */
40
+ rowSectionIds: readonly string[];
41
+ /** Section id → index of its header row. */
42
+ headerIndex: ReadonlyMap<string, number>;
43
+ listRef: RefObject<ScrollableList | null>;
44
+ /** Pixels kept above the header after a jump (sticky chrome). */
45
+ offset?: number;
46
+ /** Section shown as active before the first scroll (deep links). */
47
+ initialCategoryId?: string;
48
+ };
49
+ type UseActiveCategoryOptions = UseActiveCategoryOffsetOptions | UseActiveCategoryIndexOptions;
15
50
  declare const useActiveCategory: (opts: UseActiveCategoryOptions) => {
51
+ onViewableItemsChanged: (info: ActiveCategoryViewableInfo) => void;
52
+ selectCategory: (categoryId: string) => void;
53
+ onScroll: (event?: ScrollOffsetEvent) => void;
54
+ registerCategory: (categoryId: string) => (_event: LayoutChangeEvent) => void;
16
55
  activeCategoryId: string;
17
56
  setActiveCategoryId: import("react").Dispatch<import("react").SetStateAction<string>>;
18
- selectCategory: (categoryId: string) => void;
19
- onScroll: (event: {
20
- nativeEvent: {
21
- contentOffset: {
22
- y: number;
23
- };
24
- };
25
- }) => void;
26
- registerCategory: (categoryId: string) => (event: LayoutChangeEvent) => void;
27
57
  };
28
58
  //#endregion
29
- export { UseActiveCategoryOptions, useActiveCategory };
59
+ export { ActiveCategoryViewToken, ActiveCategoryViewableInfo, ScrollableList, UseActiveCategoryIndexOptions, UseActiveCategoryOffsetOptions, UseActiveCategoryOptions, useActiveCategory };
@@ -4,24 +4,56 @@ import { useEffect, useRef, useState } from "react";
4
4
  const SCROLL_SETTLE_MS = 140;
5
5
  /** Fallback if scroll never starts; scroll handler re-arms while moving. */
6
6
  const SCROLL_LOCK_FALLBACK_MS = 1e3;
7
+ const EMPTY_IDS = [];
8
+ const EMPTY_HEADERS = /* @__PURE__ */ new Map();
9
+ const EMPTY_LIST_REF = { current: null };
10
+ const isIndexMode = (opts) => {
11
+ return "rowSectionIds" in opts;
12
+ };
13
+ const firstSectionId = (headerIndex, rowSectionIds) => {
14
+ let bestId = "";
15
+ let bestAt = Number.POSITIVE_INFINITY;
16
+ for (const [id, index] of headerIndex) {
17
+ if (index >= bestAt) continue;
18
+ bestAt = index;
19
+ bestId = id;
20
+ }
21
+ if (bestId) return bestId;
22
+ return rowSectionIds[0] ?? "";
23
+ };
24
+ const ignoreLayout = (_event) => {};
25
+ const ignoreViewable = (_info) => {};
7
26
  const useActiveCategory = (opts) => {
8
- const { categoryIds, offset = 0, scrollRef } = opts;
9
- const [activeCategoryId, setActiveCategoryId] = useState(() => {
27
+ const indexMode = isIndexMode(opts);
28
+ const offset = opts.offset ?? 0;
29
+ const categoryIds = indexMode ? EMPTY_IDS : opts.categoryIds;
30
+ const rowSectionIds = indexMode ? opts.rowSectionIds : EMPTY_IDS;
31
+ const headerIndex = indexMode ? opts.headerIndex : EMPTY_HEADERS;
32
+ const listRef = indexMode ? opts.listRef : EMPTY_LIST_REF;
33
+ const scrollRef = indexMode ? void 0 : opts.scrollRef;
34
+ const initialCategoryId = indexMode ? opts.initialCategoryId : void 0;
35
+ const [storedId, setStoredId] = useState(() => {
36
+ if (indexMode) return initialCategoryId ?? "";
10
37
  return categoryIds[0] ?? "";
11
38
  });
12
39
  const offsetsRef = useRef(/* @__PURE__ */ new Map());
13
40
  const lockedIdRef = useRef(null);
14
41
  const settleTimerRef = useRef(null);
15
42
  useEffect(() => {
43
+ if (indexMode) return;
16
44
  if (categoryIds.length === 0) {
17
- setActiveCategoryId("");
45
+ setStoredId("");
18
46
  return;
19
47
  }
20
- setActiveCategoryId((current) => {
48
+ setStoredId((current) => {
21
49
  if (categoryIds.includes(current)) return current;
22
50
  return categoryIds[0] ?? "";
23
51
  });
24
- }, [categoryIds, categoryIds.join("\0")]);
52
+ }, [
53
+ indexMode,
54
+ categoryIds,
55
+ categoryIds.join("\0")
56
+ ]);
25
57
  useEffect(() => {
26
58
  return () => {
27
59
  if (settleTimerRef.current) clearTimeout(settleTimerRef.current);
@@ -33,13 +65,34 @@ const useActiveCategory = (opts) => {
33
65
  lockedIdRef.current = null;
34
66
  }, SCROLL_SETTLE_MS);
35
67
  };
68
+ let activeCategoryId = storedId;
69
+ if (indexMode && !headerIndex.has(storedId)) activeCategoryId = firstSectionId(headerIndex, rowSectionIds);
36
70
  const registerCategory = (categoryId) => {
71
+ if (indexMode) return ignoreLayout;
37
72
  return (event) => {
38
73
  const y = event.nativeEvent.layout.y;
39
74
  offsetsRef.current.set(categoryId, y);
40
75
  };
41
76
  };
77
+ const applyVisibleIndex = (index) => {
78
+ if (typeof index !== "number" || index < 0) return;
79
+ const sectionId = rowSectionIds[index];
80
+ if (!sectionId) return;
81
+ setStoredId((previous) => {
82
+ if (previous === sectionId) return previous;
83
+ return sectionId;
84
+ });
85
+ };
42
86
  const onScroll = (event) => {
87
+ if (indexMode) {
88
+ if (lockedIdRef.current) {
89
+ armSettleUnlock();
90
+ return;
91
+ }
92
+ applyVisibleIndex(listRef.current?.getFirstVisibleIndex?.());
93
+ return;
94
+ }
95
+ if (!event) return;
43
96
  const scrollY = event.nativeEvent.contentOffset.y;
44
97
  if (lockedIdRef.current) {
45
98
  armSettleUnlock();
@@ -51,31 +104,75 @@ const useActiveCategory = (opts) => {
51
104
  const top = offsetsRef.current.get(id);
52
105
  if (top !== void 0 && top <= threshold) current = id;
53
106
  }
54
- setActiveCategoryId((previous) => {
107
+ setStoredId((previous) => {
55
108
  return previous === current ? previous : current;
56
109
  });
57
110
  };
111
+ const onViewableItemsChanged = (info) => {
112
+ if (!indexMode) return;
113
+ if (lockedIdRef.current) {
114
+ armSettleUnlock();
115
+ return;
116
+ }
117
+ let lowest = null;
118
+ for (const token of info.viewableItems) {
119
+ if (token.isViewable === false) continue;
120
+ const tokenIndex = token.index;
121
+ if (tokenIndex == null || tokenIndex < 0) continue;
122
+ if (lowest == null || tokenIndex < lowest) lowest = tokenIndex;
123
+ }
124
+ if (lowest == null) return;
125
+ applyVisibleIndex(lowest);
126
+ };
127
+ const onViewableRef = useRef(onViewableItemsChanged);
128
+ onViewableRef.current = onViewableItemsChanged;
129
+ const stableViewable = useRef((info) => {
130
+ onViewableRef.current(info);
131
+ }).current;
58
132
  const selectCategory = (categoryId) => {
59
133
  lockedIdRef.current = categoryId;
60
- setActiveCategoryId(categoryId);
61
- const top = offsetsRef.current.get(categoryId) ?? 0;
62
- const y = Math.max(0, top - offset);
63
- scrollRef?.current?.scrollTo({
64
- y,
65
- animated: true
66
- });
134
+ setStoredId(categoryId);
135
+ if (indexMode) {
136
+ const index = headerIndex.get(categoryId);
137
+ const list = listRef.current;
138
+ if (index !== void 0 && list) {
139
+ const jump = {
140
+ index,
141
+ animated: true,
142
+ viewOffset: offset
143
+ };
144
+ const pending = list.scrollToIndex(jump);
145
+ Promise.resolve(pending).catch(() => void 0);
146
+ }
147
+ } else {
148
+ const top = offsetsRef.current.get(categoryId) ?? 0;
149
+ const y = Math.max(0, top - offset);
150
+ scrollRef?.current?.scrollTo({
151
+ y,
152
+ animated: true
153
+ });
154
+ }
67
155
  if (settleTimerRef.current) clearTimeout(settleTimerRef.current);
68
156
  settleTimerRef.current = setTimeout(() => {
69
157
  lockedIdRef.current = null;
70
158
  }, SCROLL_LOCK_FALLBACK_MS);
71
159
  };
72
- return {
160
+ const viewable = indexMode ? stableViewable : ignoreViewable;
161
+ const b1 = {
73
162
  activeCategoryId,
74
- setActiveCategoryId,
163
+ setActiveCategoryId: setStoredId
164
+ };
165
+ const b2 = {
75
166
  selectCategory,
76
167
  onScroll,
77
168
  registerCategory
78
169
  };
170
+ const b3 = { onViewableItemsChanged: viewable };
171
+ return {
172
+ ...b1,
173
+ ...b2,
174
+ ...b3
175
+ };
79
176
  };
80
177
  //#endregion
81
178
  export { useActiveCategory };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fire-spark/rn-ui",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "Fire Spark React Native UI components",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -121,6 +121,7 @@
121
121
  "build": "tsdown",
122
122
  "dev": "tsdown --watch",
123
123
  "check-types": "tsc --noEmit",
124
+ "test:utils": "vitest run",
124
125
  "lint": "oxlint ./src --max-warnings=0",
125
126
  "format": "oxfmt ./src",
126
127
  "check-format": "oxfmt --check ./src"