jfs-components 0.0.73 → 0.0.77
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 +115 -6
- package/lib/commonjs/components/AccountCard/AccountCard.js +247 -0
- package/lib/commonjs/components/ActionFooter/ActionFooter.js +147 -82
- package/lib/commonjs/components/AppBar/AppBar.js +17 -11
- package/lib/commonjs/components/Avatar/Avatar.js +20 -0
- package/lib/commonjs/components/Badge/Badge.js +23 -0
- package/lib/commonjs/components/Button/Button.js +37 -0
- package/lib/commonjs/components/CardBankAccount/CardBankAccount.js +18 -2
- package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +40 -25
- package/lib/commonjs/components/Dropdown/Dropdown.js +214 -0
- package/lib/commonjs/components/DropdownInput/DropdownInput.js +542 -0
- package/lib/commonjs/components/FormField/FormField.js +328 -178
- package/lib/commonjs/components/IconButton/IconButton.js +20 -0
- package/lib/commonjs/components/Image/Image.js +26 -1
- package/lib/commonjs/components/LottieIntroBlock/LottieIntroBlock.js +150 -0
- package/lib/commonjs/components/LottiePlayer/LottiePlayer.js +116 -0
- package/lib/commonjs/components/LottiePlayer/LottiePlayer.web.js +82 -0
- package/lib/commonjs/components/LottiePlayer/loadNativeLottieView.js +74 -0
- package/lib/commonjs/components/LottiePlayer/loadWebLottieView.js +50 -0
- package/lib/commonjs/components/PageHero/PageHero.js +189 -0
- package/lib/commonjs/components/PoweredByLabel/PoweredByLabel.js +135 -0
- package/lib/commonjs/components/PoweredByLabel/finvu.png +0 -0
- package/lib/commonjs/components/RechargeCard/RechargeCard.js +32 -17
- package/lib/commonjs/components/Text/Text.js +40 -3
- package/lib/commonjs/components/Tooltip/Tooltip.js +34 -27
- package/lib/commonjs/components/index.js +67 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/Icon.js +16 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/commonjs/index.js +12 -0
- package/lib/commonjs/skeleton/Skeleton.js +234 -0
- package/lib/commonjs/skeleton/SkeletonGroup.js +140 -0
- package/lib/commonjs/skeleton/index.js +58 -0
- package/lib/commonjs/skeleton/shimmer-tokens.js +189 -0
- package/lib/commonjs/skeleton/useReducedMotion.js +64 -0
- package/lib/module/components/AccountCard/AccountCard.js +241 -0
- package/lib/module/components/ActionFooter/ActionFooter.js +146 -82
- package/lib/module/components/AppBar/AppBar.js +17 -11
- package/lib/module/components/Avatar/Avatar.js +19 -0
- package/lib/module/components/Badge/Badge.js +23 -0
- package/lib/module/components/Button/Button.js +37 -0
- package/lib/module/components/CardBankAccount/CardBankAccount.js +17 -2
- package/lib/module/components/CheckboxItem/CheckboxItem.js +41 -26
- package/lib/module/components/Dropdown/Dropdown.js +206 -0
- package/lib/module/components/DropdownInput/DropdownInput.js +536 -0
- package/lib/module/components/FormField/FormField.js +330 -180
- package/lib/module/components/IconButton/IconButton.js +20 -0
- package/lib/module/components/Image/Image.js +25 -1
- package/lib/module/components/LottieIntroBlock/LottieIntroBlock.js +144 -0
- package/lib/module/components/LottiePlayer/LottiePlayer.js +111 -0
- package/lib/module/components/LottiePlayer/LottiePlayer.web.js +77 -0
- package/lib/module/components/LottiePlayer/loadNativeLottieView.js +69 -0
- package/lib/module/components/LottiePlayer/loadWebLottieView.js +45 -0
- package/lib/module/components/PageHero/PageHero.js +183 -0
- package/lib/module/components/PoweredByLabel/PoweredByLabel.js +130 -0
- package/lib/module/components/PoweredByLabel/finvu.png +0 -0
- package/lib/module/components/RechargeCard/RechargeCard.js +33 -17
- package/lib/module/components/Text/Text.js +40 -3
- package/lib/module/components/Tooltip/Tooltip.js +34 -27
- package/lib/module/components/index.js +8 -1
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/Icon.js +16 -0
- package/lib/module/icons/registry.js +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/skeleton/Skeleton.js +229 -0
- package/lib/module/skeleton/SkeletonGroup.js +133 -0
- package/lib/module/skeleton/index.js +6 -0
- package/lib/module/skeleton/shimmer-tokens.js +181 -0
- package/lib/module/skeleton/useReducedMotion.js +61 -0
- package/lib/typescript/src/components/AccountCard/AccountCard.d.ts +81 -0
- package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +26 -21
- package/lib/typescript/src/components/Avatar/Avatar.d.ts +7 -1
- package/lib/typescript/src/components/Badge/Badge.d.ts +7 -1
- package/lib/typescript/src/components/Button/Button.d.ts +8 -1
- package/lib/typescript/src/components/CardBankAccount/CardBankAccount.d.ts +9 -2
- package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +18 -2
- package/lib/typescript/src/components/Dropdown/Dropdown.d.ts +62 -0
- package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +107 -0
- package/lib/typescript/src/components/FormField/FormField.d.ts +76 -19
- package/lib/typescript/src/components/IconButton/IconButton.d.ts +7 -1
- package/lib/typescript/src/components/Image/Image.d.ts +8 -1
- package/lib/typescript/src/components/LottieIntroBlock/LottieIntroBlock.d.ts +58 -0
- package/lib/typescript/src/components/LottiePlayer/LottiePlayer.d.ts +85 -0
- package/lib/typescript/src/components/LottiePlayer/LottiePlayer.web.d.ts +28 -0
- package/lib/typescript/src/components/LottiePlayer/loadNativeLottieView.d.ts +11 -0
- package/lib/typescript/src/components/LottiePlayer/loadWebLottieView.d.ts +11 -0
- package/lib/typescript/src/components/PageHero/PageHero.d.ts +79 -0
- package/lib/typescript/src/components/PoweredByLabel/PoweredByLabel.d.ts +70 -0
- package/lib/typescript/src/components/Text/Text.d.ts +31 -2
- package/lib/typescript/src/components/Tooltip/Tooltip.d.ts +13 -2
- package/lib/typescript/src/components/index.d.ts +8 -1
- package/lib/typescript/src/icons/Icon.d.ts +7 -1
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/skeleton/Skeleton.d.ts +60 -0
- package/lib/typescript/src/skeleton/SkeletonGroup.d.ts +78 -0
- package/lib/typescript/src/skeleton/index.d.ts +5 -0
- package/lib/typescript/src/skeleton/shimmer-tokens.d.ts +160 -0
- package/lib/typescript/src/skeleton/useReducedMotion.d.ts +15 -0
- package/package.json +11 -3
- package/src/components/AccountCard/AccountCard.tsx +376 -0
- package/src/components/ActionFooter/ActionFooter.tsx +152 -86
- package/src/components/AppBar/AppBar.tsx +25 -14
- package/src/components/Avatar/Avatar.tsx +26 -0
- package/src/components/Badge/Badge.tsx +27 -0
- package/src/components/Button/Button.tsx +40 -0
- package/src/components/CardBankAccount/CardBankAccount.tsx +29 -3
- package/src/components/CheckboxItem/CheckboxItem.tsx +65 -30
- package/src/components/Dropdown/Dropdown.tsx +331 -0
- package/src/components/DropdownInput/DropdownInput.tsx +819 -0
- package/src/components/FormField/FormField.tsx +542 -215
- package/src/components/IconButton/IconButton.tsx +27 -0
- package/src/components/Image/Image.tsx +25 -0
- package/src/components/LottieIntroBlock/LottieIntroBlock.tsx +202 -0
- package/src/components/LottiePlayer/LottiePlayer.tsx +145 -0
- package/src/components/LottiePlayer/LottiePlayer.web.tsx +94 -0
- package/src/components/LottiePlayer/loadNativeLottieView.tsx +87 -0
- package/src/components/LottiePlayer/loadWebLottieView.tsx +64 -0
- package/src/components/PageHero/PageHero.tsx +257 -0
- package/src/components/PoweredByLabel/PoweredByLabel.tsx +221 -0
- package/src/components/PoweredByLabel/finvu.png +0 -0
- package/src/components/RechargeCard/RechargeCard.tsx +32 -24
- package/src/components/Text/Text.tsx +78 -3
- package/src/components/Tooltip/Tooltip.tsx +50 -25
- package/src/components/index.ts +16 -1
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/icons/Icon.tsx +17 -0
- package/src/icons/registry.ts +1 -1
- package/src/index.ts +1 -0
- package/src/skeleton/Skeleton.tsx +298 -0
- package/src/skeleton/SkeletonGroup.tsx +193 -0
- package/src/skeleton/index.ts +10 -0
- package/src/skeleton/shimmer-tokens.ts +221 -0
- package/src/skeleton/useReducedMotion.ts +72 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import React from 'react'
|
|
1
|
+
import React, { useMemo } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
|
+
Platform,
|
|
4
5
|
type ViewStyle,
|
|
5
6
|
type StyleProp,
|
|
6
|
-
Platform,
|
|
7
7
|
} from 'react-native'
|
|
8
8
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
9
9
|
import { EMPTY_MODES, cloneChildrenWithModes, flattenChildren } from '../../utils/react-utils'
|
|
@@ -12,48 +12,118 @@ import IconButton from '../IconButton/IconButton'
|
|
|
12
12
|
export type ActionFooterProps = {
|
|
13
13
|
/**
|
|
14
14
|
* Content to render inside the action footer slot.
|
|
15
|
-
* Typically includes IconButton and Button components.
|
|
15
|
+
* Typically includes `IconButton` and `Button` components.
|
|
16
|
+
* `IconButton` children keep their intrinsic square size; everything else
|
|
17
|
+
* is auto-stretched to share the remaining horizontal space equally.
|
|
16
18
|
*/
|
|
17
19
|
children?: React.ReactNode
|
|
18
20
|
/**
|
|
19
21
|
* Mode configuration passed to the token resolver.
|
|
20
|
-
*
|
|
22
|
+
* Automatically merged into every slot child via {@link cloneChildrenWithModes}
|
|
23
|
+
* so callers don't have to thread modes down by hand.
|
|
21
24
|
*/
|
|
22
25
|
modes?: Record<string, any>
|
|
23
26
|
/**
|
|
24
|
-
* Optional style overrides for the container
|
|
27
|
+
* Optional style overrides for the outer container.
|
|
25
28
|
*/
|
|
26
29
|
style?: StyleProp<ViewStyle>
|
|
27
30
|
/**
|
|
28
|
-
* Accessibility label for the footer region
|
|
31
|
+
* Accessibility label for the footer region (announced for the toolbar).
|
|
29
32
|
*/
|
|
30
33
|
accessibilityLabel?: string
|
|
31
34
|
}
|
|
32
35
|
|
|
36
|
+
const IS_WEB = Platform.OS === 'web'
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Yoga-safe stretch
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
//
|
|
42
|
+
// React Native (Yoga) interprets the `flex: 1` shorthand as
|
|
43
|
+
// { flexGrow: 1, flexShrink: 1, flexBasis: 0 }
|
|
44
|
+
// which is the *equal-share* variant. That is the correct math for what we
|
|
45
|
+
// want here (equal-width action buttons), BUT Yoga has a well-known foot-gun
|
|
46
|
+
// when this child sits inside a parent whose main-axis size hasn't been
|
|
47
|
+
// resolved yet on the first layout pass: the child collapses to 0 and the
|
|
48
|
+
// inner text gets clipped to "" before the parent ever measures.
|
|
49
|
+
//
|
|
50
|
+
// The defensive incantation used elsewhere in this codebase (see
|
|
51
|
+
// `CardCTA.leftWrap` and the `MediaCard.Header` fix in CHANGELOG.md) is to
|
|
52
|
+
// keep the equal-share math but explicitly clamp `minWidth` to 0 so Yoga
|
|
53
|
+
// always allows the child to participate in the shrink algorithm, even when
|
|
54
|
+
// the parent itself is in an undetermined state. Combined with explicit
|
|
55
|
+
// `flexGrow`/`flexShrink`/`flexBasis` (NOT the `flex` shorthand) this
|
|
56
|
+
// renders correctly on iOS, Android, and Web — and crucially never produces
|
|
57
|
+
// the "buttons render as empty pills" failure mode the previous version had
|
|
58
|
+
// on iOS dev clients.
|
|
59
|
+
const STRETCH_STYLE: ViewStyle = {
|
|
60
|
+
flexGrow: 1,
|
|
61
|
+
flexShrink: 1,
|
|
62
|
+
flexBasis: 0,
|
|
63
|
+
minWidth: 0,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Platform-specific drop shadow. Web boxShadow can't go through
|
|
67
|
+
// Platform.select (RN's typed surface doesn't include it) so we keep it as a
|
|
68
|
+
// separate constant and append it below.
|
|
69
|
+
const NATIVE_SHADOW = Platform.select<ViewStyle>({
|
|
70
|
+
ios: {
|
|
71
|
+
shadowColor: '#0c0d10',
|
|
72
|
+
shadowOffset: { width: 0, height: -12 },
|
|
73
|
+
shadowOpacity: 0.16,
|
|
74
|
+
shadowRadius: 24,
|
|
75
|
+
},
|
|
76
|
+
android: {
|
|
77
|
+
elevation: 16,
|
|
78
|
+
},
|
|
79
|
+
default: {},
|
|
80
|
+
}) as ViewStyle
|
|
81
|
+
|
|
82
|
+
const WEB_SHADOW = IS_WEB
|
|
83
|
+
? ({
|
|
84
|
+
boxShadow:
|
|
85
|
+
'0px -12px 24px 0px rgba(12, 13, 16, 0.12), 0px -16px 48px 0px rgba(12, 13, 16, 0.16)',
|
|
86
|
+
} as ViewStyle)
|
|
87
|
+
: null
|
|
88
|
+
|
|
89
|
+
// The runtime token a slot child must equal (by reference) to be treated as
|
|
90
|
+
// an IconButton. `IconButton` is exported wrapped in `React.memo`, so the
|
|
91
|
+
// element.type identity comparison works for both `<IconButton />` from the
|
|
92
|
+
// same module and any `React.memo`-wrapped re-export. The fallback check
|
|
93
|
+
// (`type.type === IconButton`) catches one extra layer of `forwardRef` /
|
|
94
|
+
// `memo` wrapping which can happen when consumers re-export the component.
|
|
95
|
+
function isIconButtonElement(element: React.ReactElement<any>): boolean {
|
|
96
|
+
const t: any = element.type
|
|
97
|
+
if (t === IconButton) return true
|
|
98
|
+
if (t && typeof t === 'object' && t.type === IconButton) return true
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
|
|
33
102
|
/**
|
|
34
|
-
* ActionFooter
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
103
|
+
* ActionFooter — a sticky bottom container for primary screen actions.
|
|
104
|
+
*
|
|
105
|
+
* Layout contract:
|
|
106
|
+
* - The outer container stretches horizontally (`alignSelf: 'stretch'`) so
|
|
107
|
+
* it fills the parent regardless of whether the parent is a flex column,
|
|
108
|
+
* a ScrollView contentContainer, or a plain View.
|
|
109
|
+
* - The inner slot is a single row sized by its tallest child. It does NOT
|
|
110
|
+
* use `flex: 1` — that previously caused the row to collapse to zero on
|
|
111
|
+
* the first Yoga pass on native, taking the button labels with it.
|
|
112
|
+
* - `IconButton` children keep their intrinsic square size.
|
|
113
|
+
* - Every other child is auto-stretched with the Yoga-safe stretch style
|
|
114
|
+
* above so two `<Button>` siblings render at equal width on iOS, Android,
|
|
115
|
+
* and Web.
|
|
116
|
+
*
|
|
117
|
+
* The `modes` prop is automatically pushed down to every slot child via
|
|
118
|
+
* {@link cloneChildrenWithModes}; explicit child-level modes win over the
|
|
119
|
+
* parent's modes.
|
|
120
|
+
*
|
|
49
121
|
* @example
|
|
50
122
|
* ```tsx
|
|
51
|
-
* // Basic usage - modes are automatically passed to all children.
|
|
52
|
-
* // Non-IconButton children (e.g., Button) are auto-stretched to fill.
|
|
53
123
|
* <ActionFooter modes={modes}>
|
|
54
124
|
* <IconButton iconName="ic_split" />
|
|
55
|
-
* <Button label="Request" />
|
|
56
|
-
* <Button label="Pay" />
|
|
125
|
+
* <Button label="Request" modes={{ AppearanceBrand: 'Secondary' }} />
|
|
126
|
+
* <Button label="Pay" modes={{ AppearanceBrand: 'Primary' }} />
|
|
57
127
|
* </ActionFooter>
|
|
58
128
|
* ```
|
|
59
129
|
*/
|
|
@@ -61,79 +131,75 @@ function ActionFooter({
|
|
|
61
131
|
children,
|
|
62
132
|
modes = EMPTY_MODES,
|
|
63
133
|
style,
|
|
64
|
-
accessibilityLabel
|
|
134
|
+
accessibilityLabel,
|
|
65
135
|
}: ActionFooterProps) {
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
136
|
+
// All token reads collapsed into a single useMemo keyed on `modes`. With
|
|
137
|
+
// the shared `EMPTY_MODES` default this resolves once for the common path
|
|
138
|
+
// and never re-allocates the container/slot style objects between renders.
|
|
139
|
+
const { containerStyle, slotStyle } = useMemo(() => {
|
|
140
|
+
const backgroundColor =
|
|
141
|
+
(getVariableByName('actionFooter/background', modes) ?? '#ffffff') as string
|
|
142
|
+
const gap = (getVariableByName('actionFooter/gap', modes) ?? 8) as number
|
|
143
|
+
const paddingHorizontal =
|
|
144
|
+
(getVariableByName('actionFooter/padding/horizontal', modes) ?? 16) as number
|
|
145
|
+
const paddingTop = (getVariableByName('actionFooter/padding/top', modes) ?? 10) as number
|
|
146
|
+
const paddingBottom = (getVariableByName('actionFooter/padding/bottom', modes) ?? 41) as number
|
|
72
147
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Web shadow using boxShadow (RNW supports this)
|
|
86
|
-
},
|
|
87
|
-
}) as ViewStyle
|
|
148
|
+
const container: ViewStyle = {
|
|
149
|
+
// `alignSelf: 'stretch'` is the cross-platform way to ask "fill the
|
|
150
|
+
// parent's cross axis" — in the common case (column parent) this gives
|
|
151
|
+
// us full-width without the caller needing to pass `width: '100%'`.
|
|
152
|
+
alignSelf: 'stretch',
|
|
153
|
+
backgroundColor,
|
|
154
|
+
paddingLeft: paddingHorizontal,
|
|
155
|
+
paddingRight: paddingHorizontal,
|
|
156
|
+
paddingTop,
|
|
157
|
+
paddingBottom,
|
|
158
|
+
...NATIVE_SHADOW,
|
|
159
|
+
}
|
|
88
160
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
161
|
+
const slot: ViewStyle = {
|
|
162
|
+
flexDirection: 'row',
|
|
163
|
+
// Vertically center the IconButton against the slightly taller Buttons
|
|
164
|
+
// so the row reads as a single optical baseline.
|
|
165
|
+
alignItems: 'center',
|
|
166
|
+
gap,
|
|
167
|
+
}
|
|
97
168
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
flexDirection: 'row',
|
|
101
|
-
alignItems: 'flex-start',
|
|
102
|
-
gap,
|
|
103
|
-
flex: 1,
|
|
104
|
-
}
|
|
169
|
+
return { containerStyle: container, slotStyle: slot }
|
|
170
|
+
}, [modes])
|
|
105
171
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const enhancedChildren = (
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
172
|
+
// Process children once per (children, modes) tuple:
|
|
173
|
+
// 1. Flatten Fragments so each action is its own keyed sibling.
|
|
174
|
+
// 2. Push `modes` down so callers don't have to thread it manually.
|
|
175
|
+
// 3. Auto-stretch every non-IconButton with the Yoga-safe stretch style.
|
|
176
|
+
//
|
|
177
|
+
// The result identity is stable across re-renders when the inputs don't
|
|
178
|
+
// change, which keeps the `React.memo`-wrapped Button/IconButton children
|
|
179
|
+
// from re-rendering for no reason.
|
|
180
|
+
const enhancedChildren = useMemo(() => {
|
|
181
|
+
const flat = flattenChildren(children)
|
|
182
|
+
const withModes = cloneChildrenWithModes(flat, modes) as React.ReactNode[]
|
|
183
|
+
return withModes.map((child, index) => {
|
|
184
|
+
if (!React.isValidElement(child)) return child
|
|
185
|
+
const element = child as React.ReactElement<any>
|
|
186
|
+
if (isIconButtonElement(element)) return element
|
|
187
|
+
return React.cloneElement(element, {
|
|
188
|
+
key: element.key ?? `action-footer-item-${index}`,
|
|
189
|
+
style: [STRETCH_STYLE, element.props.style],
|
|
190
|
+
})
|
|
122
191
|
})
|
|
123
|
-
})
|
|
192
|
+
}, [children, modes])
|
|
124
193
|
|
|
125
194
|
return (
|
|
126
195
|
<View
|
|
127
|
-
style={[containerStyle,
|
|
196
|
+
style={[containerStyle, WEB_SHADOW, style]}
|
|
128
197
|
accessibilityRole="toolbar"
|
|
129
|
-
accessibilityLabel={
|
|
198
|
+
accessibilityLabel={accessibilityLabel}
|
|
130
199
|
>
|
|
131
|
-
<View style={slotStyle}>
|
|
132
|
-
{enhancedChildren}
|
|
133
|
-
</View>
|
|
200
|
+
<View style={slotStyle}>{enhancedChildren}</View>
|
|
134
201
|
</View>
|
|
135
202
|
)
|
|
136
203
|
}
|
|
137
204
|
|
|
138
|
-
export default ActionFooter
|
|
139
|
-
|
|
205
|
+
export default React.memo(ActionFooter)
|
|
@@ -89,12 +89,13 @@ export default function AppBar({
|
|
|
89
89
|
const containerStyle: ViewStyle = {
|
|
90
90
|
flexDirection: 'row',
|
|
91
91
|
alignItems: 'center',
|
|
92
|
-
justifyContent:
|
|
92
|
+
// No `justifyContent` here: with the inline middle slot using `flex: 1`
|
|
93
|
+
// the three sections lay out naturally (leading | middle | actions).
|
|
94
|
+
// When middleSlot is absent we fall back to `space-between` at the wrapper
|
|
95
|
+
// level so leading & actions still anchor to the edges.
|
|
93
96
|
paddingHorizontal: paddingHorizontal ?? 16,
|
|
94
97
|
paddingVertical: paddingVertical ?? (isMain ? 16 : 10),
|
|
95
98
|
backgroundColor: backgroundColor ?? '#FFFFFF',
|
|
96
|
-
// We can set minHeight if we want to enforce consistency, but padding should dictate it mostly.
|
|
97
|
-
// Figma shows specific heights implicitly via padding + content.
|
|
98
99
|
// MainPage: h=68 (16 top/bot padding? 36 height content?)
|
|
99
100
|
// SubPage: h=52
|
|
100
101
|
}
|
|
@@ -159,9 +160,18 @@ export default function AppBar({
|
|
|
159
160
|
? <View style={actionsStyle}>{cloneChildrenWithModes(React.Children.toArray(actionsSlot), modes)}</View>
|
|
160
161
|
: null
|
|
161
162
|
|
|
163
|
+
// When there is no middleSlot we want leading & actions pinned to the
|
|
164
|
+
// outer edges, so we apply `space-between` at the wrapper. With a middle
|
|
165
|
+
// slot present, the middle (flex: 1) absorbs the remaining space, so
|
|
166
|
+
// `space-between` is a no-op.
|
|
167
|
+
const wrapperStyle: ViewStyle = {
|
|
168
|
+
...containerStyle,
|
|
169
|
+
justifyContent: processedMiddle ? 'flex-start' : 'space-between',
|
|
170
|
+
}
|
|
171
|
+
|
|
162
172
|
return (
|
|
163
173
|
<View
|
|
164
|
-
style={[
|
|
174
|
+
style={[wrapperStyle, style]}
|
|
165
175
|
accessibilityRole="header"
|
|
166
176
|
accessibilityLabel={undefined}
|
|
167
177
|
{...(accessibilityHint ? { accessibilityHint } : {})}
|
|
@@ -172,21 +182,22 @@ export default function AppBar({
|
|
|
172
182
|
{processedLeading}
|
|
173
183
|
</View>
|
|
174
184
|
|
|
175
|
-
{/*
|
|
176
|
-
|
|
177
|
-
|
|
185
|
+
{/*
|
|
186
|
+
* Middle Section — rendered as an in-flow flex item (`flex: 1`) so it
|
|
187
|
+
* occupies the space between leading and actions but never overflows
|
|
188
|
+
* past them. This fixes wide children (e.g. <LinearProgress /> with
|
|
189
|
+
* width: '100%') stretching edge-to-edge under the leading/actions.
|
|
190
|
+
* `minWidth: 0` is required so the flex item can shrink below its
|
|
191
|
+
* content's intrinsic width on platforms that respect it (web).
|
|
192
|
+
*/}
|
|
178
193
|
{processedMiddle && (
|
|
179
194
|
<View
|
|
180
195
|
style={{
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
right: 0,
|
|
184
|
-
top: 0,
|
|
185
|
-
bottom: 0,
|
|
196
|
+
flex: 1,
|
|
197
|
+
minWidth: 0,
|
|
186
198
|
alignItems: 'center',
|
|
187
199
|
justifyContent: 'center',
|
|
188
|
-
|
|
189
|
-
// Usually middle title shouldn't block actions. `pointerEvents="box-none"` is safer.
|
|
200
|
+
paddingHorizontal: 8,
|
|
190
201
|
}}
|
|
191
202
|
pointerEvents="box-none"
|
|
192
203
|
>
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
} from 'react-native'
|
|
15
15
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
16
16
|
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
17
|
+
import Skeleton from '../../skeleton/Skeleton'
|
|
18
|
+
import { useSkeleton } from '../../skeleton/SkeletonGroup'
|
|
17
19
|
|
|
18
20
|
const avatarImage = require('./31595e70c4181263f9971590224b12934b280c9b.png')
|
|
19
21
|
|
|
@@ -134,6 +136,12 @@ export type AvatarProps = {
|
|
|
134
136
|
accessibilityLabel?: string;
|
|
135
137
|
onPress?: () => void;
|
|
136
138
|
disabled?: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* Explicit per-instance loading override. When `true`, renders a
|
|
141
|
+
* same-size circular skeleton instead of the avatar. Defaults to
|
|
142
|
+
* inheriting from the surrounding `<SkeletonGroup>`.
|
|
143
|
+
*/
|
|
144
|
+
loading?: boolean;
|
|
137
145
|
} & Omit<React.ComponentProps<typeof View>, 'style' | 'accessibilityRole' | 'accessibilityLabel'>;
|
|
138
146
|
|
|
139
147
|
function Avatar({
|
|
@@ -145,11 +153,17 @@ function Avatar({
|
|
|
145
153
|
// component intentionally renders `accessibilityLabel={undefined}` on the
|
|
146
154
|
// wrapper (the inner Text/Image carry the label instead).
|
|
147
155
|
accessibilityLabel: _accessibilityLabel,
|
|
156
|
+
loading,
|
|
148
157
|
...rest
|
|
149
158
|
}: AvatarProps) {
|
|
150
159
|
const isMonogram = style === 'Monogram'
|
|
151
160
|
const tokens = useMemo(() => resolveAvatarTokens(modes, isMonogram), [modes, isMonogram])
|
|
152
161
|
|
|
162
|
+
// Skeleton context — read unconditionally; the actual short-circuit
|
|
163
|
+
// happens AFTER all remaining hooks below.
|
|
164
|
+
const { active: groupActive } = useSkeleton()
|
|
165
|
+
const isLoading = loading ?? groupActive
|
|
166
|
+
|
|
153
167
|
// Focus is a sustained visible state — keep mirroring on web; gate the
|
|
154
168
|
// setter so it never fires on native (where focus events don't fire on
|
|
155
169
|
// these elements anyway).
|
|
@@ -197,6 +211,18 @@ function Avatar({
|
|
|
197
211
|
[tokens.containerStyle, isFocused]
|
|
198
212
|
)
|
|
199
213
|
|
|
214
|
+
if (isLoading) {
|
|
215
|
+
const size = tokens.containerStyle.width as number
|
|
216
|
+
return (
|
|
217
|
+
<Skeleton
|
|
218
|
+
kind="other"
|
|
219
|
+
width={size}
|
|
220
|
+
height={size}
|
|
221
|
+
modes={modes}
|
|
222
|
+
/>
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
200
226
|
// The inner content varies; everything else (wrapper, handlers, style) is shared.
|
|
201
227
|
const innerContent = isMonogram ? (
|
|
202
228
|
<View style={monogramContainerStyle}>
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
} from 'react-native'
|
|
9
9
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
10
10
|
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
11
|
+
import Skeleton from '../../skeleton/Skeleton'
|
|
12
|
+
import { useSkeleton } from '../../skeleton/SkeletonGroup'
|
|
11
13
|
|
|
12
14
|
type BadgeProps = {
|
|
13
15
|
/** Visible label text shown inside the badge */
|
|
@@ -19,6 +21,12 @@ type BadgeProps = {
|
|
|
19
21
|
accessibilityLabel?: string
|
|
20
22
|
style?: ViewStyle
|
|
21
23
|
labelStyle?: TextStyle
|
|
24
|
+
/**
|
|
25
|
+
* Explicit per-instance loading override. When `true`, renders a
|
|
26
|
+
* badge-shaped skeleton placeholder instead of the badge. Defaults to
|
|
27
|
+
* inheriting from the surrounding `<SkeletonGroup>`.
|
|
28
|
+
*/
|
|
29
|
+
loading?: boolean
|
|
22
30
|
} & Omit<React.ComponentProps<typeof View>, 'style' | 'accessibilityLabel' | 'accessibilityRole'>
|
|
23
31
|
|
|
24
32
|
function Badge({
|
|
@@ -28,6 +36,7 @@ function Badge({
|
|
|
28
36
|
accessibilityLabel,
|
|
29
37
|
style,
|
|
30
38
|
labelStyle,
|
|
39
|
+
loading,
|
|
31
40
|
...rest
|
|
32
41
|
}: BadgeProps) {
|
|
33
42
|
// Resolve token values (fall back to sensible defaults)
|
|
@@ -51,6 +60,24 @@ function Badge({
|
|
|
51
60
|
Number(getVariableByName('badge/label/lineHeight', modes) as unknown) ||
|
|
52
61
|
Math.round(fontSize * 1.2)
|
|
53
62
|
|
|
63
|
+
// Skeleton short-circuit. Size derived from the same tokens the loaded
|
|
64
|
+
// badge would use so the placeholder occupies the same box.
|
|
65
|
+
const { active: groupActive } = useSkeleton()
|
|
66
|
+
const isLoading = loading ?? groupActive
|
|
67
|
+
if (isLoading) {
|
|
68
|
+
const charWidth = fontSize * 0.55
|
|
69
|
+
const labelWidth = Math.max(label.length, 3) * charWidth
|
|
70
|
+
return (
|
|
71
|
+
<Skeleton
|
|
72
|
+
kind="badge"
|
|
73
|
+
width={paddingHorizontal * 2 + labelWidth}
|
|
74
|
+
height={paddingVertical * 2 + lineHeight}
|
|
75
|
+
style={{ alignSelf: 'flex-start' }}
|
|
76
|
+
modes={modes}
|
|
77
|
+
/>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
54
81
|
const Container: any = onPress ? Pressable : View
|
|
55
82
|
|
|
56
83
|
const containerStyle: ViewStyle = {
|
|
@@ -14,6 +14,8 @@ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
|
14
14
|
import { usePressableWebSupport, type SafePressableProps, type WebAccessibilityProps } from '../../utils/web-platform-utils'
|
|
15
15
|
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
16
16
|
import Icon from '../../icons/Icon'
|
|
17
|
+
import Skeleton from '../../skeleton/Skeleton'
|
|
18
|
+
import { useSkeleton } from '../../skeleton/SkeletonGroup'
|
|
17
19
|
|
|
18
20
|
export type ButtonProps = SafePressableProps & {
|
|
19
21
|
label?: string;
|
|
@@ -54,6 +56,13 @@ export type ButtonProps = SafePressableProps & {
|
|
|
54
56
|
* Web-specific accessibility props (only used on web platform)
|
|
55
57
|
*/
|
|
56
58
|
webAccessibilityProps?: WebAccessibilityProps;
|
|
59
|
+
/**
|
|
60
|
+
* Explicit per-instance loading override. When `true`, the button renders
|
|
61
|
+
* as a pill-shaped skeleton of the same size; when `false`, the
|
|
62
|
+
* surrounding `<SkeletonGroup>` is ignored. Defaults to inheriting from
|
|
63
|
+
* the group.
|
|
64
|
+
*/
|
|
65
|
+
loading?: boolean;
|
|
57
66
|
};
|
|
58
67
|
|
|
59
68
|
// ---------------------------------------------------------------------------
|
|
@@ -224,6 +233,7 @@ function ButtonImpl({
|
|
|
224
233
|
accessibilityHint,
|
|
225
234
|
accessibilityState,
|
|
226
235
|
webAccessibilityProps,
|
|
236
|
+
loading,
|
|
227
237
|
...rest
|
|
228
238
|
}: ButtonProps) {
|
|
229
239
|
// Hover state is web-only in practice; the setter is gated so native taps
|
|
@@ -248,6 +258,12 @@ function ButtonImpl({
|
|
|
248
258
|
[modes, disabled]
|
|
249
259
|
)
|
|
250
260
|
|
|
261
|
+
// Skeleton context — read unconditionally so React's hook order stays
|
|
262
|
+
// stable. The actual short-circuit return happens AFTER all remaining
|
|
263
|
+
// hooks have been called below.
|
|
264
|
+
const { active: groupActive } = useSkeleton()
|
|
265
|
+
const isLoading = loading ?? groupActive
|
|
266
|
+
|
|
251
267
|
// Active label color: base by default; hover override (web-only) when hovered.
|
|
252
268
|
// Press color is intentionally NOT applied to the label on native — applying
|
|
253
269
|
// it would require a React render per touch and re-introduce the flicker.
|
|
@@ -354,6 +370,30 @@ function ButtonImpl({
|
|
|
354
370
|
}
|
|
355
371
|
}
|
|
356
372
|
|
|
373
|
+
if (isLoading) {
|
|
374
|
+
const { container, baseLabel, iconSize, accessoryOffset } = tokens
|
|
375
|
+
const paddingHorizontal = (container.paddingHorizontal as number) ?? 20
|
|
376
|
+
const paddingVertical = (container.paddingVertical as number) ?? 12
|
|
377
|
+
const lineHeight = (baseLabel.lineHeight as number) ?? 19
|
|
378
|
+
const fontSize = (baseLabel.fontSize as number) ?? 16
|
|
379
|
+
const labelText = typeof label === 'string' ? label : 'Button'
|
|
380
|
+
const charWidth = fontSize * 0.55
|
|
381
|
+
const labelWidth = Math.max(labelText.length, 4) * charWidth
|
|
382
|
+
const hasAccessory = !!(leading || trailing || icon)
|
|
383
|
+
const accessoryWidth = hasAccessory ? iconSize + accessoryOffset * 2 : 0
|
|
384
|
+
const skeletonWidth = paddingHorizontal * 2 + labelWidth + accessoryWidth
|
|
385
|
+
const skeletonHeight = paddingVertical * 2 + lineHeight
|
|
386
|
+
return (
|
|
387
|
+
<Skeleton
|
|
388
|
+
kind="other"
|
|
389
|
+
width={skeletonWidth}
|
|
390
|
+
height={skeletonHeight}
|
|
391
|
+
style={style as any}
|
|
392
|
+
modes={modes}
|
|
393
|
+
/>
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
|
|
357
397
|
return (
|
|
358
398
|
<Pressable
|
|
359
399
|
accessibilityRole="button"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react'
|
|
1
|
+
import React, { useMemo } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
@@ -66,7 +66,14 @@ export type CardBankAccountProps = {
|
|
|
66
66
|
* `false`/`null` to hide it entirely.
|
|
67
67
|
*/
|
|
68
68
|
footer?: React.ReactNode | false | null
|
|
69
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Design token modes for theming (e.g. `{ 'Color Mode': 'Light' }`).
|
|
71
|
+
*
|
|
72
|
+
* Defaults to `{ 'Button / Size': 'S', AppearanceBrand: 'Secondary',
|
|
73
|
+
* Emphasis: 'Medium' }` so the footer button matches the Figma reference
|
|
74
|
+
* out of the box. Caller-supplied modes are merged on top and can
|
|
75
|
+
* override any of the default keys.
|
|
76
|
+
*/
|
|
70
77
|
modes?: Record<string, any>
|
|
71
78
|
/** Container style override. */
|
|
72
79
|
style?: StyleProp<ViewStyle>
|
|
@@ -80,6 +87,14 @@ const DEFAULT_ITEMS: CardBankAccountItem[] = [
|
|
|
80
87
|
{ label: 'Last updated', value: 'Korem ipsum' },
|
|
81
88
|
]
|
|
82
89
|
|
|
90
|
+
// Component-level defaults that match the Figma reference. Caller-provided
|
|
91
|
+
// `modes` are merged on top so every key here can be overridden per-instance.
|
|
92
|
+
const DEFAULT_MODES: Readonly<Record<string, any>> = Object.freeze({
|
|
93
|
+
'Button / Size': 'S',
|
|
94
|
+
AppearanceBrand: 'Secondary',
|
|
95
|
+
Emphasis: 'Medium',
|
|
96
|
+
})
|
|
97
|
+
|
|
83
98
|
const toNumber = (value: unknown, fallback: number): number => {
|
|
84
99
|
if (typeof value === 'number') return Number.isFinite(value) ? value : fallback
|
|
85
100
|
if (typeof value === 'string') {
|
|
@@ -119,10 +134,21 @@ function CardBankAccount({
|
|
|
119
134
|
buttonLabel = 'Button',
|
|
120
135
|
onButtonPress,
|
|
121
136
|
footer,
|
|
122
|
-
modes = EMPTY_MODES,
|
|
137
|
+
modes: propModes = EMPTY_MODES,
|
|
123
138
|
style,
|
|
124
139
|
accessibilityLabel,
|
|
125
140
|
}: CardBankAccountProps) {
|
|
141
|
+
// Merge caller modes on top of `DEFAULT_MODES` so every default key
|
|
142
|
+
// (e.g. `Button / Size`, `AppearanceBrand`, `Emphasis`) can be overridden
|
|
143
|
+
// per-instance while still applying out of the box.
|
|
144
|
+
const modes = useMemo(
|
|
145
|
+
() =>
|
|
146
|
+
propModes === EMPTY_MODES
|
|
147
|
+
? DEFAULT_MODES
|
|
148
|
+
: { ...DEFAULT_MODES, ...propModes },
|
|
149
|
+
[propModes],
|
|
150
|
+
)
|
|
151
|
+
|
|
126
152
|
const background =
|
|
127
153
|
(getVariableByName('bankAccountCard/background', modes) as string | null) ?? '#ffffff'
|
|
128
154
|
const radius = toNumber(getVariableByName('bankAccountCard/radius', modes), 16)
|