jfs-components 0.0.62 → 0.0.64
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/CHANGELOG.md +59 -0
- package/lib/commonjs/components/Accordion/Accordion.js +1 -1
- package/lib/commonjs/components/ActionFooter/ActionFooter.js +1 -1
- package/lib/commonjs/components/ActionTile/ActionTile.js +2 -1
- package/lib/commonjs/components/AmountInput/AmountInput.js +2 -1
- package/lib/commonjs/components/AppBar/AppBar.js +1 -1
- package/lib/commonjs/components/Avatar/Avatar.js +184 -162
- package/lib/commonjs/components/AvatarGroup/AvatarGroup.js +1 -1
- package/lib/commonjs/components/Badge/Badge.js +2 -1
- package/lib/commonjs/components/Balance/Balance.js +2 -1
- package/lib/commonjs/components/BottomNav/BottomNav.js +2 -1
- package/lib/commonjs/components/BottomNavItem/BottomNavItem.js +106 -86
- package/lib/commonjs/components/Button/Button.js +190 -93
- package/lib/commonjs/components/ButtonGroup/ButtonGroup.js +1 -1
- package/lib/commonjs/components/Card/Card.js +2 -1
- package/lib/commonjs/components/CardCTA/CardCTA.js +1 -1
- package/lib/commonjs/components/CardProviderInfo/CardProviderInfo.js +1 -1
- package/lib/commonjs/components/Carousel/Carousel.js +3 -2
- package/lib/commonjs/components/Checkbox/Checkbox.js +2 -1
- package/lib/commonjs/components/ChipGroup/ChipGroup.js +1 -1
- package/lib/commonjs/components/ChipSelect/ChipSelect.js +2 -1
- package/lib/commonjs/components/DebitCard/DebitCard.js +1 -1
- package/lib/commonjs/components/Disclaimer/Disclaimer.js +2 -1
- package/lib/commonjs/components/Divider/Divider.js +2 -1
- package/lib/commonjs/components/Drawer/Drawer.js +109 -48
- package/lib/commonjs/components/EmptyState/EmptyState.js +2 -1
- package/lib/commonjs/components/FilterBar/FilterBar.js +1 -1
- package/lib/commonjs/components/Form/Form.js +2 -1
- package/lib/commonjs/components/FormField/FormField.js +3 -2
- package/lib/commonjs/components/HStack/HStack.js +1 -1
- package/lib/commonjs/components/HoldingsCard/HoldingsCard.js +2 -1
- package/lib/commonjs/components/IconButton/IconButton.js +118 -128
- package/lib/commonjs/components/IconCapsule/IconCapsule.js +61 -57
- package/lib/commonjs/components/InputSearch/InputSearch.js +7 -3
- package/lib/commonjs/components/LazyList/LazyList.js +1 -1
- package/lib/commonjs/components/LinearMeter/LinearMeter.js +3 -2
- package/lib/commonjs/components/ListGroup/ListGroup.js +1 -1
- package/lib/commonjs/components/ListItem/ListItem.js +190 -142
- package/lib/commonjs/components/MediaCard/MediaCard.js +3 -3
- package/lib/commonjs/components/MerchantProfile/MerchantProfile.js +2 -1
- package/lib/commonjs/components/MoneyValue/MoneyValue.js +2 -1
- package/lib/commonjs/components/NavArrow/NavArrow.js +82 -59
- package/lib/commonjs/components/NoteInput/NoteInput.js +2 -1
- package/lib/commonjs/components/Nudge/Nudge.js +1 -1
- package/lib/commonjs/components/Numpad/Numpad.js +2 -1
- package/lib/commonjs/components/OTP/OTP.js +1 -1
- package/lib/commonjs/components/PaymentFeedback/PaymentFeedback.js +2 -1
- package/lib/commonjs/components/Popup/Popup.js +2 -1
- package/lib/commonjs/components/ProductLabel/ProductLabel.js +2 -1
- package/lib/commonjs/components/ProgressBadge/ProgressBadge.js +2 -1
- package/lib/commonjs/components/RadioButton/RadioButton.js +2 -1
- package/lib/commonjs/components/RechargeCard/RechargeCard.js +2 -1
- package/lib/commonjs/components/Screen/Screen.js +1 -1
- package/lib/commonjs/components/Section/Section.js +500 -166
- package/lib/commonjs/components/SegmentedControl/SegmentedControl.js +3 -2
- package/lib/commonjs/components/StatItem/StatItem.js +2 -1
- package/lib/commonjs/components/StatusHero/StatusHero.js +2 -1
- package/lib/commonjs/components/Stepper/Step.js +2 -1
- package/lib/commonjs/components/Stepper/StepLabel.js +2 -1
- package/lib/commonjs/components/Stepper/Stepper.js +2 -1
- package/lib/commonjs/components/SupportText/SupportText.js +2 -1
- package/lib/commonjs/components/SupportText/SupportTextIcon.js +2 -1
- package/lib/commonjs/components/SwappableAmount/SwappableAmount.js +2 -1
- package/lib/commonjs/components/Tabs/TabItem.js +2 -1
- package/lib/commonjs/components/Tabs/Tabs.js +2 -1
- package/lib/commonjs/components/Text/Text.js +2 -1
- package/lib/commonjs/components/TextInput/TextInput.js +2 -2
- package/lib/commonjs/components/ThreadHero/ThreadHero.js +2 -1
- package/lib/commonjs/components/Title/Title.js +2 -1
- package/lib/commonjs/components/Toast/Toast.js +2 -1
- package/lib/commonjs/components/Toggle/Toggle.js +2 -1
- package/lib/commonjs/components/Tooltip/Tooltip.js +2 -1
- package/lib/commonjs/components/TransactionBubble/TransactionBubble.js +1 -1
- package/lib/commonjs/components/TransactionDetails/TransactionDetails.js +2 -2
- package/lib/commonjs/components/TransactionStatus/TransactionStatus.js +3 -2
- package/lib/commonjs/components/UpiHandle/UpiHandle.js +144 -110
- package/lib/commonjs/components/VStack/VStack.js +1 -1
- package/lib/commonjs/design-tokens/figma-variables-resolver.js +21 -3
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/commonjs/utils/react-utils.js +17 -0
- package/lib/module/components/Accordion/Accordion.js +2 -2
- package/lib/module/components/ActionFooter/ActionFooter.js +2 -2
- package/lib/module/components/ActionTile/ActionTile.js +2 -1
- package/lib/module/components/AmountInput/AmountInput.js +2 -1
- package/lib/module/components/AppBar/AppBar.js +2 -2
- package/lib/module/components/Avatar/Avatar.js +184 -162
- package/lib/module/components/AvatarGroup/AvatarGroup.js +2 -2
- package/lib/module/components/Badge/Badge.js +2 -1
- package/lib/module/components/Balance/Balance.js +2 -1
- package/lib/module/components/BottomNav/BottomNav.js +2 -1
- package/lib/module/components/BottomNavItem/BottomNavItem.js +108 -88
- package/lib/module/components/Button/Button.js +192 -95
- package/lib/module/components/ButtonGroup/ButtonGroup.js +2 -2
- package/lib/module/components/Card/Card.js +2 -1
- package/lib/module/components/CardCTA/CardCTA.js +2 -2
- package/lib/module/components/CardProviderInfo/CardProviderInfo.js +2 -2
- package/lib/module/components/Carousel/Carousel.js +3 -2
- package/lib/module/components/Checkbox/Checkbox.js +2 -1
- package/lib/module/components/ChipGroup/ChipGroup.js +2 -2
- package/lib/module/components/ChipSelect/ChipSelect.js +2 -1
- package/lib/module/components/DebitCard/DebitCard.js +2 -2
- package/lib/module/components/Disclaimer/Disclaimer.js +2 -1
- package/lib/module/components/Divider/Divider.js +2 -1
- package/lib/module/components/Drawer/Drawer.js +109 -48
- package/lib/module/components/EmptyState/EmptyState.js +2 -1
- package/lib/module/components/FilterBar/FilterBar.js +2 -2
- package/lib/module/components/Form/Form.js +2 -1
- package/lib/module/components/FormField/FormField.js +3 -2
- package/lib/module/components/HStack/HStack.js +2 -2
- package/lib/module/components/HoldingsCard/HoldingsCard.js +2 -1
- package/lib/module/components/IconButton/IconButton.js +120 -130
- package/lib/module/components/IconCapsule/IconCapsule.js +60 -57
- package/lib/module/components/InputSearch/InputSearch.js +7 -3
- package/lib/module/components/LazyList/LazyList.js +2 -2
- package/lib/module/components/LinearMeter/LinearMeter.js +3 -2
- package/lib/module/components/ListGroup/ListGroup.js +2 -2
- package/lib/module/components/ListItem/ListItem.js +194 -146
- package/lib/module/components/MediaCard/MediaCard.js +4 -2
- package/lib/module/components/MerchantProfile/MerchantProfile.js +2 -1
- package/lib/module/components/MoneyValue/MoneyValue.js +2 -1
- package/lib/module/components/NavArrow/NavArrow.js +82 -58
- package/lib/module/components/NoteInput/NoteInput.js +2 -1
- package/lib/module/components/Nudge/Nudge.js +2 -2
- package/lib/module/components/Numpad/Numpad.js +2 -1
- package/lib/module/components/OTP/OTP.js +2 -2
- package/lib/module/components/PaymentFeedback/PaymentFeedback.js +2 -1
- package/lib/module/components/Popup/Popup.js +2 -1
- package/lib/module/components/ProductLabel/ProductLabel.js +2 -1
- package/lib/module/components/ProgressBadge/ProgressBadge.js +2 -1
- package/lib/module/components/RadioButton/RadioButton.js +2 -1
- package/lib/module/components/RechargeCard/RechargeCard.js +2 -1
- package/lib/module/components/Screen/Screen.js +2 -2
- package/lib/module/components/Section/Section.js +503 -169
- package/lib/module/components/SegmentedControl/SegmentedControl.js +3 -2
- package/lib/module/components/StatItem/StatItem.js +2 -1
- package/lib/module/components/StatusHero/StatusHero.js +2 -1
- package/lib/module/components/Stepper/Step.js +2 -1
- package/lib/module/components/Stepper/StepLabel.js +2 -1
- package/lib/module/components/Stepper/Stepper.js +2 -1
- package/lib/module/components/SupportText/SupportText.js +2 -1
- package/lib/module/components/SupportText/SupportTextIcon.js +2 -1
- package/lib/module/components/SwappableAmount/SwappableAmount.js +2 -1
- package/lib/module/components/Tabs/TabItem.js +2 -1
- package/lib/module/components/Tabs/Tabs.js +2 -1
- package/lib/module/components/Text/Text.js +2 -1
- package/lib/module/components/TextInput/TextInput.js +3 -3
- package/lib/module/components/ThreadHero/ThreadHero.js +2 -1
- package/lib/module/components/Title/Title.js +2 -1
- package/lib/module/components/Toast/Toast.js +2 -1
- package/lib/module/components/Toggle/Toggle.js +2 -1
- package/lib/module/components/Tooltip/Tooltip.js +2 -1
- package/lib/module/components/TransactionBubble/TransactionBubble.js +2 -2
- package/lib/module/components/TransactionDetails/TransactionDetails.js +3 -3
- package/lib/module/components/TransactionStatus/TransactionStatus.js +3 -2
- package/lib/module/components/UpiHandle/UpiHandle.js +147 -113
- package/lib/module/components/VStack/VStack.js +2 -2
- package/lib/module/design-tokens/figma-variables-resolver.js +21 -3
- package/lib/module/icons/registry.js +1 -1
- package/lib/module/utils/react-utils.js +16 -0
- package/lib/typescript/src/components/Avatar/Avatar.d.ts +11 -17
- package/lib/typescript/src/components/BottomNavItem/BottomNavItem.d.ts +12 -8
- package/lib/typescript/src/components/Button/Button.d.ts +18 -1
- package/lib/typescript/src/components/IconButton/IconButton.d.ts +12 -29
- package/lib/typescript/src/components/IconCapsule/IconCapsule.d.ts +10 -18
- package/lib/typescript/src/components/InputSearch/InputSearch.d.ts +8 -3
- package/lib/typescript/src/components/ListItem/ListItem.d.ts +14 -1
- package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +12 -11
- package/lib/typescript/src/components/Section/Section.d.ts +43 -48
- package/lib/typescript/src/components/UpiHandle/UpiHandle.d.ts +13 -12
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/lib/typescript/src/utils/react-utils.d.ts +15 -0
- package/package.json +4 -6
- package/src/components/Accordion/Accordion.tsx +2 -2
- package/src/components/ActionFooter/ActionFooter.tsx +2 -2
- package/src/components/ActionTile/ActionTile.tsx +2 -1
- package/src/components/AmountInput/AmountInput.tsx +2 -1
- package/src/components/AppBar/AppBar.tsx +2 -2
- package/src/components/Avatar/Avatar.tsx +229 -158
- package/src/components/AvatarGroup/AvatarGroup.tsx +2 -2
- package/src/components/Badge/Badge.tsx +2 -1
- package/src/components/Balance/Balance.tsx +2 -1
- package/src/components/BottomNav/BottomNav.tsx +2 -1
- package/src/components/BottomNavItem/BottomNavItem.tsx +159 -88
- package/src/components/Button/Button.tsx +228 -101
- package/src/components/ButtonGroup/ButtonGroup.tsx +2 -2
- package/src/components/Card/Card.tsx +2 -1
- package/src/components/CardCTA/CardCTA.tsx +2 -2
- package/src/components/CardProviderInfo/CardProviderInfo.tsx +2 -2
- package/src/components/Carousel/Carousel.tsx +3 -2
- package/src/components/Checkbox/Checkbox.tsx +2 -1
- package/src/components/ChipGroup/ChipGroup.tsx +2 -2
- package/src/components/ChipSelect/ChipSelect.tsx +2 -1
- package/src/components/DebitCard/DebitCard.tsx +2 -2
- package/src/components/Disclaimer/Disclaimer.tsx +2 -1
- package/src/components/Divider/Divider.tsx +2 -1
- package/src/components/Drawer/Drawer.tsx +124 -58
- package/src/components/EmptyState/EmptyState.tsx +2 -1
- package/src/components/FilterBar/FilterBar.tsx +2 -2
- package/src/components/Form/Form.tsx +2 -1
- package/src/components/FormField/FormField.tsx +3 -2
- package/src/components/HStack/HStack.tsx +2 -2
- package/src/components/HoldingsCard/HoldingsCard.tsx +2 -1
- package/src/components/IconButton/IconButton.tsx +154 -126
- package/src/components/IconCapsule/IconCapsule.tsx +73 -54
- package/src/components/InputSearch/InputSearch.tsx +19 -5
- package/src/components/LazyList/LazyList.tsx +2 -2
- package/src/components/LinearMeter/LinearMeter.tsx +3 -2
- package/src/components/ListGroup/ListGroup.tsx +2 -2
- package/src/components/ListItem/ListItem.tsx +257 -187
- package/src/components/MediaCard/MediaCard.tsx +2 -1
- package/src/components/MerchantProfile/MerchantProfile.tsx +2 -1
- package/src/components/MoneyValue/MoneyValue.tsx +2 -1
- package/src/components/NavArrow/NavArrow.tsx +91 -58
- package/src/components/NoteInput/NoteInput.tsx +2 -1
- package/src/components/Nudge/Nudge.tsx +2 -2
- package/src/components/Numpad/Numpad.tsx +2 -1
- package/src/components/OTP/OTP.tsx +2 -2
- package/src/components/PaymentFeedback/PaymentFeedback.tsx +2 -1
- package/src/components/Popup/Popup.tsx +2 -1
- package/src/components/ProductLabel/ProductLabel.tsx +2 -1
- package/src/components/ProgressBadge/ProgressBadge.tsx +2 -2
- package/src/components/RadioButton/RadioButton.tsx +2 -1
- package/src/components/RechargeCard/RechargeCard.tsx +2 -1
- package/src/components/Screen/Screen.tsx +2 -2
- package/src/components/Section/Section.tsx +672 -176
- package/src/components/SegmentedControl/SegmentedControl.tsx +3 -2
- package/src/components/StatItem/StatItem.tsx +2 -1
- package/src/components/StatusHero/StatusHero.tsx +2 -1
- package/src/components/Stepper/Step.tsx +2 -1
- package/src/components/Stepper/StepLabel.tsx +2 -1
- package/src/components/Stepper/Stepper.tsx +2 -1
- package/src/components/SupportText/SupportText.tsx +2 -1
- package/src/components/SupportText/SupportTextIcon.tsx +2 -1
- package/src/components/SwappableAmount/SwappableAmount.tsx +2 -1
- package/src/components/Tabs/TabItem.tsx +2 -1
- package/src/components/Tabs/Tabs.tsx +2 -1
- package/src/components/Text/Text.tsx +2 -1
- package/src/components/TextInput/TextInput.tsx +3 -3
- package/src/components/ThreadHero/ThreadHero.tsx +2 -1
- package/src/components/Title/Title.tsx +2 -1
- package/src/components/Toast/Toast.tsx +2 -1
- package/src/components/Toggle/Toggle.tsx +2 -1
- package/src/components/Tooltip/Tooltip.tsx +2 -1
- package/src/components/TransactionBubble/TransactionBubble.tsx +2 -2
- package/src/components/TransactionDetails/TransactionDetails.tsx +3 -3
- package/src/components/TransactionStatus/TransactionStatus.tsx +3 -2
- package/src/components/UpiHandle/UpiHandle.tsx +193 -125
- package/src/components/VStack/VStack.tsx +2 -2
- package/src/design-tokens/figma-variables-resolver.ts +21 -3
- package/src/icons/registry.ts +1 -1
- package/src/utils/react-utils.ts +16 -0
- package/lib/typescript/App.d.ts +0 -2
- package/lib/typescript/index.d.ts +0 -2
- package/lib/typescript/metro.config.d.ts +0 -78
- package/lib/typescript/react-native.config.d.ts +0 -4
|
@@ -1,83 +1,269 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React, { useState, useRef, useCallback
|
|
3
|
+
import React, { useState, useMemo, useRef, useCallback } from 'react';
|
|
4
4
|
import { View, Text, Pressable, Platform } from 'react-native';
|
|
5
|
+
import Animated, { FadeInUp, FadeOutUp, ReduceMotion, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
|
|
5
6
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
7
|
import NavArrow from '../NavArrow/NavArrow';
|
|
8
|
+
import IconCapsule from '../IconCapsule/IconCapsule';
|
|
9
|
+
import ListItem from '../ListItem/ListItem';
|
|
7
10
|
import { usePressableWebSupport } from '../../utils/web-platform-utils';
|
|
8
|
-
import { cloneChildrenWithModes, flattenChildren } from '../../utils/react-utils';
|
|
11
|
+
import { EMPTY_MODES, cloneChildrenWithModes, flattenChildren } from '../../utils/react-utils';
|
|
12
|
+
|
|
13
|
+
// Match Button: delay the press visual on iOS so a scroll-cancelled touch
|
|
14
|
+
// never applies the "pressed" style. See Button.tsx for the rationale.
|
|
15
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
16
|
+
const IS_WEB = Platform.OS === 'web';
|
|
17
|
+
const IS_IOS = Platform.OS === 'ios';
|
|
18
|
+
const HEADER_PRESS_DELAY = IS_IOS ? 130 : 0;
|
|
19
|
+
|
|
20
|
+
// Module-scope style constants — never re-allocated per render.
|
|
21
|
+
const headerWrapStyle = {
|
|
22
|
+
flexDirection: 'row',
|
|
23
|
+
alignItems: 'center',
|
|
24
|
+
justifyContent: 'space-between'
|
|
25
|
+
};
|
|
26
|
+
const headerHoverStyle = {
|
|
27
|
+
opacity: 0.95
|
|
28
|
+
};
|
|
29
|
+
const headerPressedStyle = {
|
|
30
|
+
opacity: 0.85
|
|
31
|
+
};
|
|
32
|
+
const headerFocusStyle = {
|
|
33
|
+
borderColor: '#222',
|
|
34
|
+
borderWidth: 1
|
|
35
|
+
};
|
|
9
36
|
|
|
10
37
|
// ---------------------------------------------------------------------------
|
|
11
|
-
// Shared grid layout
|
|
12
|
-
//
|
|
38
|
+
// Shared grid layout — first-row-anchored sizing.
|
|
39
|
+
//
|
|
40
|
+
// We measure each cell of the *first row* via `onLayout`, take their max as
|
|
41
|
+
// the canonical cellWidth, then apply that explicit width to every cell in
|
|
42
|
+
// every row. Combined with `justify-content: space-between`, this preserves
|
|
43
|
+
// three visual invariants regardless of viewport width:
|
|
44
|
+
// 1. The first cell hugs the container's left edge.
|
|
45
|
+
// 2. The last cell hugs the container's right edge.
|
|
46
|
+
// 3. Cells in row N column K align with cells in row N+1 column K
|
|
47
|
+
// (uniform cell width across the whole grid).
|
|
48
|
+
//
|
|
49
|
+
// Why first-row-anchored?
|
|
50
|
+
//
|
|
51
|
+
// The first row is *always present* (collapsed and expanded both render it),
|
|
52
|
+
// so the measurement happens exactly once on first mount and stays valid for
|
|
53
|
+
// the lifetime of the SlotGrid — the cellWidth is *stable across toggles*.
|
|
54
|
+
// New cells in row 2+ mount when the user expands, and by that time
|
|
55
|
+
// `cellWidth` is already cached, so their `Animated.View` `entering` cascade
|
|
56
|
+
// is never interrupted by a re-measurement-driven re-render.
|
|
57
|
+
//
|
|
58
|
+
// Why not container-width math (e.g. `(containerWidth - gaps) / columns`)?
|
|
59
|
+
// That makes every cell wide enough to fill its share of the row. On wide
|
|
60
|
+
// viewports each cell becomes much wider than its content (icon + label),
|
|
61
|
+
// the content centers inside its oversized cell, and the visible result is
|
|
62
|
+
// a "dead margin" of empty space on the left and right of the grid. Sizing
|
|
63
|
+
// cells to natural content + `space-between` instead pushes the first cell
|
|
64
|
+
// flush left and the last cell flush right, distributing leftover space as
|
|
65
|
+
// the inter-cell gap.
|
|
66
|
+
//
|
|
67
|
+
// Why not measure every cell?
|
|
68
|
+
// The original implementation did, and `max()` could change when the item
|
|
69
|
+
// count changed (collapsed picks one max, expanded another), producing a
|
|
70
|
+
// visible width jump on toggle. The per-cell remeasurement also forced a
|
|
71
|
+
// re-render in the same React batch as the `entering` animation mounting,
|
|
72
|
+
// which Reanimated treats as a cancellation signal — cells visibly didn't
|
|
73
|
+
// animate. Anchoring to the first row eliminates both costs.
|
|
74
|
+
//
|
|
75
|
+
// First-frame behavior is preserved (no blank-flash): until the first-row
|
|
76
|
+
// `onLayout` fires, cells render at their natural width with `space-between`
|
|
77
|
+
// already laying them out correctly; the only thing that changes after
|
|
78
|
+
// measurement is that subsequent rows snap to the same cellWidth so columns
|
|
79
|
+
// align.
|
|
13
80
|
// ---------------------------------------------------------------------------
|
|
14
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
15
81
|
const SLOT_GRID_MAX_COLUMNS = 4;
|
|
16
|
-
|
|
82
|
+
|
|
83
|
+
// Beyond this many "extra" cells, additional cells reuse the cap delay so very
|
|
84
|
+
// large grids (16, 32, …) still feel snappy instead of cascading for >1s.
|
|
85
|
+
const SLOT_GRID_STAGGER_CAP = 8;
|
|
86
|
+
const SLOT_GRID_ENTER_STAGGER_MS = 35;
|
|
87
|
+
const SLOT_GRID_EXIT_STAGGER_MS = 20;
|
|
88
|
+
const SLOT_GRID_EXIT_DURATION_MS = 160;
|
|
89
|
+
const slotGridRowFlowStyle = {
|
|
90
|
+
flexDirection: 'row',
|
|
91
|
+
justifyContent: 'space-between'
|
|
92
|
+
};
|
|
93
|
+
const SlotGrid = /*#__PURE__*/React.memo(function SlotGrid({
|
|
17
94
|
items,
|
|
18
95
|
gap,
|
|
19
|
-
maxColumns = SLOT_GRID_MAX_COLUMNS
|
|
96
|
+
maxColumns = SLOT_GRID_MAX_COLUMNS,
|
|
97
|
+
animateExtrasFromIndex,
|
|
98
|
+
animateContainerLayout
|
|
20
99
|
}) {
|
|
21
|
-
const [maxItemWidth, setMaxItemWidth] = useState(null);
|
|
22
|
-
const [measureTimedOut, setMeasureTimedOut] = useState(false);
|
|
23
|
-
const itemWidthsRef = useRef(new Map());
|
|
24
100
|
const totalItems = items.length;
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
itemWidthsRef.current.clear();
|
|
27
|
-
setMaxItemWidth(null);
|
|
28
|
-
setMeasureTimedOut(false);
|
|
29
|
-
}, [totalItems]);
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
if (maxItemWidth !== null) return;
|
|
32
|
-
const timer = setTimeout(() => setMeasureTimedOut(true), 500);
|
|
33
|
-
return () => clearTimeout(timer);
|
|
34
|
-
}, [maxItemWidth, totalItems]);
|
|
35
|
-
const handleItemLayout = useCallback((index, width) => {
|
|
36
|
-
itemWidthsRef.current.set(index, width);
|
|
37
|
-
if (itemWidthsRef.current.size >= totalItems && totalItems > 0) {
|
|
38
|
-
setMaxItemWidth(Math.max(...itemWidthsRef.current.values()));
|
|
39
|
-
}
|
|
40
|
-
}, [totalItems]);
|
|
41
|
-
const isMeasured = maxItemWidth !== null;
|
|
42
101
|
const columns = Math.min(maxColumns, totalItems || 1);
|
|
102
|
+
// Number of cells in the first row. Capped by `columns` (a fully-filled row)
|
|
103
|
+
// and by `totalItems` (e.g. a 1-item grid has a 1-cell first row).
|
|
104
|
+
const firstRowSize = Math.min(columns, totalItems);
|
|
105
|
+
|
|
106
|
+
// First-row width measurement. Only cells whose `itemIndex < firstRowSize`
|
|
107
|
+
// get an `onLayout` callback. Once we have a width sample for each first-row
|
|
108
|
+
// cell, we publish the max and the callbacks become inert — no further
|
|
109
|
+
// measurement happens for the rest of the SlotGrid's lifetime, so toggles
|
|
110
|
+
// never trigger a re-measurement-driven re-render.
|
|
111
|
+
const [firstRowMaxWidth, setFirstRowMaxWidth] = useState(null);
|
|
112
|
+
const firstRowWidthsRef = useRef(new Map());
|
|
113
|
+
const handleFirstRowItemLayout = useCallback((index, width) => {
|
|
114
|
+
const widths = firstRowWidthsRef.current;
|
|
115
|
+
const previous = widths.get(index);
|
|
116
|
+
if (previous !== undefined && Math.abs(previous - width) < 0.5) return;
|
|
117
|
+
widths.set(index, width);
|
|
118
|
+
if (widths.size >= firstRowSize && firstRowSize > 0) {
|
|
119
|
+
let newMax = 0;
|
|
120
|
+
for (const w of widths.values()) {
|
|
121
|
+
if (w > newMax) newMax = w;
|
|
122
|
+
}
|
|
123
|
+
setFirstRowMaxWidth(prev => prev !== null && Math.abs(prev - newMax) < 0.5 ? prev : newMax);
|
|
124
|
+
}
|
|
125
|
+
}, [firstRowSize]);
|
|
126
|
+
const cellWidth = firstRowMaxWidth;
|
|
43
127
|
const rows = [];
|
|
44
128
|
for (let i = 0; i < items.length; i += columns) {
|
|
45
129
|
rows.push(items.slice(i, i + columns));
|
|
46
130
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
131
|
+
const containerStyle = useMemo(() => ({
|
|
132
|
+
gap
|
|
133
|
+
}), [gap]);
|
|
134
|
+
const cellStyle = useMemo(() => cellWidth !== null ? {
|
|
135
|
+
width: cellWidth
|
|
136
|
+
} : undefined, [cellWidth]);
|
|
137
|
+
|
|
138
|
+
// `space-between` distributes any leftover row space as inter-cell gap, so
|
|
139
|
+
// the first cell is flush-left and the last cell is flush-right regardless
|
|
140
|
+
// of whether cellWidth has been measured yet. Identical layout strategy
|
|
141
|
+
// before and after measurement — no first-frame layout shift.
|
|
142
|
+
const rowStyle = slotGridRowFlowStyle;
|
|
143
|
+
const animationsEnabled = animateExtrasFromIndex !== undefined;
|
|
144
|
+
// Resolve the threshold once. When undefined we treat it as
|
|
145
|
+
// Number.POSITIVE_INFINITY so the per-cell branch always picks the plain
|
|
146
|
+
// `<View>` path.
|
|
147
|
+
const extrasThreshold = animationsEnabled ? animateExtrasFromIndex : Number.POSITIVE_INFINITY;
|
|
148
|
+
// Total count of "extra" cells currently rendered. Used to compute the
|
|
149
|
+
// reverse-stagger delay for the exiting animation so that the last cell
|
|
150
|
+
// leaves first.
|
|
151
|
+
const extrasCount = animationsEnabled ? Math.max(0, totalItems - extrasThreshold) : 0;
|
|
152
|
+
const useAnimatedContainer = animateContainerLayout === true;
|
|
153
|
+
|
|
154
|
+
// Explicit-height clip animation:
|
|
155
|
+
//
|
|
156
|
+
// Reanimated's `LinearTransition` interpolates the container's bounds, and
|
|
157
|
+
// in practice that interpolation drags on the cells inside (they momentarily
|
|
158
|
+
// appear squashed because the parent is shorter than its natural content
|
|
159
|
+
// for the duration of the animation). To keep cells at their *natural size
|
|
160
|
+
// throughout*, we instead:
|
|
161
|
+
// 1. Render the rows inside an inner `<View>` that sizes to its content
|
|
162
|
+
// naturally — cells are never squeezed, never resized.
|
|
163
|
+
// 2. Wrap that inner view in an `Animated.View` with `overflow: 'hidden'`
|
|
164
|
+
// and an explicit `height` driven by a shared value.
|
|
165
|
+
// 3. The inner view reports its natural height via `onLayout`. The first
|
|
166
|
+
// measurement snaps the shared value (no first-mount animation). Every
|
|
167
|
+
// subsequent change (e.g. expand/collapse adds or removes rows) springs
|
|
168
|
+
// the shared value to the new natural height.
|
|
169
|
+
//
|
|
170
|
+
// Visually: the container reveals/conceals content like a curtain, and the
|
|
171
|
+
// cells never deform.
|
|
172
|
+
const animatedHeight = useSharedValue(-1);
|
|
173
|
+
const isFirstHeightLayoutRef = useRef(true);
|
|
174
|
+
const handleContentLayout = useCallback(e => {
|
|
175
|
+
const h = e.nativeEvent.layout.height;
|
|
176
|
+
if (h <= 0) return;
|
|
177
|
+
if (isFirstHeightLayoutRef.current) {
|
|
178
|
+
isFirstHeightLayoutRef.current = false;
|
|
179
|
+
animatedHeight.value = h;
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
animatedHeight.value = withSpring(h, {
|
|
183
|
+
damping: 22,
|
|
184
|
+
stiffness: 180,
|
|
185
|
+
reduceMotion: ReduceMotion.System
|
|
186
|
+
});
|
|
187
|
+
}, [animatedHeight]);
|
|
188
|
+
const animatedHeightStyle = useAnimatedStyle(() => animatedHeight.value < 0 ? {} : {
|
|
189
|
+
height: animatedHeight.value,
|
|
190
|
+
overflow: 'hidden'
|
|
191
|
+
});
|
|
192
|
+
const rowsChildren = /*#__PURE__*/_jsx(_Fragment, {
|
|
54
193
|
children: rows.map((row, rowIndex) => {
|
|
55
194
|
const spacersNeeded = row.length < columns ? columns - row.length : 0;
|
|
56
195
|
return /*#__PURE__*/_jsxs(View, {
|
|
57
|
-
style:
|
|
58
|
-
flexDirection: 'row',
|
|
59
|
-
justifyContent: 'space-between'
|
|
60
|
-
},
|
|
196
|
+
style: rowStyle,
|
|
61
197
|
children: [row.map((child, colIndex) => {
|
|
62
198
|
const itemIndex = rowIndex * columns + colIndex;
|
|
199
|
+
// Only first-row cells participate in measurement, and only
|
|
200
|
+
// until firstRowMaxWidth has been published. After that the
|
|
201
|
+
// onLayout becomes a no-op (we elide it).
|
|
202
|
+
const onLayoutHandler = firstRowMaxWidth === null && itemIndex < firstRowSize ? e => handleFirstRowItemLayout(itemIndex, e.nativeEvent.layout.width) : undefined;
|
|
203
|
+
if (itemIndex >= extrasThreshold) {
|
|
204
|
+
const extraOrdinal = itemIndex - extrasThreshold;
|
|
205
|
+
const enterStaggerSteps = Math.min(extraOrdinal, SLOT_GRID_STAGGER_CAP);
|
|
206
|
+
const reverseOrdinal = Math.max(0, extrasCount - 1 - extraOrdinal);
|
|
207
|
+
const exitStaggerSteps = Math.min(reverseOrdinal, SLOT_GRID_STAGGER_CAP);
|
|
208
|
+
const entering = FadeInUp.springify().damping(18).delay(enterStaggerSteps * SLOT_GRID_ENTER_STAGGER_MS).reduceMotion(ReduceMotion.System);
|
|
209
|
+
const exiting = FadeOutUp.duration(SLOT_GRID_EXIT_DURATION_MS).delay(exitStaggerSteps * SLOT_GRID_EXIT_STAGGER_MS).reduceMotion(ReduceMotion.System);
|
|
210
|
+
return /*#__PURE__*/_jsx(Animated.View, {
|
|
211
|
+
entering: entering,
|
|
212
|
+
exiting: exiting,
|
|
213
|
+
...(onLayoutHandler ? {
|
|
214
|
+
onLayout: onLayoutHandler
|
|
215
|
+
} : null),
|
|
216
|
+
style: cellStyle,
|
|
217
|
+
children: child
|
|
218
|
+
}, itemIndex);
|
|
219
|
+
}
|
|
63
220
|
return /*#__PURE__*/_jsx(View, {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
221
|
+
...(onLayoutHandler ? {
|
|
222
|
+
onLayout: onLayoutHandler
|
|
223
|
+
} : null),
|
|
224
|
+
style: cellStyle,
|
|
68
225
|
children: child
|
|
69
226
|
}, itemIndex);
|
|
70
|
-
}),
|
|
227
|
+
}), cellWidth !== null && spacersNeeded > 0 && Array.from({
|
|
71
228
|
length: spacersNeeded
|
|
72
229
|
}, (_, i) => /*#__PURE__*/_jsx(View, {
|
|
73
|
-
style:
|
|
74
|
-
width: maxItemWidth
|
|
75
|
-
}
|
|
230
|
+
style: cellStyle
|
|
76
231
|
}, `spacer-${i}`))]
|
|
77
232
|
}, rowIndex);
|
|
78
233
|
})
|
|
79
234
|
});
|
|
235
|
+
if (useAnimatedContainer) {
|
|
236
|
+
// Outer Animated.View clips and animates height. Inner View holds the
|
|
237
|
+
// rows at natural size and reports its natural height for the spring
|
|
238
|
+
// target. Cell-width measurement happens on the cells themselves
|
|
239
|
+
// (first-row only) — no container-level onLayout is needed.
|
|
240
|
+
return /*#__PURE__*/_jsx(Animated.View, {
|
|
241
|
+
style: animatedHeightStyle,
|
|
242
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
243
|
+
style: containerStyle,
|
|
244
|
+
onLayout: handleContentLayout,
|
|
245
|
+
children: rowsChildren
|
|
246
|
+
})
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return /*#__PURE__*/_jsx(View, {
|
|
250
|
+
style: containerStyle,
|
|
251
|
+
children: rowsChildren
|
|
252
|
+
});
|
|
253
|
+
}, slotGridPropsAreEqual);
|
|
254
|
+
function slotGridPropsAreEqual(prev, next) {
|
|
255
|
+
if (prev.gap !== next.gap) return false;
|
|
256
|
+
if ((prev.maxColumns ?? SLOT_GRID_MAX_COLUMNS) !== (next.maxColumns ?? SLOT_GRID_MAX_COLUMNS)) return false;
|
|
257
|
+
if (prev.animateExtrasFromIndex !== next.animateExtrasFromIndex) return false;
|
|
258
|
+
if (prev.animateContainerLayout !== next.animateContainerLayout) return false;
|
|
259
|
+
if (prev.items === next.items) return true;
|
|
260
|
+
if (prev.items.length !== next.items.length) return false;
|
|
261
|
+
for (let i = 0; i < prev.items.length; i++) {
|
|
262
|
+
if (prev.items[i] !== next.items[i]) return false;
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
80
265
|
}
|
|
266
|
+
|
|
81
267
|
/**
|
|
82
268
|
* Section component that mirrors the Figma "Section" component.
|
|
83
269
|
*
|
|
@@ -102,96 +288,99 @@ function SlotGrid({
|
|
|
102
288
|
* @param {string} [props.accessibilityLabel] - Accessibility label for the section. If not provided, uses title
|
|
103
289
|
* @param {string} [props.accessibilityHint] - Additional accessibility hint for screen readers
|
|
104
290
|
*/
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
supportText = 'Section support text',
|
|
108
|
-
showSupportText = true,
|
|
109
|
-
slot,
|
|
110
|
-
slotDirection = 'row',
|
|
111
|
-
modes = {},
|
|
112
|
-
onPress,
|
|
113
|
-
style,
|
|
114
|
-
accessibilityLabel,
|
|
115
|
-
accessibilityHint,
|
|
116
|
-
webAccessibilityProps,
|
|
117
|
-
...rest
|
|
118
|
-
}) {
|
|
119
|
-
const [isHeaderFocused, setIsHeaderFocused] = useState(false);
|
|
120
|
-
const [isHeaderHovered, setIsHeaderHovered] = useState(false);
|
|
121
|
-
const [isHeaderPressed, setIsHeaderPressed] = useState(false);
|
|
122
|
-
const headerFocusStyle = isHeaderFocused ? {
|
|
123
|
-
borderColor: '#222',
|
|
124
|
-
borderWidth: 1
|
|
125
|
-
} : {};
|
|
126
|
-
const headerHoverStyle = isHeaderHovered ? {
|
|
127
|
-
opacity: 0.95
|
|
128
|
-
} : {};
|
|
129
|
-
const headerPressedStyle = isHeaderPressed ? {
|
|
130
|
-
opacity: 0.85
|
|
131
|
-
} : {};
|
|
132
|
-
// Resolve section container tokens
|
|
291
|
+
|
|
292
|
+
function resolveSectionTokens(modes) {
|
|
133
293
|
const backgroundColor = getVariableByName('section/background/color', modes) || '#ffffff';
|
|
134
294
|
const sectionGap = getVariableByName('section/gap', modes) || 12;
|
|
135
295
|
const slotGap = getVariableByName('slot/gap', modes) || 12;
|
|
136
296
|
const paddingHorizontal = getVariableByName('section/padding/horizontal', modes) || 12;
|
|
137
297
|
const paddingVertical = getVariableByName('section/padding/vertical', modes) || 16;
|
|
138
298
|
const radius = getVariableByName('section/radius', modes) || 12;
|
|
139
|
-
|
|
140
|
-
// Resolve section header tokens
|
|
141
299
|
const headerGap = getVariableByName('section/header/gap', modes) || 8;
|
|
142
300
|
const headerPaddingHorizontal = getVariableByName('section/header/padding/horizontal', modes) || 0;
|
|
143
301
|
const headerPaddingVertical = getVariableByName('section/header/padding/vertical', modes) || 0;
|
|
144
|
-
|
|
145
|
-
// Resolve section title tokens
|
|
146
302
|
const titleColor = getVariableByName('section/title/color', modes) || '#0f0d0a';
|
|
147
303
|
const titleFontSize = getVariableByName('section/title/fontSize', modes) || 18;
|
|
148
304
|
const titleLineHeight = getVariableByName('section/title/lineHeight', modes) || 20;
|
|
149
305
|
const titleFontFamily = getVariableByName('section/title/fontFamily', modes) || 'System';
|
|
150
306
|
const titleFontWeightRaw = getVariableByName('section/title/fontWeight', modes) || 800;
|
|
151
307
|
const titleFontWeight = typeof titleFontWeightRaw === 'number' ? titleFontWeightRaw.toString() : titleFontWeightRaw;
|
|
152
|
-
|
|
153
|
-
// Resolve section support text tokens
|
|
154
308
|
const supportTextColor = getVariableByName('section/supportText/color', modes) || '#1f1a14';
|
|
155
309
|
const supportTextFontSize = getVariableByName('section/supportText/fontSize', modes) || 14;
|
|
156
310
|
const supportTextLineHeight = getVariableByName('section/supportText/lineHeight', modes) || 18;
|
|
157
311
|
const supportTextFontFamily = getVariableByName('section/supportText/fontFamily', modes) || 'System';
|
|
158
312
|
const supportTextFontWeightRaw = getVariableByName('section/supportText/fontWeight', modes) || 500;
|
|
159
313
|
const supportTextFontWeight = typeof supportTextFontWeightRaw === 'number' ? supportTextFontWeightRaw.toString() : supportTextFontWeightRaw;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
fontWeight: supportTextFontWeight
|
|
314
|
+
return {
|
|
315
|
+
containerStyle: {
|
|
316
|
+
backgroundColor,
|
|
317
|
+
paddingHorizontal,
|
|
318
|
+
paddingVertical,
|
|
319
|
+
borderRadius: radius,
|
|
320
|
+
gap: sectionGap
|
|
321
|
+
},
|
|
322
|
+
headerStyle: {
|
|
323
|
+
paddingHorizontal: headerPaddingHorizontal,
|
|
324
|
+
paddingVertical: headerPaddingVertical,
|
|
325
|
+
gap: headerGap
|
|
326
|
+
},
|
|
327
|
+
titleStyle: {
|
|
328
|
+
flex: 1,
|
|
329
|
+
color: titleColor,
|
|
330
|
+
fontSize: titleFontSize,
|
|
331
|
+
lineHeight: titleLineHeight,
|
|
332
|
+
fontFamily: titleFontFamily,
|
|
333
|
+
fontWeight: titleFontWeight
|
|
334
|
+
},
|
|
335
|
+
supportTextStyle: {
|
|
336
|
+
color: supportTextColor,
|
|
337
|
+
fontSize: supportTextFontSize,
|
|
338
|
+
lineHeight: supportTextLineHeight,
|
|
339
|
+
fontFamily: supportTextFontFamily,
|
|
340
|
+
fontWeight: supportTextFontWeight
|
|
341
|
+
},
|
|
342
|
+
sectionGap,
|
|
343
|
+
slotGap
|
|
191
344
|
};
|
|
345
|
+
}
|
|
346
|
+
function Section({
|
|
347
|
+
title = 'Section title',
|
|
348
|
+
supportText = 'Section support text',
|
|
349
|
+
showSupportText = true,
|
|
350
|
+
slot,
|
|
351
|
+
slotDirection = 'row',
|
|
352
|
+
modes = EMPTY_MODES,
|
|
353
|
+
onPress,
|
|
354
|
+
style,
|
|
355
|
+
// accessibilityLabel is intentionally accepted on the type for API
|
|
356
|
+
// back-compat, but the inner Pressable/View deliberately pass
|
|
357
|
+
// `accessibilityLabel={undefined}` (the title Text carries the label
|
|
358
|
+
// instead). Prefix to satisfy the unused-var lint while keeping the prop
|
|
359
|
+
// shape unchanged.
|
|
360
|
+
accessibilityLabel: _accessibilityLabel,
|
|
361
|
+
accessibilityHint,
|
|
362
|
+
webAccessibilityProps,
|
|
363
|
+
...rest
|
|
364
|
+
}) {
|
|
365
|
+
// Focus and hover are still mirrored in React because they are visible,
|
|
366
|
+
// sustained states (web-only in practice). The setters are gated so they
|
|
367
|
+
// never fire on native — keeping the component render-free during touch.
|
|
368
|
+
// Press is handled imperatively via the `Pressable` style callback so a
|
|
369
|
+
// scroll-cancelled touch never schedules a React render.
|
|
370
|
+
const [isHeaderFocused, setIsHeaderFocused] = useState(false);
|
|
371
|
+
const [isHeaderHovered, setIsHeaderHovered] = useState(false);
|
|
192
372
|
|
|
193
|
-
//
|
|
194
|
-
|
|
373
|
+
// Mirror user handlers in a ref so our wrappers can stay referentially
|
|
374
|
+
// stable. Without this, every parent re-render would hand Pressable fresh
|
|
375
|
+
// function identities and re-bind every event.
|
|
376
|
+
const userHandlersRef = useRef({});
|
|
377
|
+
userHandlersRef.current.onPressIn = rest?.onPressIn;
|
|
378
|
+
userHandlersRef.current.onPressOut = rest?.onPressOut;
|
|
379
|
+
userHandlersRef.current.onHoverIn = rest?.onHoverIn;
|
|
380
|
+
userHandlersRef.current.onHoverOut = rest?.onHoverOut;
|
|
381
|
+
userHandlersRef.current.onFocus = rest?.onFocus;
|
|
382
|
+
userHandlersRef.current.onBlur = rest?.onBlur;
|
|
383
|
+
const tokens = useMemo(() => resolveSectionTokens(modes), [modes]);
|
|
195
384
|
|
|
196
385
|
// Get web platform support props (only used when onPress is defined)
|
|
197
386
|
const webProps = usePressableWebSupport({
|
|
@@ -205,7 +394,7 @@ function Section({
|
|
|
205
394
|
children: [/*#__PURE__*/_jsxs(View, {
|
|
206
395
|
style: headerWrapStyle,
|
|
207
396
|
children: [/*#__PURE__*/_jsx(Text, {
|
|
208
|
-
style: titleStyle,
|
|
397
|
+
style: tokens.titleStyle,
|
|
209
398
|
numberOfLines: 1,
|
|
210
399
|
accessibilityElementsHidden: true,
|
|
211
400
|
importantForAccessibility: "no",
|
|
@@ -215,16 +404,49 @@ function Section({
|
|
|
215
404
|
modes: modes
|
|
216
405
|
})]
|
|
217
406
|
}), showSupportText && /*#__PURE__*/_jsx(Text, {
|
|
218
|
-
style: supportTextStyle,
|
|
407
|
+
style: tokens.supportTextStyle,
|
|
219
408
|
numberOfLines: 2,
|
|
220
409
|
accessibilityElementsHidden: true,
|
|
221
410
|
importantForAccessibility: "no",
|
|
222
411
|
children: supportText
|
|
223
412
|
})]
|
|
224
413
|
});
|
|
414
|
+
|
|
415
|
+
// Stable handler identities. User handlers are read through the ref so
|
|
416
|
+
// these wrappers don't need new identities each render.
|
|
417
|
+
const handlePressIn = useCallback(e => {
|
|
418
|
+
userHandlersRef.current.onPressIn?.(e);
|
|
419
|
+
}, []);
|
|
420
|
+
const handlePressOut = useCallback(e => {
|
|
421
|
+
userHandlersRef.current.onPressOut?.(e);
|
|
422
|
+
}, []);
|
|
423
|
+
const handleHoverIn = useCallback(e => {
|
|
424
|
+
if (IS_WEB) setIsHeaderHovered(true);
|
|
425
|
+
userHandlersRef.current.onHoverIn?.(e);
|
|
426
|
+
}, []);
|
|
427
|
+
const handleHoverOut = useCallback(e => {
|
|
428
|
+
if (IS_WEB) setIsHeaderHovered(false);
|
|
429
|
+
userHandlersRef.current.onHoverOut?.(e);
|
|
430
|
+
}, []);
|
|
431
|
+
const handleFocus = useCallback(e => {
|
|
432
|
+
if (IS_WEB) setIsHeaderFocused(true);
|
|
433
|
+
userHandlersRef.current.onFocus?.(e);
|
|
434
|
+
}, []);
|
|
435
|
+
const handleBlur = useCallback(e => {
|
|
436
|
+
if (IS_WEB) setIsHeaderFocused(false);
|
|
437
|
+
userHandlersRef.current.onBlur?.(e);
|
|
438
|
+
}, []);
|
|
439
|
+
|
|
440
|
+
// The pressed visual is applied by the host view directly through the
|
|
441
|
+
// Pressable style callback — no React render is scheduled. We still want
|
|
442
|
+
// the (constant) hover style on web so we keep it in the array.
|
|
443
|
+
const headerStyleCallback = useCallback(({
|
|
444
|
+
pressed
|
|
445
|
+
}) => [tokens.headerStyle, pressed ? headerPressedStyle : null, isHeaderHovered ? headerHoverStyle : null, isHeaderFocused ? headerFocusStyle : null], [tokens.headerStyle, isHeaderHovered, isHeaderFocused]);
|
|
446
|
+
const containerStyleArray = useMemo(() => [tokens.containerStyle, style], [tokens.containerStyle, style]);
|
|
225
447
|
return /*#__PURE__*/_jsxs(View, {
|
|
226
|
-
style:
|
|
227
|
-
...(
|
|
448
|
+
style: containerStyleArray,
|
|
449
|
+
...(IS_WEB ? {
|
|
228
450
|
accessibilityRole: 'region'
|
|
229
451
|
} : undefined),
|
|
230
452
|
accessibilityLabel: undefined,
|
|
@@ -233,52 +455,59 @@ function Section({
|
|
|
233
455
|
children: [onPress ? /*#__PURE__*/_jsx(Pressable, {
|
|
234
456
|
accessibilityRole: "button",
|
|
235
457
|
accessibilityLabel: undefined,
|
|
236
|
-
accessibilityHint: accessibilityHint ||
|
|
458
|
+
accessibilityHint: accessibilityHint || 'Opens section details',
|
|
237
459
|
onPress: onPress,
|
|
238
|
-
onPressIn:
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
onFocus: e => {
|
|
247
|
-
setIsHeaderFocused(true);
|
|
248
|
-
rest?.onFocus?.(e);
|
|
249
|
-
},
|
|
250
|
-
onBlur: e => {
|
|
251
|
-
setIsHeaderFocused(false);
|
|
252
|
-
rest?.onBlur?.(e);
|
|
253
|
-
},
|
|
254
|
-
onHoverIn: e => {
|
|
255
|
-
setIsHeaderHovered(true);
|
|
256
|
-
rest?.onHoverIn?.(e);
|
|
257
|
-
},
|
|
258
|
-
onHoverOut: e => {
|
|
259
|
-
setIsHeaderHovered(false);
|
|
260
|
-
rest?.onHoverOut?.(e);
|
|
261
|
-
},
|
|
262
|
-
style: ({
|
|
263
|
-
pressed
|
|
264
|
-
}) => [headerStyle, pressed ? headerPressedStyle : null, headerHoverStyle, headerFocusStyle],
|
|
460
|
+
onPressIn: handlePressIn,
|
|
461
|
+
onPressOut: handlePressOut,
|
|
462
|
+
onFocus: handleFocus,
|
|
463
|
+
onBlur: handleBlur,
|
|
464
|
+
onHoverIn: handleHoverIn,
|
|
465
|
+
onHoverOut: handleHoverOut,
|
|
466
|
+
unstable_pressDelay: HEADER_PRESS_DELAY,
|
|
467
|
+
style: headerStyleCallback,
|
|
265
468
|
...webProps,
|
|
266
469
|
children: headerContent
|
|
267
470
|
}) : /*#__PURE__*/_jsx(View, {
|
|
268
|
-
style: headerStyle,
|
|
471
|
+
style: tokens.headerStyle,
|
|
269
472
|
children: headerContent
|
|
270
|
-
}), slot &&
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
gap: slotGap
|
|
277
|
-
},
|
|
278
|
-
children: cloneChildrenWithModes(flattenChildren(slot), modes)
|
|
473
|
+
}), slot && /*#__PURE__*/_jsx(SectionSlot, {
|
|
474
|
+
slot: slot,
|
|
475
|
+
modes: modes,
|
|
476
|
+
direction: slotDirection,
|
|
477
|
+
rowGap: tokens.sectionGap,
|
|
478
|
+
columnGap: tokens.slotGap
|
|
279
479
|
})]
|
|
280
480
|
});
|
|
281
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* Internal helper that processes the slot children once per (slot, modes) pair
|
|
484
|
+
* and dispatches to the row (SlotGrid) or column layout. Splitting this out of
|
|
485
|
+
* `Section` lets the parent re-render (e.g. for header press/hover state)
|
|
486
|
+
* without re-walking the slot tree via `cloneChildrenWithModes`.
|
|
487
|
+
*/
|
|
488
|
+
function SectionSlot({
|
|
489
|
+
slot,
|
|
490
|
+
modes,
|
|
491
|
+
direction,
|
|
492
|
+
rowGap,
|
|
493
|
+
columnGap
|
|
494
|
+
}) {
|
|
495
|
+
const processed = useMemo(() => cloneChildrenWithModes(flattenChildren(slot), modes), [slot, modes]);
|
|
496
|
+
const columnContainerStyle = useMemo(() => ({
|
|
497
|
+
flexDirection: 'column',
|
|
498
|
+
gap: columnGap
|
|
499
|
+
}), [columnGap]);
|
|
500
|
+
if (direction === 'row') {
|
|
501
|
+
return /*#__PURE__*/_jsx(SlotGrid, {
|
|
502
|
+
items: processed,
|
|
503
|
+
gap: rowGap
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
return /*#__PURE__*/_jsx(View, {
|
|
507
|
+
style: columnContainerStyle,
|
|
508
|
+
children: processed
|
|
509
|
+
});
|
|
510
|
+
}
|
|
282
511
|
/**
|
|
283
512
|
* Section.Bento component that mirrors the Figma "Section.Bento" component.
|
|
284
513
|
*
|
|
@@ -301,30 +530,88 @@ function Section({
|
|
|
301
530
|
* @param {string} [props.accessibilityLabel] - Accessibility label for the section
|
|
302
531
|
* @param {string} [props.accessibilityHint] - Additional accessibility hint for screen readers
|
|
303
532
|
*/
|
|
533
|
+
const sectionBentoUpiRowStyle = {
|
|
534
|
+
flexDirection: 'row',
|
|
535
|
+
gap: 8
|
|
536
|
+
};
|
|
304
537
|
function SectionBento({
|
|
305
538
|
navSlot,
|
|
306
539
|
upiSlot,
|
|
307
|
-
modes =
|
|
540
|
+
modes = EMPTY_MODES,
|
|
308
541
|
style,
|
|
309
|
-
|
|
542
|
+
// Same rationale as Section: accepted on the type but unused internally.
|
|
543
|
+
accessibilityLabel: _accessibilityLabel,
|
|
310
544
|
accessibilityHint,
|
|
545
|
+
collapsedCount = SLOT_GRID_MAX_COLUMNS,
|
|
546
|
+
defaultExpanded = false,
|
|
547
|
+
expanded: controlledExpanded,
|
|
548
|
+
onExpandedChange,
|
|
549
|
+
toggleMoreLabel = 'More',
|
|
550
|
+
toggleLessLabel = 'Less',
|
|
551
|
+
toggleMoreIcon = 'ic_chevron_down',
|
|
552
|
+
toggleLessIcon = 'ic_chevron_up',
|
|
553
|
+
renderToggle,
|
|
311
554
|
...rest
|
|
312
555
|
}) {
|
|
313
|
-
// Resolve section container tokens
|
|
314
556
|
const backgroundColor = getVariableByName('section/background/color', modes) || '#ffffff';
|
|
315
557
|
const gap = getVariableByName('section/gap', modes) || 12;
|
|
316
558
|
const paddingHorizontal = getVariableByName('section/padding/horizontal', modes) || 12;
|
|
317
559
|
const paddingVertical = getVariableByName('section/padding/vertical', modes) || 16;
|
|
318
560
|
const radius = getVariableByName('section/radius', modes) || 12;
|
|
319
|
-
const containerStyle = {
|
|
561
|
+
const containerStyle = useMemo(() => ({
|
|
320
562
|
backgroundColor,
|
|
321
563
|
paddingHorizontal,
|
|
322
564
|
paddingVertical,
|
|
323
565
|
borderRadius: radius,
|
|
324
566
|
gap
|
|
325
|
-
};
|
|
326
|
-
const processedNavSlot = navSlot ? cloneChildrenWithModes(flattenChildren(navSlot), modes) : null;
|
|
327
|
-
const processedUpiSlot = upiSlot ? cloneChildrenWithModes(flattenChildren(upiSlot), modes) : null;
|
|
567
|
+
}), [backgroundColor, paddingHorizontal, paddingVertical, radius, gap]);
|
|
568
|
+
const processedNavSlot = useMemo(() => navSlot ? cloneChildrenWithModes(flattenChildren(navSlot), modes) : null, [navSlot, modes]);
|
|
569
|
+
const processedUpiSlot = useMemo(() => upiSlot ? cloneChildrenWithModes(flattenChildren(upiSlot), modes) : null, [upiSlot, modes]);
|
|
570
|
+
|
|
571
|
+
// `canExpand` is true iff there are strictly more real items than fit into
|
|
572
|
+
// the collapsed grid. When `allRealItems.length === collapsedCount` we just
|
|
573
|
+
// render them all with no toggle — identical to the pre-feature behavior.
|
|
574
|
+
const allRealItems = useMemo(() => processedNavSlot ?? [], [processedNavSlot]);
|
|
575
|
+
const canExpand = allRealItems.length > collapsedCount;
|
|
576
|
+
const isControlled = controlledExpanded !== undefined;
|
|
577
|
+
const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
|
|
578
|
+
const expanded = isControlled ? controlledExpanded : internalExpanded;
|
|
579
|
+
|
|
580
|
+
// Mirror onExpandedChange in a ref so `toggle` can stay referentially stable.
|
|
581
|
+
const onExpandedChangeRef = useRef(onExpandedChange);
|
|
582
|
+
onExpandedChangeRef.current = onExpandedChange;
|
|
583
|
+
const isControlledRef = useRef(isControlled);
|
|
584
|
+
isControlledRef.current = isControlled;
|
|
585
|
+
const expandedRef = useRef(expanded);
|
|
586
|
+
expandedRef.current = expanded;
|
|
587
|
+
const toggle = useCallback(() => {
|
|
588
|
+
const next = !expandedRef.current;
|
|
589
|
+
if (!isControlledRef.current) {
|
|
590
|
+
setInternalExpanded(next);
|
|
591
|
+
}
|
|
592
|
+
onExpandedChangeRef.current?.(next);
|
|
593
|
+
}, []);
|
|
594
|
+
const navGridItems = useMemo(() => {
|
|
595
|
+
if (!canExpand) {
|
|
596
|
+
return allRealItems;
|
|
597
|
+
}
|
|
598
|
+
// Leave the last collapsed slot for the toggle cell.
|
|
599
|
+
const visibleRealItems = expanded ? allRealItems : allRealItems.slice(0, collapsedCount - 1);
|
|
600
|
+
const toggleNode = renderToggle ? renderToggle({
|
|
601
|
+
expanded,
|
|
602
|
+
toggle
|
|
603
|
+
}) : /*#__PURE__*/_jsx(DefaultBentoToggle, {
|
|
604
|
+
expanded: expanded,
|
|
605
|
+
onPress: toggle,
|
|
606
|
+
modes: modes,
|
|
607
|
+
moreLabel: toggleMoreLabel,
|
|
608
|
+
lessLabel: toggleLessLabel,
|
|
609
|
+
moreIcon: toggleMoreIcon,
|
|
610
|
+
lessIcon: toggleLessIcon,
|
|
611
|
+
extraCount: allRealItems.length - (collapsedCount - 1)
|
|
612
|
+
});
|
|
613
|
+
return [...visibleRealItems, toggleNode];
|
|
614
|
+
}, [canExpand, allRealItems, expanded, collapsedCount, renderToggle, toggle, modes, toggleMoreLabel, toggleLessLabel, toggleMoreIcon, toggleLessIcon]);
|
|
328
615
|
return /*#__PURE__*/_jsxs(View, {
|
|
329
616
|
style: [containerStyle, style],
|
|
330
617
|
...(Platform.OS === 'web' ? {
|
|
@@ -333,19 +620,66 @@ function SectionBento({
|
|
|
333
620
|
accessibilityLabel: undefined,
|
|
334
621
|
accessibilityHint: accessibilityHint,
|
|
335
622
|
...rest,
|
|
336
|
-
children: [
|
|
337
|
-
items:
|
|
338
|
-
gap: gap
|
|
623
|
+
children: [navGridItems.length > 0 && /*#__PURE__*/_jsx(SlotGrid, {
|
|
624
|
+
items: navGridItems,
|
|
625
|
+
gap: gap,
|
|
626
|
+
...(canExpand ? {
|
|
627
|
+
animateExtrasFromIndex: collapsedCount,
|
|
628
|
+
animateContainerLayout: true
|
|
629
|
+
} : null)
|
|
339
630
|
}), processedUpiSlot && /*#__PURE__*/_jsx(View, {
|
|
340
|
-
style:
|
|
341
|
-
flexDirection: 'row',
|
|
342
|
-
gap: 8
|
|
343
|
-
},
|
|
631
|
+
style: sectionBentoUpiRowStyle,
|
|
344
632
|
children: processedUpiSlot
|
|
345
633
|
})]
|
|
346
634
|
});
|
|
347
635
|
}
|
|
348
636
|
|
|
637
|
+
// ---------------------------------------------------------------------------
|
|
638
|
+
// DefaultBentoToggle — internal toggle cell rendered by `SectionBento` when no
|
|
639
|
+
// `renderToggle` prop is provided. Uses a vertical `ListItem` so the cell
|
|
640
|
+
// matches the visual rhythm of the surrounding nav items in every mode.
|
|
641
|
+
//
|
|
642
|
+
// Two icons (`ic_chevron_down` / `ic_chevron_up`) are used instead of a
|
|
643
|
+
// rotating single icon, because the toggle cell is reconciled by position
|
|
644
|
+
// (collapsed: end of row 1; expanded: end of last row), so any persistent
|
|
645
|
+
// shared-value-driven rotation would lose its anchor across toggles.
|
|
646
|
+
// ---------------------------------------------------------------------------
|
|
647
|
+
|
|
648
|
+
function DefaultBentoToggle({
|
|
649
|
+
expanded,
|
|
650
|
+
onPress,
|
|
651
|
+
modes,
|
|
652
|
+
moreLabel,
|
|
653
|
+
lessLabel,
|
|
654
|
+
moreIcon,
|
|
655
|
+
lessIcon,
|
|
656
|
+
extraCount
|
|
657
|
+
}) {
|
|
658
|
+
const label = expanded ? lessLabel : moreLabel;
|
|
659
|
+
const iconName = expanded ? lessIcon : moreIcon;
|
|
660
|
+
const accessibilityState = useMemo(() => ({
|
|
661
|
+
expanded
|
|
662
|
+
}), [expanded]);
|
|
663
|
+
const webAccessibilityProps = useMemo(() => ({
|
|
664
|
+
ariaExpanded: expanded
|
|
665
|
+
}), [expanded]);
|
|
666
|
+
const accessibilityHint = expanded ? `Hides ${extraCount} additional ${extraCount === 1 ? 'action' : 'actions'}` : `Shows ${extraCount} additional ${extraCount === 1 ? 'action' : 'actions'}`;
|
|
667
|
+
return /*#__PURE__*/_jsx(ListItem, {
|
|
668
|
+
layout: "Vertical",
|
|
669
|
+
supportText: label,
|
|
670
|
+
leading: /*#__PURE__*/_jsx(IconCapsule, {
|
|
671
|
+
iconName: iconName,
|
|
672
|
+
modes: modes
|
|
673
|
+
}),
|
|
674
|
+
modes: modes,
|
|
675
|
+
onPress: onPress,
|
|
676
|
+
accessibilityLabel: label,
|
|
677
|
+
accessibilityHint: accessibilityHint,
|
|
678
|
+
accessibilityState: accessibilityState,
|
|
679
|
+
webAccessibilityProps: webAccessibilityProps
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
349
683
|
// Attach Bento as a property of Section using namespace pattern
|
|
350
684
|
Section.Bento = SectionBento;
|
|
351
685
|
export default Section;
|