jfs-components 0.0.74 → 0.0.78

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 (146) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/lib/commonjs/components/Accordion/Accordion.js +55 -55
  3. package/lib/commonjs/components/ActionFooter/ActionFooter.js +193 -82
  4. package/lib/commonjs/components/Avatar/Avatar.js +20 -0
  5. package/lib/commonjs/components/Badge/Badge.js +23 -0
  6. package/lib/commonjs/components/Button/Button.js +37 -0
  7. package/lib/commonjs/components/Checkbox/Checkbox.js +21 -9
  8. package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -16
  9. package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +167 -0
  10. package/lib/commonjs/components/FormField/FormField.js +14 -1
  11. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +355 -0
  12. package/lib/commonjs/components/IconButton/IconButton.js +20 -0
  13. package/lib/commonjs/components/Image/Image.js +26 -1
  14. package/lib/commonjs/components/ListItem/ListItem.js +25 -10
  15. package/lib/commonjs/components/LottiePlayer/LottiePlayer.js +116 -0
  16. package/lib/commonjs/components/LottiePlayer/LottiePlayer.web.js +82 -0
  17. package/lib/commonjs/components/LottiePlayer/loadNativeLottieView.js +74 -0
  18. package/lib/commonjs/components/LottiePlayer/loadWebLottieView.js +50 -0
  19. package/lib/commonjs/components/MessageField/MessageField.js +318 -0
  20. package/lib/commonjs/components/NavArrow/NavArrow.js +58 -17
  21. package/lib/commonjs/components/PageHero/PageHero.js +41 -5
  22. package/lib/commonjs/components/RechargeCard/RechargeCard.js +32 -17
  23. package/lib/commonjs/components/Stepper/Step.js +47 -60
  24. package/lib/commonjs/components/Stepper/StepLabel.js +40 -10
  25. package/lib/commonjs/components/Stepper/Stepper.js +15 -17
  26. package/lib/commonjs/components/SuggestiveSearch/SuggestiveSearch.js +487 -0
  27. package/lib/commonjs/components/Text/Text.js +31 -1
  28. package/lib/commonjs/components/TextInput/TextInput.js +16 -1
  29. package/lib/commonjs/components/Title/Title.js +10 -2
  30. package/lib/commonjs/components/index.js +35 -0
  31. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  32. package/lib/commonjs/icons/Icon.js +16 -0
  33. package/lib/commonjs/icons/registry.js +1 -1
  34. package/lib/commonjs/index.js +12 -0
  35. package/lib/commonjs/skeleton/Skeleton.js +234 -0
  36. package/lib/commonjs/skeleton/SkeletonGroup.js +140 -0
  37. package/lib/commonjs/skeleton/index.js +58 -0
  38. package/lib/commonjs/skeleton/shimmer-tokens.js +189 -0
  39. package/lib/commonjs/skeleton/useReducedMotion.js +64 -0
  40. package/lib/module/components/Accordion/Accordion.js +56 -56
  41. package/lib/module/components/ActionFooter/ActionFooter.js +193 -83
  42. package/lib/module/components/Avatar/Avatar.js +19 -0
  43. package/lib/module/components/Badge/Badge.js +23 -0
  44. package/lib/module/components/Button/Button.js +37 -0
  45. package/lib/module/components/Checkbox/Checkbox.js +22 -10
  46. package/lib/module/components/DropdownInput/DropdownInput.js +30 -16
  47. package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +161 -0
  48. package/lib/module/components/FormField/FormField.js +16 -3
  49. package/lib/module/components/FullscreenModal/FullscreenModal.js +350 -0
  50. package/lib/module/components/IconButton/IconButton.js +20 -0
  51. package/lib/module/components/Image/Image.js +25 -1
  52. package/lib/module/components/ListItem/ListItem.js +25 -10
  53. package/lib/module/components/LottiePlayer/LottiePlayer.js +111 -0
  54. package/lib/module/components/LottiePlayer/LottiePlayer.web.js +77 -0
  55. package/lib/module/components/LottiePlayer/loadNativeLottieView.js +69 -0
  56. package/lib/module/components/LottiePlayer/loadWebLottieView.js +45 -0
  57. package/lib/module/components/MessageField/MessageField.js +313 -0
  58. package/lib/module/components/NavArrow/NavArrow.js +59 -18
  59. package/lib/module/components/PageHero/PageHero.js +41 -5
  60. package/lib/module/components/RechargeCard/RechargeCard.js +33 -17
  61. package/lib/module/components/Stepper/Step.js +48 -61
  62. package/lib/module/components/Stepper/StepLabel.js +40 -10
  63. package/lib/module/components/Stepper/Stepper.js +15 -17
  64. package/lib/module/components/SuggestiveSearch/SuggestiveSearch.js +481 -0
  65. package/lib/module/components/Text/Text.js +31 -1
  66. package/lib/module/components/TextInput/TextInput.js +17 -2
  67. package/lib/module/components/Title/Title.js +10 -2
  68. package/lib/module/components/index.js +5 -0
  69. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  70. package/lib/module/icons/Icon.js +16 -0
  71. package/lib/module/icons/registry.js +1 -1
  72. package/lib/module/index.js +2 -1
  73. package/lib/module/skeleton/Skeleton.js +229 -0
  74. package/lib/module/skeleton/SkeletonGroup.js +133 -0
  75. package/lib/module/skeleton/index.js +6 -0
  76. package/lib/module/skeleton/shimmer-tokens.js +181 -0
  77. package/lib/module/skeleton/useReducedMotion.js +61 -0
  78. package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -20
  79. package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +26 -21
  80. package/lib/typescript/src/components/Avatar/Avatar.d.ts +7 -1
  81. package/lib/typescript/src/components/Badge/Badge.d.ts +7 -1
  82. package/lib/typescript/src/components/Button/Button.d.ts +8 -1
  83. package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +63 -0
  84. package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +99 -0
  85. package/lib/typescript/src/components/IconButton/IconButton.d.ts +7 -1
  86. package/lib/typescript/src/components/Image/Image.d.ts +8 -1
  87. package/lib/typescript/src/components/LottiePlayer/LottiePlayer.d.ts +85 -0
  88. package/lib/typescript/src/components/LottiePlayer/LottiePlayer.web.d.ts +28 -0
  89. package/lib/typescript/src/components/LottiePlayer/loadNativeLottieView.d.ts +11 -0
  90. package/lib/typescript/src/components/LottiePlayer/loadWebLottieView.d.ts +11 -0
  91. package/lib/typescript/src/components/MessageField/MessageField.d.ts +81 -0
  92. package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +10 -5
  93. package/lib/typescript/src/components/PageHero/PageHero.d.ts +31 -5
  94. package/lib/typescript/src/components/Stepper/Step.d.ts +4 -1
  95. package/lib/typescript/src/components/Stepper/StepLabel.d.ts +4 -1
  96. package/lib/typescript/src/components/Stepper/Stepper.d.ts +3 -1
  97. package/lib/typescript/src/components/SuggestiveSearch/SuggestiveSearch.d.ts +123 -0
  98. package/lib/typescript/src/components/Text/Text.d.ts +20 -1
  99. package/lib/typescript/src/components/index.d.ts +8 -3
  100. package/lib/typescript/src/icons/Icon.d.ts +7 -1
  101. package/lib/typescript/src/icons/registry.d.ts +1 -1
  102. package/lib/typescript/src/index.d.ts +1 -0
  103. package/lib/typescript/src/skeleton/Skeleton.d.ts +60 -0
  104. package/lib/typescript/src/skeleton/SkeletonGroup.d.ts +78 -0
  105. package/lib/typescript/src/skeleton/index.d.ts +5 -0
  106. package/lib/typescript/src/skeleton/shimmer-tokens.d.ts +160 -0
  107. package/lib/typescript/src/skeleton/useReducedMotion.d.ts +15 -0
  108. package/package.json +11 -1
  109. package/src/components/Accordion/Accordion.tsx +113 -73
  110. package/src/components/ActionFooter/ActionFooter.tsx +210 -92
  111. package/src/components/Avatar/Avatar.tsx +26 -0
  112. package/src/components/Badge/Badge.tsx +27 -0
  113. package/src/components/Button/Button.tsx +40 -0
  114. package/src/components/Checkbox/Checkbox.tsx +22 -9
  115. package/src/components/DropdownInput/DropdownInput.tsx +67 -39
  116. package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +237 -0
  117. package/src/components/FormField/FormField.tsx +19 -3
  118. package/src/components/FullscreenModal/FullscreenModal.tsx +414 -0
  119. package/src/components/IconButton/IconButton.tsx +27 -0
  120. package/src/components/Image/Image.tsx +25 -0
  121. package/src/components/ListItem/ListItem.tsx +21 -10
  122. package/src/components/LottiePlayer/LottiePlayer.tsx +145 -0
  123. package/src/components/LottiePlayer/LottiePlayer.web.tsx +94 -0
  124. package/src/components/LottiePlayer/loadNativeLottieView.tsx +87 -0
  125. package/src/components/LottiePlayer/loadWebLottieView.tsx +64 -0
  126. package/src/components/MessageField/MessageField.tsx +543 -0
  127. package/src/components/NavArrow/NavArrow.tsx +81 -17
  128. package/src/components/PageHero/PageHero.tsx +61 -4
  129. package/src/components/RechargeCard/RechargeCard.tsx +32 -24
  130. package/src/components/Stepper/Step.tsx +52 -51
  131. package/src/components/Stepper/StepLabel.tsx +46 -9
  132. package/src/components/Stepper/Stepper.tsx +20 -15
  133. package/src/components/SuggestiveSearch/SuggestiveSearch.tsx +756 -0
  134. package/src/components/Text/Text.tsx +54 -0
  135. package/src/components/TextInput/TextInput.tsx +14 -1
  136. package/src/components/Title/Title.tsx +13 -2
  137. package/src/components/index.ts +8 -3
  138. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  139. package/src/icons/Icon.tsx +17 -0
  140. package/src/icons/registry.ts +1 -1
  141. package/src/index.ts +1 -0
  142. package/src/skeleton/Skeleton.tsx +298 -0
  143. package/src/skeleton/SkeletonGroup.tsx +193 -0
  144. package/src/skeleton/index.ts +10 -0
  145. package/src/skeleton/shimmer-tokens.ts +221 -0
  146. package/src/skeleton/useReducedMotion.ts +72 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,115 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [0.0.78] - 2026-05-29
8
+
9
+ - Added `ExpandableCheckbox` — checkbox row with collapsible long label and Read more / Read less toggle.
10
+ - Added `FullscreenModal` — full-screen modal with parallax hero, close button, disclaimer, and action footer slots.
11
+ - Added `MessageField` — multi-line textarea with FormField States (Idle / Active / Read Only / Error / Disabled) and Form context integration.
12
+ - Added `SuggestiveSearch` — search input with inline suggestion dropdown.
13
+ - `Accordion` — new `contained` variant and hover state handling.
14
+ - `NavArrow` — pressable when `onPress` is provided; 44 pt touch target.
15
+ - `Checkbox` — 44 pt touch target; checkmark rendered inside the box view.
16
+ - `FormField` / `TextInput` — tap anywhere in the input row to focus on first tap (Android double-tap fix).
17
+ - `ActionFooter` — Android keyboard counter-shift so the footer stays behind the keyboard instead of jumping up.
18
+ - `ListItem` — title/support text use `AppearanceBrand: 'Neutral'` without forcing it onto slot children.
19
+ - `Stepper` / `Step` / `StepLabel` — `showLine`, `metaText`, `meta` props; larger indicator sizing; public type exports.
20
+ - `Title` — line-height clamp to prevent descender clipping on Android.
21
+
22
+ ---
23
+
24
+ ## [0.0.77] - 2026-05-25
25
+
26
+ ### Added
27
+
28
+ - **Skeleton / shimmer loading system** — new `src/skeleton/` module exported from the package root. One context provider (`<SkeletonGroup loading>`) flips an entire subtree into shape-preserving placeholders. The following primitives auto-skeletonize when inside an active group: `Text`, `Image`, `Badge`, `Button`, `IconButton`, `Avatar`, and `Icon`. Each one also accepts a per-instance `loading?: boolean` override. The atomic `<Skeleton kind="text|image|badge|other" width height />` block is available for non-primitive layouts. Honours `prefers-reduced-motion` automatically; one Reanimated clock per group; gradient angle locked to 135° on any aspect ratio.
29
+ - **`LottiePlayer`** — new component that renders Lottie animations using `lottie-react-native` (native) and `lottie-react` (web) via platform extensions (`LottiePlayer.web.tsx`). Both libraries are declared as **optional peer dependencies**, so installing `jfs-components` does not pull them in. Sizing is token-driven via `media/width` / `media/height` (`Media / Output` mode → `L | M | S` = `117 / 70 / 20`) — same contract `PageHero` and `LottieIntroBlock` already use for their media slots.
30
+ - New on-device dev-client scaffold under `example/` (Expo dev client + Storybook + Playground host), with its native deps pinned independently from the library.
31
+
32
+ ### Changed
33
+
34
+ - `ActionFooter` rewritten (~240 lines): `IconButton` children keep their intrinsic square size, every other child is auto-stretched to share the remaining horizontal space; modes now propagate to all slot children via `cloneChildrenWithModes`.
35
+ - `PageHero` API expansion (+65 lines): improved Figma alignment, new examples in stories/MDX.
36
+ - `RechargeCard` re-aligned to Figma node `2235:937`: container now draws the `rechargeCard/strokeWidth` + `rechargeCard/stroke/color` border, fallback colors for title/spec/disclaimer reset to `#000000`, `minWidth` falls back to `312`, and background fallback is now `#ffffff`. Hardcoded label/style overrides removed — colors come purely from tokens.
37
+ - `RechargeCard` now ships sensible slot mode defaults (overridable via the consumer's `modes` prop):
38
+ - inner `MoneyValue` defaults to `Context3: 'Balance & Cards'` (36 px / 900-weight Figma scale).
39
+ - inner `ButtonGroup` defaults to `AppearanceBrand: 'Secondary'`, `Button / Size: 'S'`, `Emphasis: 'High'`. The previous typo (`Appearance.Brand`) is fixed to match the actual `AppearanceBrand` collection, and defaults are spread *before* `modes` so any key can be overridden externally.
40
+ - Figma/token sync pass across `ActionFooter`, `DropdownInput`, `FormField`, `HStack`, `InputSearch`, `Nudge`, `Section`, `Stepper/StepLabel`, `SupportText/SupportTextIcon`, `VStack`, plus refreshed Coin Variables tokens, `.token-metadata.json`, and icons registry.
41
+ - Storybook config: babel-transpile `storybook-assets/` alongside `src/` so Lottie + platform-extension demo files resolve cleanly; `tsconfig.build.json` now excludes `storybook-assets/`.
42
+
43
+ ### Library usage notes
44
+
45
+ #### Lottie animations
46
+
47
+ `LottiePlayer` is the only Lottie surface — `lottie-react-native` / `lottie-react` are optional peers and must be installed by the consumer:
48
+
49
+ ```sh
50
+ # React Native (iOS / Android)
51
+ npm install lottie-react-native
52
+ cd ios && pod install
53
+
54
+ # Web (or react-native-web)
55
+ npm install lottie-react
56
+ ```
57
+
58
+ Then pass a **parsed JSON object** (URI sources are intentionally not supported — web players need data pre-parsed):
59
+
60
+ ```tsx
61
+ import { LottiePlayer, PageHero, LottieIntroBlock } from 'jfs-components'
62
+ import animation from './assets/loader.json'
63
+
64
+ <LottiePlayer source={animation} /> // 117×117 (default)
65
+ <LottiePlayer source={animation} size={70} /> // explicit size
66
+ <LottiePlayer source={animation} modes={{ 'Media / Output': 'S' }} /> // 20×20 via token
67
+ <PageHero media={<LottiePlayer source={animation} />} /> // any media slot
68
+ <LottieIntroBlock mediaSlot={<LottiePlayer source={animation} />} />
69
+ ```
70
+
71
+ #### Skeleton / shimmer loading
72
+
73
+ Wrap any subtree in `<SkeletonGroup loading={!data}>` — **no other changes to the screen are required**. All instrumented JFS primitives inside (`Text`, `Image`, `Badge`, `Button`, `IconButton`, `Avatar`, `Icon`) automatically swap themselves for a shape-preserving shimmer placeholder while `loading` is `true`, and revert to their real content when it flips to `false`. Layout never jumps because the placeholder reuses the same measured box as the real element.
74
+
75
+ ```tsx
76
+ import { SkeletonGroup, Card, Image, Text, Badge, Button } from 'jfs-components'
77
+
78
+ function Profile({ user }) {
79
+ return (
80
+ <SkeletonGroup loading={!user}>
81
+ <Card media={<Image imageSource={user?.cover} ratio={16 / 9} />}>
82
+ <Card.Title>{user?.name}</Card.Title>
83
+ <Card.SupportText>{user?.role}</Card.SupportText>
84
+ <Badge label={user?.tier ?? 'Member'} />
85
+ <Button label="Follow" />
86
+ </Card>
87
+ </SkeletonGroup>
88
+ )
89
+ }
90
+ ```
91
+
92
+ Two escape hatches when the auto-instrumentation isn't enough:
93
+
94
+ ```tsx
95
+ // 1) Bind a single primitive to its own load event, ignoring the group:
96
+ <Image imageSource={uri} ratio={16/9} loading={!loaded} onLoad={() => setLoaded(true)} />
97
+
98
+ // 2) Drop in atomic <Skeleton/> blocks for layouts not built on JFS primitives.
99
+ // Inside an active <SkeletonGroup> they shimmer with the same clock; outside one they render nothing.
100
+ import { Skeleton } from 'jfs-components'
101
+ <SkeletonGroup loading>
102
+ <View style={{ flexDirection: 'row', gap: 12 }}>
103
+ <Skeleton kind="other" width={48} height={48} />
104
+ <View style={{ flex: 1, gap: 6 }}>
105
+ <Skeleton kind="text" width="80%" height={16} />
106
+ <Skeleton kind="text" width="60%" height={14} />
107
+ </View>
108
+ </View>
109
+ </SkeletonGroup>
110
+ ```
111
+
112
+ Reduced motion is auto-detected from the OS (`AccessibilityInfo` on native, `prefers-reduced-motion` on web); override via `<SkeletonGroup reducedMotion>` for testing or forced behaviour.
113
+
114
+ ---
115
+
7
116
  ## [0.0.74] - 2026-05-19
8
117
 
9
118
  - Added new account and onboarding components: `AccountCard` (connected/add variants), `PageHero`, `LottieIntroBlock`, and `PoweredByLabel`.
@@ -17,34 +17,45 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
17
17
  if (_reactNative.Platform.OS === 'android' && _reactNative.UIManager.setLayoutAnimationEnabledExperimental) {
18
18
  _reactNative.UIManager.setLayoutAnimationEnabledExperimental(true);
19
19
  }
20
+ function resolveAccordionStateMode(disabled, isExpanded, isHovered, contained) {
21
+ if (disabled) return 'Disabled';
22
+ if (contained) {
23
+ return isExpanded ? 'Open Hover' : 'Hover';
24
+ }
25
+ if (isExpanded) {
26
+ return isHovered ? 'Open Hover' : 'Open';
27
+ }
28
+ return isHovered ? 'Hover' : 'Idle';
29
+ }
30
+ function toFontWeight(value, fallback) {
31
+ if (typeof value === 'number') return String(value);
32
+ if (typeof value === 'string') {
33
+ const normalized = value.trim().toLowerCase();
34
+ if (normalized === 'bold') return '700';
35
+ if (normalized === 'medium') return '500';
36
+ if (normalized === 'regular' || normalized === 'normal') return '400';
37
+ if (/^\d+$/.test(normalized)) return normalized;
38
+ return value;
39
+ }
40
+ return fallback;
41
+ }
20
42
  /**
21
43
  * Accordion component that mirrors the Figma "Accordion" component.
22
44
  *
23
- * This component supports:
24
- * - **Expandable/collapsible content** with smooth animation
25
- * - **States**: Idle, Hover, Open, Disabled
26
- * - **Slot** for custom content
27
- * - **Design-token driven styling** via `getVariableByName` and `modes`
45
+ * Supports two visual treatments via the `contained` prop:
46
+ * - **`contained={false}`** (default) transparent header at rest; filled
47
+ * background on hover / press.
48
+ * - **`contained={true}`** header always uses the filled background.
28
49
  *
29
- * Wherever the Figma layer name contains "Slot", this component exposes a
30
- * dedicated React "slot" prop:
31
- * - Slot "content" `children`
50
+ * Interaction states (Idle, Hover, Open, Disabled) are resolved automatically
51
+ * from `expanded`, `disabled`, hover, and `contained` — consumers should not
52
+ * pass `'Accordion States'` in `modes`.
32
53
  *
33
54
  * @component
34
- * @param {Object} props
35
- * @param {string} [props.title='Accordion title'] - The accordion header title
36
- * @param {boolean} [props.defaultExpanded=false] - Initial expanded state
37
- * @param {boolean} [props.expanded] - Controlled expanded state
38
- * @param {Function} [props.onExpandedChange] - Callback fired when expanded state changes
39
- * @param {boolean} [props.disabled=false] - Whether the accordion is disabled
40
- * @param {React.ReactNode} [props.children] - Content to display when expanded
41
- * @param {Object} [props.modes={}] - Modes object passed to `getVariableByName` for all design tokens
42
- * @param {Object} [props.style] - Optional container style overrides
43
- * @param {string} [props.accessibilityLabel] - Accessibility label for the accordion. If not provided, uses title
44
- * @param {string} [props.accessibilityHint] - Additional accessibility hint for screen readers
45
55
  */
46
56
  function Accordion({
47
57
  title = 'Accordion title',
58
+ contained = false,
48
59
  defaultExpanded = false,
49
60
  expanded: controlledExpanded,
50
61
  onExpandedChange,
@@ -58,21 +69,19 @@ function Accordion({
58
69
  webAccessibilityProps,
59
70
  ...rest
60
71
  }) {
61
- // Internal state for uncontrolled mode
62
72
  const [internalExpanded, setInternalExpanded] = (0, _react.useState)(defaultExpanded);
63
-
64
- // Determine if controlled or uncontrolled
73
+ const [isHovered, setIsHovered] = (0, _react.useState)(false);
65
74
  const isControlled = controlledExpanded !== undefined;
66
75
  const isExpanded = isControlled ? controlledExpanded : internalExpanded;
67
-
68
- // Hover state for web
69
- const [isHovered, setIsHovered] = (0, _react.useState)(false);
70
-
71
- // Handle toggle
76
+ const resolvedModes = (0, _react.useMemo)(() => {
77
+ const accordionState = resolveAccordionStateMode(disabled, isExpanded, isHovered, contained);
78
+ return {
79
+ ...modes,
80
+ 'Accordion States': accordionState
81
+ };
82
+ }, [contained, disabled, isExpanded, isHovered, modes]);
72
83
  const handleToggle = () => {
73
84
  if (disabled) return;
74
-
75
- // Animate the layout change
76
85
  _reactNative.LayoutAnimation.configureNext(_reactNative.LayoutAnimation.Presets.easeInEaseOut);
77
86
  if (isControlled) {
78
87
  onExpandedChange?.(!isExpanded);
@@ -81,23 +90,20 @@ function Accordion({
81
90
  onExpandedChange?.(!isExpanded);
82
91
  }
83
92
  };
84
-
85
- // Resolve design tokens
86
- const titleColor = disabled ? '#999999' : (0, _figmaVariablesResolver.getVariableByName)('accordion/title/color', modes) || '#0d0d0d';
87
- const titleFontSize = (0, _figmaVariablesResolver.getVariableByName)('accordion/title/fontSize', modes) || 18;
88
- const titleLineHeight = (0, _figmaVariablesResolver.getVariableByName)('accordion/title/lineHeight', modes) || 20;
89
- const titleFontFamily = (0, _figmaVariablesResolver.getVariableByName)('accordion/title/fontFamily', modes) || 'System';
90
- const iconColor = (0, _figmaVariablesResolver.getVariableByName)('accordion/icon/color', modes) || '#141414';
91
- const iconSize = (0, _figmaVariablesResolver.getVariableByName)('accordion/icon/size', modes) || 24;
92
- const headerGap = (0, _figmaVariablesResolver.getVariableByName)('accordion/header/gap', modes) || 12;
93
- const headerPaddingVertical = (0, _figmaVariablesResolver.getVariableByName)('accordion/header/padding/vertical', modes) || 24;
94
- const headerBackground = isHovered && !disabled ? '#f2f2f2' : (0, _figmaVariablesResolver.getVariableByName)('accordion/header/background', modes) || 'transparent';
95
- const contentGap = (0, _figmaVariablesResolver.getVariableByName)('accordion/content/gap', modes) || 12;
96
- const contentPaddingTop = (0, _figmaVariablesResolver.getVariableByName)('accordion/content/padding/top', modes) || 8;
97
- const contentPaddingBottom = isExpanded ? (0, _figmaVariablesResolver.getVariableByName)('accordion/content/padding/bottom', modes) ?? 24 : 8;
98
- const borderColor = (0, _figmaVariablesResolver.getVariableByName)('accordion/border/color', modes) || '#e6e6e6';
99
-
100
- // Styles
93
+ const titleColor = (0, _figmaVariablesResolver.getVariableByName)('accordion/title/color', resolvedModes) ?? '#0d0d0d';
94
+ const titleFontSize = (0, _figmaVariablesResolver.getVariableByName)('accordion/title/fontSize', resolvedModes) ?? 14;
95
+ const titleLineHeight = (0, _figmaVariablesResolver.getVariableByName)('accordion/title/lineHeight', resolvedModes) ?? 20;
96
+ const titleFontFamily = (0, _figmaVariablesResolver.getVariableByName)('accordion/title/fontFamily', resolvedModes) ?? 'System';
97
+ const titleFontWeight = toFontWeight((0, _figmaVariablesResolver.getVariableByName)('accordion/title/fontWeight', resolvedModes), '700');
98
+ const iconColor = (0, _figmaVariablesResolver.getVariableByName)('accordion/icon/color', resolvedModes) ?? '#141414';
99
+ const iconSize = (0, _figmaVariablesResolver.getVariableByName)('accordion/icon/size', resolvedModes) ?? 24;
100
+ const headerGap = (0, _figmaVariablesResolver.getVariableByName)('accordion/header/gap', resolvedModes) ?? 12;
101
+ const headerPaddingVertical = (0, _figmaVariablesResolver.getVariableByName)('accordion/header/padding/vertical', resolvedModes) ?? 8;
102
+ const headerBackground = (0, _figmaVariablesResolver.getVariableByName)('accordion/header/background', resolvedModes) ?? 'transparent';
103
+ const contentGap = (0, _figmaVariablesResolver.getVariableByName)('accordion/content/gap', resolvedModes) ?? 12;
104
+ const contentPaddingTop = (0, _figmaVariablesResolver.getVariableByName)('accordion/content/padding/top', resolvedModes) ?? 8;
105
+ const contentPaddingBottom = (0, _figmaVariablesResolver.getVariableByName)('accordion/content/padding/bottom', resolvedModes) ?? 8;
106
+ const borderColor = (0, _figmaVariablesResolver.getVariableByName)('accordion/border/color', resolvedModes) ?? '#e6e6e6';
101
107
  const containerStyle = {
102
108
  borderBottomWidth: 1,
103
109
  borderBottomColor: borderColor
@@ -117,7 +123,7 @@ function Accordion({
117
123
  fontSize: titleFontSize,
118
124
  lineHeight: titleLineHeight,
119
125
  fontFamily: titleFontFamily,
120
- fontWeight: '700'
126
+ fontWeight: titleFontWeight
121
127
  };
122
128
  const contentStyle = {
123
129
  backgroundColor: 'transparent',
@@ -127,11 +133,7 @@ function Accordion({
127
133
  paddingHorizontal: 0,
128
134
  overflow: 'hidden'
129
135
  };
130
-
131
- // Generate default accessibility label
132
136
  const defaultAccessibilityLabel = accessibilityLabel || title;
133
-
134
- // Web platform support
135
137
  const webProps = (0, _webPlatformUtils.usePressableWebSupport)({
136
138
  restProps: {},
137
139
  onPress: handleToggle,
@@ -139,9 +141,7 @@ function Accordion({
139
141
  accessibilityLabel: defaultAccessibilityLabel,
140
142
  webAccessibilityProps
141
143
  });
142
-
143
- // Process children to pass modes
144
- const processedChildren = children ? (0, _reactUtils.cloneChildrenWithModes)(_react.default.Children.toArray(children), modes) : null;
144
+ const processedChildren = children ? (0, _reactUtils.cloneChildrenWithModes)(_react.default.Children.toArray(children), resolvedModes) : null;
145
145
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
146
146
  style: [containerStyle, style],
147
147
  ...rest,
@@ -171,7 +171,7 @@ function Accordion({
171
171
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_Icon.default, {
172
172
  name: isExpanded ? 'ic_minus' : 'ic_add',
173
173
  size: iconSize,
174
- color: disabled ? '#999999' : iconColor,
174
+ color: iconColor,
175
175
  accessibilityElementsHidden: true,
176
176
  importantForAccessibility: "no"
177
177
  })]
@@ -4,37 +4,104 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = void 0;
7
- var _react = _interopRequireDefault(require("react"));
7
+ var _react = _interopRequireWildcard(require("react"));
8
8
  var _reactNative = require("react-native");
9
9
  var _figmaVariablesResolver = require("../../design-tokens/figma-variables-resolver");
10
10
  var _reactUtils = require("../../utils/react-utils");
11
11
  var _IconButton = _interopRequireDefault(require("../IconButton/IconButton"));
12
12
  var _jsxRuntime = require("react/jsx-runtime");
13
13
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
14
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
15
+ const IS_WEB = _reactNative.Platform.OS === 'web';
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Yoga-safe stretch
19
+ // ---------------------------------------------------------------------------
20
+ //
21
+ // React Native (Yoga) interprets the `flex: 1` shorthand as
22
+ // { flexGrow: 1, flexShrink: 1, flexBasis: 0 }
23
+ // which is the *equal-share* variant. That is the correct math for what we
24
+ // want here (equal-width action buttons), BUT Yoga has a well-known foot-gun
25
+ // when this child sits inside a parent whose main-axis size hasn't been
26
+ // resolved yet on the first layout pass: the child collapses to 0 and the
27
+ // inner text gets clipped to "" before the parent ever measures.
28
+ //
29
+ // The defensive incantation used elsewhere in this codebase (see
30
+ // `CardCTA.leftWrap` and the `MediaCard.Header` fix in CHANGELOG.md) is to
31
+ // keep the equal-share math but explicitly clamp `minWidth` to 0 so Yoga
32
+ // always allows the child to participate in the shrink algorithm, even when
33
+ // the parent itself is in an undetermined state. Combined with explicit
34
+ // `flexGrow`/`flexShrink`/`flexBasis` (NOT the `flex` shorthand) this
35
+ // renders correctly on iOS, Android, and Web — and crucially never produces
36
+ // the "buttons render as empty pills" failure mode the previous version had
37
+ // on iOS dev clients.
38
+ const STRETCH_STYLE = {
39
+ flexGrow: 1,
40
+ flexShrink: 1,
41
+ flexBasis: 0,
42
+ minWidth: 0
43
+ };
44
+
45
+ // Platform-specific drop shadow. Web boxShadow can't go through
46
+ // Platform.select (RN's typed surface doesn't include it) so we keep it as a
47
+ // separate constant and append it below.
48
+ const NATIVE_SHADOW = _reactNative.Platform.select({
49
+ ios: {
50
+ shadowColor: '#0c0d10',
51
+ shadowOffset: {
52
+ width: 0,
53
+ height: -12
54
+ },
55
+ shadowOpacity: 0.16,
56
+ shadowRadius: 24
57
+ },
58
+ android: {
59
+ elevation: 16
60
+ },
61
+ default: {}
62
+ });
63
+ const WEB_SHADOW = IS_WEB ? {
64
+ boxShadow: '0px -12px 24px 0px rgba(12, 13, 16, 0.12), 0px -16px 48px 0px rgba(12, 13, 16, 0.16)'
65
+ } : null;
66
+
67
+ // The runtime token a slot child must equal (by reference) to be treated as
68
+ // an IconButton. `IconButton` is exported wrapped in `React.memo`, so the
69
+ // element.type identity comparison works for both `<IconButton />` from the
70
+ // same module and any `React.memo`-wrapped re-export. The fallback check
71
+ // (`type.type === IconButton`) catches one extra layer of `forwardRef` /
72
+ // `memo` wrapping which can happen when consumers re-export the component.
73
+ function isIconButtonElement(element) {
74
+ const t = element.type;
75
+ if (t === _IconButton.default) return true;
76
+ if (t && typeof t === 'object' && t.type === _IconButton.default) return true;
77
+ return false;
78
+ }
79
+
14
80
  /**
15
- * ActionFooter component that provides a fixed footer container for action buttons.
16
- *
17
- * This component is designed to hold action items like IconButton and Button components
18
- * at the bottom of a screen. It includes a shadow for visual separation from content above.
19
- *
20
- * The `modes` prop is automatically passed to all slot children. If a child has its own
21
- * `modes` prop, it will be merged with the parent's modes (child modes take precedence).
22
- *
23
- * @component
24
- * @param {Object} props - Component props
25
- * @param {React.ReactNode} [props.children] - Action elements to display (e.g., IconButton, Button)
26
- * @param {Object} [props.modes={}] - Mode configuration for design tokens (automatically passed to children)
27
- * @param {Object} [props.style] - Optional style overrides
28
- * @param {string} [props.accessibilityLabel] - Accessibility label for the footer region
29
- *
81
+ * ActionFooter a sticky bottom container for primary screen actions.
82
+ *
83
+ * Layout contract:
84
+ * - The outer container stretches horizontally (`alignSelf: 'stretch'`) so
85
+ * it fills the parent regardless of whether the parent is a flex column,
86
+ * a ScrollView contentContainer, or a plain View.
87
+ * - The inner slot is a single row sized by its tallest child. It does NOT
88
+ * use `flex: 1` — that previously caused the row to collapse to zero on
89
+ * the first Yoga pass on native, taking the button labels with it.
90
+ * - `IconButton` children keep their intrinsic square size.
91
+ * - Every other child is auto-stretched with the Yoga-safe stretch style
92
+ * above so two `<Button>` siblings render at equal width on iOS, Android,
93
+ * and Web.
94
+ *
95
+ * The `modes` prop is automatically pushed down to every slot child via
96
+ * {@link cloneChildrenWithModes}; explicit child-level modes win over the
97
+ * parent's modes.
98
+ *
30
99
  * @example
31
100
  * ```tsx
32
- * // Basic usage - modes are automatically passed to all children.
33
- * // Non-IconButton children (e.g., Button) are auto-stretched to fill.
34
101
  * <ActionFooter modes={modes}>
35
102
  * <IconButton iconName="ic_split" />
36
- * <Button label="Request" />
37
- * <Button label="Pay" />
103
+ * <Button label="Request" modes={{ AppearanceBrand: 'Secondary' }} />
104
+ * <Button label="Pay" modes={{ AppearanceBrand: 'Primary' }} />
38
105
  * </ActionFooter>
39
106
  * ```
40
107
  */
@@ -42,76 +109,120 @@ function ActionFooter({
42
109
  children,
43
110
  modes = _reactUtils.EMPTY_MODES,
44
111
  style,
45
- accessibilityLabel = undefined
112
+ accessibilityLabel
46
113
  }) {
47
- // Resolve design tokens
48
- const backgroundColor = (0, _figmaVariablesResolver.getVariableByName)('actionFooter/background', modes) ?? '#ffffff';
49
- const gap = (0, _figmaVariablesResolver.getVariableByName)('actionFooter/gap', modes) ?? 8;
50
- const paddingHorizontal = (0, _figmaVariablesResolver.getVariableByName)('actionFooter/padding/horizontal', modes) ?? 16;
51
- const paddingTop = (0, _figmaVariablesResolver.getVariableByName)('actionFooter/padding/top', modes) ?? 10;
52
- const paddingBottom = (0, _figmaVariablesResolver.getVariableByName)('actionFooter/padding/bottom', modes) ?? 41;
53
-
54
- // Shadow styles - cross-platform
55
- const shadowStyle = _reactNative.Platform.select({
56
- ios: {
57
- shadowColor: 'rgba(12, 13, 16, 1)',
58
- shadowOffset: {
59
- width: 0,
60
- height: -12
61
- },
62
- shadowOpacity: 0.16,
63
- shadowRadius: 24
64
- },
65
- android: {
66
- elevation: 16
67
- },
68
- default: {
69
- // Web shadow using boxShadow (RNW supports this)
70
- }
71
- });
72
- const containerStyle = {
73
- backgroundColor,
74
- paddingLeft: paddingHorizontal,
75
- paddingRight: paddingHorizontal,
76
- paddingTop,
77
- paddingBottom,
78
- ...shadowStyle
79
- };
80
-
81
- // Slot container style for horizontal layout of action items
82
- const slotStyle = {
83
- flexDirection: 'row',
84
- alignItems: 'flex-start',
85
- gap,
86
- flex: 1
87
- };
114
+ // -------------------------------------------------------------------------
115
+ // Keep the footer locked in place behind the software keyboard (Android).
116
+ // -------------------------------------------------------------------------
117
+ //
118
+ // The Android activity is configured with `windowSoftInputMode="adjustResize"`,
119
+ // which shrinks the app window by the keyboard height when the keyboard
120
+ // opens. A bottom-anchored footer therefore gets lifted UP by the keyboard
121
+ // height exactly the jump the design does not want.
122
+ //
123
+ // To counteract that, we translate the footer back DOWN by the same keyboard
124
+ // height so it visually stays exactly where it was (now sitting behind the
125
+ // keyboard). iOS does not resize the window for the keyboard, so the footer
126
+ // already stays put there; we only run this on Android to avoid pushing the
127
+ // footer off-screen on platforms that don't lift it in the first place.
128
+ const keyboardOffset = (0, _react.useRef)(new _reactNative.Animated.Value(0)).current;
129
+ (0, _react.useEffect)(() => {
130
+ if (_reactNative.Platform.OS !== 'android') return undefined;
131
+ const animateTo = (toValue, duration) => {
132
+ _reactNative.Animated.timing(keyboardOffset, {
133
+ toValue,
134
+ // Match the OS keyboard animation so the resize and our counter-shift
135
+ // cancel out smoothly with no visible footer movement.
136
+ duration: typeof duration === 'number' && duration > 0 ? duration : 150,
137
+ useNativeDriver: true
138
+ }).start();
139
+ };
140
+ const showSub = _reactNative.Keyboard.addListener('keyboardDidShow', e => {
141
+ animateTo(e?.endCoordinates?.height ?? 0, e?.duration);
142
+ });
143
+ const hideSub = _reactNative.Keyboard.addListener('keyboardDidHide', e => {
144
+ animateTo(0, e?.duration);
145
+ });
146
+ return () => {
147
+ showSub.remove();
148
+ hideSub.remove();
149
+ };
150
+ }, [keyboardOffset]);
88
151
 
89
- // Web-specific box-shadow
90
- const webShadow = _reactNative.Platform.OS === 'web' ? {
91
- boxShadow: '0px -12px 24px 0px rgba(12, 13, 16, 0.12), 0px -16px 48px 0px rgba(12, 13, 16, 0.16)'
92
- } : {};
93
- const flatChildren = (0, _reactUtils.flattenChildren)(children);
94
- const processedChildren = (0, _reactUtils.cloneChildrenWithModes)(flatChildren, modes);
95
- const enhancedChildren = processedChildren.map((child, index) => {
96
- if (! /*#__PURE__*/_react.default.isValidElement(child)) return child;
97
- const element = child;
98
- const isIconButton = element.type === _IconButton.default;
99
- const stretchStyle = isIconButton ? undefined : {
100
- flex: 1
152
+ // All token reads collapsed into a single useMemo keyed on `modes`. With
153
+ // the shared `EMPTY_MODES` default this resolves once for the common path
154
+ // and never re-allocates the container/slot style objects between renders.
155
+ const {
156
+ containerStyle,
157
+ slotStyle
158
+ } = (0, _react.useMemo)(() => {
159
+ const backgroundColor = (0, _figmaVariablesResolver.getVariableByName)('actionFooter/background', modes) ?? '#ffffff';
160
+ const gap = (0, _figmaVariablesResolver.getVariableByName)('actionFooter/gap', modes) ?? 8;
161
+ const paddingHorizontal = (0, _figmaVariablesResolver.getVariableByName)('actionFooter/padding/horizontal', modes) ?? 16;
162
+ const paddingTop = (0, _figmaVariablesResolver.getVariableByName)('actionFooter/padding/top', modes) ?? 10;
163
+ const paddingBottom = (0, _figmaVariablesResolver.getVariableByName)('actionFooter/padding/bottom', modes) ?? 41;
164
+ const container = {
165
+ // `alignSelf: 'stretch'` is the cross-platform way to ask "fill the
166
+ // parent's cross axis" — in the common case (column parent) this gives
167
+ // us full-width without the caller needing to pass `width: '100%'`.
168
+ alignSelf: 'stretch',
169
+ backgroundColor,
170
+ paddingLeft: paddingHorizontal,
171
+ paddingRight: paddingHorizontal,
172
+ paddingTop,
173
+ paddingBottom,
174
+ ...NATIVE_SHADOW
175
+ };
176
+ const slot = {
177
+ flexDirection: 'row',
178
+ // Vertically center the IconButton against the slightly taller Buttons
179
+ // so the row reads as a single optical baseline.
180
+ alignItems: 'center',
181
+ gap
182
+ };
183
+ return {
184
+ containerStyle: container,
185
+ slotStyle: slot
101
186
  };
102
- return /*#__PURE__*/_react.default.cloneElement(element, {
103
- key: element.key ?? index,
104
- style: [stretchStyle, element.props.style]
187
+ }, [modes]);
188
+
189
+ // Process children once per (children, modes) tuple:
190
+ // 1. Flatten Fragments so each action is its own keyed sibling.
191
+ // 2. Push `modes` down so callers don't have to thread it manually.
192
+ // 3. Auto-stretch every non-IconButton with the Yoga-safe stretch style.
193
+ //
194
+ // The result identity is stable across re-renders when the inputs don't
195
+ // change, which keeps the `React.memo`-wrapped Button/IconButton children
196
+ // from re-rendering for no reason.
197
+ const enhancedChildren = (0, _react.useMemo)(() => {
198
+ const flat = (0, _reactUtils.flattenChildren)(children);
199
+ const withModes = (0, _reactUtils.cloneChildrenWithModes)(flat, modes);
200
+ return withModes.map((child, index) => {
201
+ if (! /*#__PURE__*/_react.default.isValidElement(child)) return child;
202
+ const element = child;
203
+ if (isIconButtonElement(element)) return element;
204
+ return /*#__PURE__*/_react.default.cloneElement(element, {
205
+ key: element.key ?? `action-footer-item-${index}`,
206
+ style: [STRETCH_STYLE, element.props.style]
207
+ });
105
208
  });
106
- });
107
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
108
- style: [containerStyle, webShadow, style],
209
+ }, [children, modes]);
210
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, {
211
+ style: [containerStyle, WEB_SHADOW, style,
212
+ // Counter-translate by the keyboard height on Android so `adjustResize`
213
+ // can't lift the footer above the keyboard (no-op on iOS/web where the
214
+ // value stays at 0).
215
+ {
216
+ transform: [{
217
+ translateY: keyboardOffset
218
+ }]
219
+ }],
109
220
  accessibilityRole: "toolbar",
110
- accessibilityLabel: undefined,
221
+ accessibilityLabel: accessibilityLabel,
111
222
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
112
223
  style: slotStyle,
113
224
  children: enhancedChildren
114
225
  })
115
226
  });
116
227
  }
117
- var _default = exports.default = ActionFooter;
228
+ var _default = exports.default = /*#__PURE__*/_react.default.memo(ActionFooter);