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.
Files changed (134) hide show
  1. package/CHANGELOG.md +115 -6
  2. package/lib/commonjs/components/AccountCard/AccountCard.js +247 -0
  3. package/lib/commonjs/components/ActionFooter/ActionFooter.js +147 -82
  4. package/lib/commonjs/components/AppBar/AppBar.js +17 -11
  5. package/lib/commonjs/components/Avatar/Avatar.js +20 -0
  6. package/lib/commonjs/components/Badge/Badge.js +23 -0
  7. package/lib/commonjs/components/Button/Button.js +37 -0
  8. package/lib/commonjs/components/CardBankAccount/CardBankAccount.js +18 -2
  9. package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +40 -25
  10. package/lib/commonjs/components/Dropdown/Dropdown.js +214 -0
  11. package/lib/commonjs/components/DropdownInput/DropdownInput.js +542 -0
  12. package/lib/commonjs/components/FormField/FormField.js +328 -178
  13. package/lib/commonjs/components/IconButton/IconButton.js +20 -0
  14. package/lib/commonjs/components/Image/Image.js +26 -1
  15. package/lib/commonjs/components/LottieIntroBlock/LottieIntroBlock.js +150 -0
  16. package/lib/commonjs/components/LottiePlayer/LottiePlayer.js +116 -0
  17. package/lib/commonjs/components/LottiePlayer/LottiePlayer.web.js +82 -0
  18. package/lib/commonjs/components/LottiePlayer/loadNativeLottieView.js +74 -0
  19. package/lib/commonjs/components/LottiePlayer/loadWebLottieView.js +50 -0
  20. package/lib/commonjs/components/PageHero/PageHero.js +189 -0
  21. package/lib/commonjs/components/PoweredByLabel/PoweredByLabel.js +135 -0
  22. package/lib/commonjs/components/PoweredByLabel/finvu.png +0 -0
  23. package/lib/commonjs/components/RechargeCard/RechargeCard.js +32 -17
  24. package/lib/commonjs/components/Text/Text.js +40 -3
  25. package/lib/commonjs/components/Tooltip/Tooltip.js +34 -27
  26. package/lib/commonjs/components/index.js +67 -0
  27. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  28. package/lib/commonjs/icons/Icon.js +16 -0
  29. package/lib/commonjs/icons/registry.js +1 -1
  30. package/lib/commonjs/index.js +12 -0
  31. package/lib/commonjs/skeleton/Skeleton.js +234 -0
  32. package/lib/commonjs/skeleton/SkeletonGroup.js +140 -0
  33. package/lib/commonjs/skeleton/index.js +58 -0
  34. package/lib/commonjs/skeleton/shimmer-tokens.js +189 -0
  35. package/lib/commonjs/skeleton/useReducedMotion.js +64 -0
  36. package/lib/module/components/AccountCard/AccountCard.js +241 -0
  37. package/lib/module/components/ActionFooter/ActionFooter.js +146 -82
  38. package/lib/module/components/AppBar/AppBar.js +17 -11
  39. package/lib/module/components/Avatar/Avatar.js +19 -0
  40. package/lib/module/components/Badge/Badge.js +23 -0
  41. package/lib/module/components/Button/Button.js +37 -0
  42. package/lib/module/components/CardBankAccount/CardBankAccount.js +17 -2
  43. package/lib/module/components/CheckboxItem/CheckboxItem.js +41 -26
  44. package/lib/module/components/Dropdown/Dropdown.js +206 -0
  45. package/lib/module/components/DropdownInput/DropdownInput.js +536 -0
  46. package/lib/module/components/FormField/FormField.js +330 -180
  47. package/lib/module/components/IconButton/IconButton.js +20 -0
  48. package/lib/module/components/Image/Image.js +25 -1
  49. package/lib/module/components/LottieIntroBlock/LottieIntroBlock.js +144 -0
  50. package/lib/module/components/LottiePlayer/LottiePlayer.js +111 -0
  51. package/lib/module/components/LottiePlayer/LottiePlayer.web.js +77 -0
  52. package/lib/module/components/LottiePlayer/loadNativeLottieView.js +69 -0
  53. package/lib/module/components/LottiePlayer/loadWebLottieView.js +45 -0
  54. package/lib/module/components/PageHero/PageHero.js +183 -0
  55. package/lib/module/components/PoweredByLabel/PoweredByLabel.js +130 -0
  56. package/lib/module/components/PoweredByLabel/finvu.png +0 -0
  57. package/lib/module/components/RechargeCard/RechargeCard.js +33 -17
  58. package/lib/module/components/Text/Text.js +40 -3
  59. package/lib/module/components/Tooltip/Tooltip.js +34 -27
  60. package/lib/module/components/index.js +8 -1
  61. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  62. package/lib/module/icons/Icon.js +16 -0
  63. package/lib/module/icons/registry.js +1 -1
  64. package/lib/module/index.js +2 -1
  65. package/lib/module/skeleton/Skeleton.js +229 -0
  66. package/lib/module/skeleton/SkeletonGroup.js +133 -0
  67. package/lib/module/skeleton/index.js +6 -0
  68. package/lib/module/skeleton/shimmer-tokens.js +181 -0
  69. package/lib/module/skeleton/useReducedMotion.js +61 -0
  70. package/lib/typescript/src/components/AccountCard/AccountCard.d.ts +81 -0
  71. package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +26 -21
  72. package/lib/typescript/src/components/Avatar/Avatar.d.ts +7 -1
  73. package/lib/typescript/src/components/Badge/Badge.d.ts +7 -1
  74. package/lib/typescript/src/components/Button/Button.d.ts +8 -1
  75. package/lib/typescript/src/components/CardBankAccount/CardBankAccount.d.ts +9 -2
  76. package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +18 -2
  77. package/lib/typescript/src/components/Dropdown/Dropdown.d.ts +62 -0
  78. package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +107 -0
  79. package/lib/typescript/src/components/FormField/FormField.d.ts +76 -19
  80. package/lib/typescript/src/components/IconButton/IconButton.d.ts +7 -1
  81. package/lib/typescript/src/components/Image/Image.d.ts +8 -1
  82. package/lib/typescript/src/components/LottieIntroBlock/LottieIntroBlock.d.ts +58 -0
  83. package/lib/typescript/src/components/LottiePlayer/LottiePlayer.d.ts +85 -0
  84. package/lib/typescript/src/components/LottiePlayer/LottiePlayer.web.d.ts +28 -0
  85. package/lib/typescript/src/components/LottiePlayer/loadNativeLottieView.d.ts +11 -0
  86. package/lib/typescript/src/components/LottiePlayer/loadWebLottieView.d.ts +11 -0
  87. package/lib/typescript/src/components/PageHero/PageHero.d.ts +79 -0
  88. package/lib/typescript/src/components/PoweredByLabel/PoweredByLabel.d.ts +70 -0
  89. package/lib/typescript/src/components/Text/Text.d.ts +31 -2
  90. package/lib/typescript/src/components/Tooltip/Tooltip.d.ts +13 -2
  91. package/lib/typescript/src/components/index.d.ts +8 -1
  92. package/lib/typescript/src/icons/Icon.d.ts +7 -1
  93. package/lib/typescript/src/icons/registry.d.ts +1 -1
  94. package/lib/typescript/src/index.d.ts +1 -0
  95. package/lib/typescript/src/skeleton/Skeleton.d.ts +60 -0
  96. package/lib/typescript/src/skeleton/SkeletonGroup.d.ts +78 -0
  97. package/lib/typescript/src/skeleton/index.d.ts +5 -0
  98. package/lib/typescript/src/skeleton/shimmer-tokens.d.ts +160 -0
  99. package/lib/typescript/src/skeleton/useReducedMotion.d.ts +15 -0
  100. package/package.json +11 -3
  101. package/src/components/AccountCard/AccountCard.tsx +376 -0
  102. package/src/components/ActionFooter/ActionFooter.tsx +152 -86
  103. package/src/components/AppBar/AppBar.tsx +25 -14
  104. package/src/components/Avatar/Avatar.tsx +26 -0
  105. package/src/components/Badge/Badge.tsx +27 -0
  106. package/src/components/Button/Button.tsx +40 -0
  107. package/src/components/CardBankAccount/CardBankAccount.tsx +29 -3
  108. package/src/components/CheckboxItem/CheckboxItem.tsx +65 -30
  109. package/src/components/Dropdown/Dropdown.tsx +331 -0
  110. package/src/components/DropdownInput/DropdownInput.tsx +819 -0
  111. package/src/components/FormField/FormField.tsx +542 -215
  112. package/src/components/IconButton/IconButton.tsx +27 -0
  113. package/src/components/Image/Image.tsx +25 -0
  114. package/src/components/LottieIntroBlock/LottieIntroBlock.tsx +202 -0
  115. package/src/components/LottiePlayer/LottiePlayer.tsx +145 -0
  116. package/src/components/LottiePlayer/LottiePlayer.web.tsx +94 -0
  117. package/src/components/LottiePlayer/loadNativeLottieView.tsx +87 -0
  118. package/src/components/LottiePlayer/loadWebLottieView.tsx +64 -0
  119. package/src/components/PageHero/PageHero.tsx +257 -0
  120. package/src/components/PoweredByLabel/PoweredByLabel.tsx +221 -0
  121. package/src/components/PoweredByLabel/finvu.png +0 -0
  122. package/src/components/RechargeCard/RechargeCard.tsx +32 -24
  123. package/src/components/Text/Text.tsx +78 -3
  124. package/src/components/Tooltip/Tooltip.tsx +50 -25
  125. package/src/components/index.ts +16 -1
  126. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  127. package/src/icons/Icon.tsx +17 -0
  128. package/src/icons/registry.ts +1 -1
  129. package/src/index.ts +1 -0
  130. package/src/skeleton/Skeleton.tsx +298 -0
  131. package/src/skeleton/SkeletonGroup.tsx +193 -0
  132. package/src/skeleton/index.ts +10 -0
  133. package/src/skeleton/shimmer-tokens.ts +221 -0
  134. 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
- * Pass the same modes to children components for consistent theming.
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 component that provides a fixed footer container for action buttons.
35
- *
36
- * This component is designed to hold action items like IconButton and Button components
37
- * at the bottom of a screen. It includes a shadow for visual separation from content above.
38
- *
39
- * The `modes` prop is automatically passed to all slot children. If a child has its own
40
- * `modes` prop, it will be merged with the parent's modes (child modes take precedence).
41
- *
42
- * @component
43
- * @param {Object} props - Component props
44
- * @param {React.ReactNode} [props.children] - Action elements to display (e.g., IconButton, Button)
45
- * @param {Object} [props.modes={}] - Mode configuration for design tokens (automatically passed to children)
46
- * @param {Object} [props.style] - Optional style overrides
47
- * @param {string} [props.accessibilityLabel] - Accessibility label for the footer region
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 = undefined,
134
+ accessibilityLabel,
65
135
  }: ActionFooterProps) {
66
- // Resolve design tokens
67
- const backgroundColor = getVariableByName('actionFooter/background', modes) ?? '#ffffff'
68
- const gap = getVariableByName('actionFooter/gap', modes) ?? 8
69
- const paddingHorizontal = getVariableByName('actionFooter/padding/horizontal', modes) ?? 16
70
- const paddingTop = getVariableByName('actionFooter/padding/top', modes) ?? 10
71
- const paddingBottom = getVariableByName('actionFooter/padding/bottom', modes) ?? 41
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
- // Shadow styles - cross-platform
74
- const shadowStyle: ViewStyle = Platform.select({
75
- ios: {
76
- shadowColor: 'rgba(12, 13, 16, 1)',
77
- shadowOffset: { width: 0, height: -12 },
78
- shadowOpacity: 0.16,
79
- shadowRadius: 24,
80
- },
81
- android: {
82
- elevation: 16,
83
- },
84
- default: {
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
- const containerStyle: ViewStyle = {
90
- backgroundColor,
91
- paddingLeft: paddingHorizontal,
92
- paddingRight: paddingHorizontal,
93
- paddingTop,
94
- paddingBottom,
95
- ...shadowStyle,
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
- // Slot container style for horizontal layout of action items
99
- const slotStyle: ViewStyle = {
100
- flexDirection: 'row',
101
- alignItems: 'flex-start',
102
- gap,
103
- flex: 1,
104
- }
169
+ return { containerStyle: container, slotStyle: slot }
170
+ }, [modes])
105
171
 
106
- // Web-specific box-shadow
107
- const webShadow = Platform.OS === 'web'
108
- ? { boxShadow: '0px -12px 24px 0px rgba(12, 13, 16, 0.12), 0px -16px 48px 0px rgba(12, 13, 16, 0.16)' } as any
109
- : {}
110
-
111
- const flatChildren = flattenChildren(children)
112
- const processedChildren = cloneChildrenWithModes(flatChildren, modes)
113
-
114
- const enhancedChildren = (processedChildren as React.ReactNode[]).map((child, index) => {
115
- if (!React.isValidElement(child)) return child
116
- const element = child as React.ReactElement<any>
117
- const isIconButton = element.type === IconButton
118
- const stretchStyle = isIconButton ? undefined : { flex: 1 }
119
- return React.cloneElement(element, {
120
- key: element.key ?? index,
121
- style: [stretchStyle, element.props.style],
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, webShadow, style]}
196
+ style={[containerStyle, WEB_SHADOW, style]}
128
197
  accessibilityRole="toolbar"
129
- accessibilityLabel={undefined}
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: 'space-between',
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={[containerStyle, 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
- {/* Middle Section (Absolute centered often, or flex? Figma shows "Slot 'Middle'" inside a wrapper that is absolute center) */}
176
- {/* Figma: "absolute flex h-full items-center justify-center left-1/2 ... translate-x-[-50%]" */}
177
- {/* We should only render this wrapper if there IS middle content, to avoid z-index blocking hits. */}
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
- position: 'absolute',
182
- left: 0,
183
- right: 0,
184
- top: 0,
185
- bottom: 0,
196
+ flex: 1,
197
+ minWidth: 0,
186
198
  alignItems: 'center',
187
199
  justifyContent: 'center',
188
- zIndex: -1, // Behind actions if overlap? Or should be on top?
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
- /** Design token modes for theming (e.g. `{ 'Color Mode': 'Light' }`). */
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)