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
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useReducedMotion = useReducedMotion;
7
+ var _react = require("react");
8
+ var _reactNative = require("react-native");
9
+ /**
10
+ * Cross-platform "prefers reduced motion" hook.
11
+ *
12
+ * - Native: reads `AccessibilityInfo.isReduceMotionEnabled()` and subscribes
13
+ * to `reduceMotionChanged` events so the value stays live as the user
14
+ * toggles the OS setting.
15
+ * - Web: uses `window.matchMedia('(prefers-reduced-motion: reduce)')`,
16
+ * subscribing to its `change` event.
17
+ * - Anywhere either API is missing: returns `false` (no reduction).
18
+ *
19
+ * The hook never throws — every native API access is defensively guarded so
20
+ * the skeleton system stays safe in tests, SSR, and constrained sandboxes.
21
+ */
22
+ function useReducedMotion() {
23
+ const [reduced, setReduced] = (0, _react.useState)(false);
24
+ (0, _react.useEffect)(() => {
25
+ let cancelled = false;
26
+ if (_reactNative.Platform.OS === 'web') {
27
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
28
+ return;
29
+ }
30
+ const mql = window.matchMedia('(prefers-reduced-motion: reduce)');
31
+ const update = matches => {
32
+ if (!cancelled) setReduced(matches);
33
+ };
34
+ update(mql.matches);
35
+ const listener = e => update(e.matches);
36
+ if (typeof mql.addEventListener === 'function') {
37
+ mql.addEventListener('change', listener);
38
+ return () => {
39
+ cancelled = true;
40
+ mql.removeEventListener('change', listener);
41
+ };
42
+ }
43
+ const legacyMql = mql;
44
+ legacyMql.addListener?.(listener);
45
+ return () => {
46
+ cancelled = true;
47
+ legacyMql.removeListener?.(listener);
48
+ };
49
+ }
50
+ if (typeof _reactNative.AccessibilityInfo?.isReduceMotionEnabled === 'function') {
51
+ _reactNative.AccessibilityInfo.isReduceMotionEnabled().then(value => {
52
+ if (!cancelled) setReduced(!!value);
53
+ }).catch(() => {});
54
+ }
55
+ const sub = typeof _reactNative.AccessibilityInfo?.addEventListener === 'function' ? _reactNative.AccessibilityInfo.addEventListener('reduceMotionChanged', value => {
56
+ if (!cancelled) setReduced(!!value);
57
+ }) : null;
58
+ return () => {
59
+ cancelled = true;
60
+ sub?.remove?.();
61
+ };
62
+ }, []);
63
+ return reduced;
64
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- import React, { useState } from 'react';
3
+ import React, { useMemo, useState } from 'react';
4
4
  import { View, Text, Pressable, LayoutAnimation, Platform, UIManager } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import Icon from '../../icons/Icon';
@@ -12,34 +12,45 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
12
  if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
13
13
  UIManager.setLayoutAnimationEnabledExperimental(true);
14
14
  }
15
+ function resolveAccordionStateMode(disabled, isExpanded, isHovered, contained) {
16
+ if (disabled) return 'Disabled';
17
+ if (contained) {
18
+ return isExpanded ? 'Open Hover' : 'Hover';
19
+ }
20
+ if (isExpanded) {
21
+ return isHovered ? 'Open Hover' : 'Open';
22
+ }
23
+ return isHovered ? 'Hover' : 'Idle';
24
+ }
25
+ function toFontWeight(value, fallback) {
26
+ if (typeof value === 'number') return String(value);
27
+ if (typeof value === 'string') {
28
+ const normalized = value.trim().toLowerCase();
29
+ if (normalized === 'bold') return '700';
30
+ if (normalized === 'medium') return '500';
31
+ if (normalized === 'regular' || normalized === 'normal') return '400';
32
+ if (/^\d+$/.test(normalized)) return normalized;
33
+ return value;
34
+ }
35
+ return fallback;
36
+ }
15
37
  /**
16
38
  * Accordion component that mirrors the Figma "Accordion" component.
17
39
  *
18
- * This component supports:
19
- * - **Expandable/collapsible content** with smooth animation
20
- * - **States**: Idle, Hover, Open, Disabled
21
- * - **Slot** for custom content
22
- * - **Design-token driven styling** via `getVariableByName` and `modes`
40
+ * Supports two visual treatments via the `contained` prop:
41
+ * - **`contained={false}`** (default) transparent header at rest; filled
42
+ * background on hover / press.
43
+ * - **`contained={true}`** header always uses the filled background.
23
44
  *
24
- * Wherever the Figma layer name contains "Slot", this component exposes a
25
- * dedicated React "slot" prop:
26
- * - Slot "content" `children`
45
+ * Interaction states (Idle, Hover, Open, Disabled) are resolved automatically
46
+ * from `expanded`, `disabled`, hover, and `contained` — consumers should not
47
+ * pass `'Accordion States'` in `modes`.
27
48
  *
28
49
  * @component
29
- * @param {Object} props
30
- * @param {string} [props.title='Accordion title'] - The accordion header title
31
- * @param {boolean} [props.defaultExpanded=false] - Initial expanded state
32
- * @param {boolean} [props.expanded] - Controlled expanded state
33
- * @param {Function} [props.onExpandedChange] - Callback fired when expanded state changes
34
- * @param {boolean} [props.disabled=false] - Whether the accordion is disabled
35
- * @param {React.ReactNode} [props.children] - Content to display when expanded
36
- * @param {Object} [props.modes={}] - Modes object passed to `getVariableByName` for all design tokens
37
- * @param {Object} [props.style] - Optional container style overrides
38
- * @param {string} [props.accessibilityLabel] - Accessibility label for the accordion. If not provided, uses title
39
- * @param {string} [props.accessibilityHint] - Additional accessibility hint for screen readers
40
50
  */
41
51
  function Accordion({
42
52
  title = 'Accordion title',
53
+ contained = false,
43
54
  defaultExpanded = false,
44
55
  expanded: controlledExpanded,
45
56
  onExpandedChange,
@@ -53,21 +64,19 @@ function Accordion({
53
64
  webAccessibilityProps,
54
65
  ...rest
55
66
  }) {
56
- // Internal state for uncontrolled mode
57
67
  const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
58
-
59
- // Determine if controlled or uncontrolled
68
+ const [isHovered, setIsHovered] = useState(false);
60
69
  const isControlled = controlledExpanded !== undefined;
61
70
  const isExpanded = isControlled ? controlledExpanded : internalExpanded;
62
-
63
- // Hover state for web
64
- const [isHovered, setIsHovered] = useState(false);
65
-
66
- // Handle toggle
71
+ const resolvedModes = useMemo(() => {
72
+ const accordionState = resolveAccordionStateMode(disabled, isExpanded, isHovered, contained);
73
+ return {
74
+ ...modes,
75
+ 'Accordion States': accordionState
76
+ };
77
+ }, [contained, disabled, isExpanded, isHovered, modes]);
67
78
  const handleToggle = () => {
68
79
  if (disabled) return;
69
-
70
- // Animate the layout change
71
80
  LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
72
81
  if (isControlled) {
73
82
  onExpandedChange?.(!isExpanded);
@@ -76,23 +85,20 @@ function Accordion({
76
85
  onExpandedChange?.(!isExpanded);
77
86
  }
78
87
  };
79
-
80
- // Resolve design tokens
81
- const titleColor = disabled ? '#999999' : getVariableByName('accordion/title/color', modes) || '#0d0d0d';
82
- const titleFontSize = getVariableByName('accordion/title/fontSize', modes) || 18;
83
- const titleLineHeight = getVariableByName('accordion/title/lineHeight', modes) || 20;
84
- const titleFontFamily = getVariableByName('accordion/title/fontFamily', modes) || 'System';
85
- const iconColor = getVariableByName('accordion/icon/color', modes) || '#141414';
86
- const iconSize = getVariableByName('accordion/icon/size', modes) || 24;
87
- const headerGap = getVariableByName('accordion/header/gap', modes) || 12;
88
- const headerPaddingVertical = getVariableByName('accordion/header/padding/vertical', modes) || 24;
89
- const headerBackground = isHovered && !disabled ? '#f2f2f2' : getVariableByName('accordion/header/background', modes) || 'transparent';
90
- const contentGap = getVariableByName('accordion/content/gap', modes) || 12;
91
- const contentPaddingTop = getVariableByName('accordion/content/padding/top', modes) || 8;
92
- const contentPaddingBottom = isExpanded ? getVariableByName('accordion/content/padding/bottom', modes) ?? 24 : 8;
93
- const borderColor = getVariableByName('accordion/border/color', modes) || '#e6e6e6';
94
-
95
- // Styles
88
+ const titleColor = getVariableByName('accordion/title/color', resolvedModes) ?? '#0d0d0d';
89
+ const titleFontSize = getVariableByName('accordion/title/fontSize', resolvedModes) ?? 14;
90
+ const titleLineHeight = getVariableByName('accordion/title/lineHeight', resolvedModes) ?? 20;
91
+ const titleFontFamily = getVariableByName('accordion/title/fontFamily', resolvedModes) ?? 'System';
92
+ const titleFontWeight = toFontWeight(getVariableByName('accordion/title/fontWeight', resolvedModes), '700');
93
+ const iconColor = getVariableByName('accordion/icon/color', resolvedModes) ?? '#141414';
94
+ const iconSize = getVariableByName('accordion/icon/size', resolvedModes) ?? 24;
95
+ const headerGap = getVariableByName('accordion/header/gap', resolvedModes) ?? 12;
96
+ const headerPaddingVertical = getVariableByName('accordion/header/padding/vertical', resolvedModes) ?? 8;
97
+ const headerBackground = getVariableByName('accordion/header/background', resolvedModes) ?? 'transparent';
98
+ const contentGap = getVariableByName('accordion/content/gap', resolvedModes) ?? 12;
99
+ const contentPaddingTop = getVariableByName('accordion/content/padding/top', resolvedModes) ?? 8;
100
+ const contentPaddingBottom = getVariableByName('accordion/content/padding/bottom', resolvedModes) ?? 8;
101
+ const borderColor = getVariableByName('accordion/border/color', resolvedModes) ?? '#e6e6e6';
96
102
  const containerStyle = {
97
103
  borderBottomWidth: 1,
98
104
  borderBottomColor: borderColor
@@ -112,7 +118,7 @@ function Accordion({
112
118
  fontSize: titleFontSize,
113
119
  lineHeight: titleLineHeight,
114
120
  fontFamily: titleFontFamily,
115
- fontWeight: '700'
121
+ fontWeight: titleFontWeight
116
122
  };
117
123
  const contentStyle = {
118
124
  backgroundColor: 'transparent',
@@ -122,11 +128,7 @@ function Accordion({
122
128
  paddingHorizontal: 0,
123
129
  overflow: 'hidden'
124
130
  };
125
-
126
- // Generate default accessibility label
127
131
  const defaultAccessibilityLabel = accessibilityLabel || title;
128
-
129
- // Web platform support
130
132
  const webProps = usePressableWebSupport({
131
133
  restProps: {},
132
134
  onPress: handleToggle,
@@ -134,9 +136,7 @@ function Accordion({
134
136
  accessibilityLabel: defaultAccessibilityLabel,
135
137
  webAccessibilityProps
136
138
  });
137
-
138
- // Process children to pass modes
139
- const processedChildren = children ? cloneChildrenWithModes(React.Children.toArray(children), modes) : null;
139
+ const processedChildren = children ? cloneChildrenWithModes(React.Children.toArray(children), resolvedModes) : null;
140
140
  return /*#__PURE__*/_jsxs(View, {
141
141
  style: [containerStyle, style],
142
142
  ...rest,
@@ -166,7 +166,7 @@ function Accordion({
166
166
  }), /*#__PURE__*/_jsx(Icon, {
167
167
  name: isExpanded ? 'ic_minus' : 'ic_add',
168
168
  size: iconSize,
169
- color: disabled ? '#999999' : iconColor,
169
+ color: iconColor,
170
170
  accessibilityElementsHidden: true,
171
171
  importantForAccessibility: "no"
172
172
  })]
@@ -1,35 +1,101 @@
1
1
  "use strict";
2
2
 
3
- import React from 'react';
4
- import { View, Platform } from 'react-native';
3
+ import React, { useEffect, useMemo, useRef } from 'react';
4
+ import { Animated, Keyboard, View, Platform } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import { EMPTY_MODES, cloneChildrenWithModes, flattenChildren } from '../../utils/react-utils';
7
7
  import IconButton from '../IconButton/IconButton';
8
8
  import { jsx as _jsx } from "react/jsx-runtime";
9
+ const IS_WEB = Platform.OS === 'web';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Yoga-safe stretch
13
+ // ---------------------------------------------------------------------------
14
+ //
15
+ // React Native (Yoga) interprets the `flex: 1` shorthand as
16
+ // { flexGrow: 1, flexShrink: 1, flexBasis: 0 }
17
+ // which is the *equal-share* variant. That is the correct math for what we
18
+ // want here (equal-width action buttons), BUT Yoga has a well-known foot-gun
19
+ // when this child sits inside a parent whose main-axis size hasn't been
20
+ // resolved yet on the first layout pass: the child collapses to 0 and the
21
+ // inner text gets clipped to "" before the parent ever measures.
22
+ //
23
+ // The defensive incantation used elsewhere in this codebase (see
24
+ // `CardCTA.leftWrap` and the `MediaCard.Header` fix in CHANGELOG.md) is to
25
+ // keep the equal-share math but explicitly clamp `minWidth` to 0 so Yoga
26
+ // always allows the child to participate in the shrink algorithm, even when
27
+ // the parent itself is in an undetermined state. Combined with explicit
28
+ // `flexGrow`/`flexShrink`/`flexBasis` (NOT the `flex` shorthand) this
29
+ // renders correctly on iOS, Android, and Web — and crucially never produces
30
+ // the "buttons render as empty pills" failure mode the previous version had
31
+ // on iOS dev clients.
32
+ const STRETCH_STYLE = {
33
+ flexGrow: 1,
34
+ flexShrink: 1,
35
+ flexBasis: 0,
36
+ minWidth: 0
37
+ };
38
+
39
+ // Platform-specific drop shadow. Web boxShadow can't go through
40
+ // Platform.select (RN's typed surface doesn't include it) so we keep it as a
41
+ // separate constant and append it below.
42
+ const NATIVE_SHADOW = Platform.select({
43
+ ios: {
44
+ shadowColor: '#0c0d10',
45
+ shadowOffset: {
46
+ width: 0,
47
+ height: -12
48
+ },
49
+ shadowOpacity: 0.16,
50
+ shadowRadius: 24
51
+ },
52
+ android: {
53
+ elevation: 16
54
+ },
55
+ default: {}
56
+ });
57
+ const WEB_SHADOW = IS_WEB ? {
58
+ boxShadow: '0px -12px 24px 0px rgba(12, 13, 16, 0.12), 0px -16px 48px 0px rgba(12, 13, 16, 0.16)'
59
+ } : null;
60
+
61
+ // The runtime token a slot child must equal (by reference) to be treated as
62
+ // an IconButton. `IconButton` is exported wrapped in `React.memo`, so the
63
+ // element.type identity comparison works for both `<IconButton />` from the
64
+ // same module and any `React.memo`-wrapped re-export. The fallback check
65
+ // (`type.type === IconButton`) catches one extra layer of `forwardRef` /
66
+ // `memo` wrapping which can happen when consumers re-export the component.
67
+ function isIconButtonElement(element) {
68
+ const t = element.type;
69
+ if (t === IconButton) return true;
70
+ if (t && typeof t === 'object' && t.type === IconButton) return true;
71
+ return false;
72
+ }
73
+
9
74
  /**
10
- * ActionFooter component that provides a fixed footer container for action buttons.
11
- *
12
- * This component is designed to hold action items like IconButton and Button components
13
- * at the bottom of a screen. It includes a shadow for visual separation from content above.
14
- *
15
- * The `modes` prop is automatically passed to all slot children. If a child has its own
16
- * `modes` prop, it will be merged with the parent's modes (child modes take precedence).
17
- *
18
- * @component
19
- * @param {Object} props - Component props
20
- * @param {React.ReactNode} [props.children] - Action elements to display (e.g., IconButton, Button)
21
- * @param {Object} [props.modes={}] - Mode configuration for design tokens (automatically passed to children)
22
- * @param {Object} [props.style] - Optional style overrides
23
- * @param {string} [props.accessibilityLabel] - Accessibility label for the footer region
24
- *
75
+ * ActionFooter a sticky bottom container for primary screen actions.
76
+ *
77
+ * Layout contract:
78
+ * - The outer container stretches horizontally (`alignSelf: 'stretch'`) so
79
+ * it fills the parent regardless of whether the parent is a flex column,
80
+ * a ScrollView contentContainer, or a plain View.
81
+ * - The inner slot is a single row sized by its tallest child. It does NOT
82
+ * use `flex: 1` — that previously caused the row to collapse to zero on
83
+ * the first Yoga pass on native, taking the button labels with it.
84
+ * - `IconButton` children keep their intrinsic square size.
85
+ * - Every other child is auto-stretched with the Yoga-safe stretch style
86
+ * above so two `<Button>` siblings render at equal width on iOS, Android,
87
+ * and Web.
88
+ *
89
+ * The `modes` prop is automatically pushed down to every slot child via
90
+ * {@link cloneChildrenWithModes}; explicit child-level modes win over the
91
+ * parent's modes.
92
+ *
25
93
  * @example
26
94
  * ```tsx
27
- * // Basic usage - modes are automatically passed to all children.
28
- * // Non-IconButton children (e.g., Button) are auto-stretched to fill.
29
95
  * <ActionFooter modes={modes}>
30
96
  * <IconButton iconName="ic_split" />
31
- * <Button label="Request" />
32
- * <Button label="Pay" />
97
+ * <Button label="Request" modes={{ AppearanceBrand: 'Secondary' }} />
98
+ * <Button label="Pay" modes={{ AppearanceBrand: 'Primary' }} />
33
99
  * </ActionFooter>
34
100
  * ```
35
101
  */
@@ -37,76 +103,120 @@ function ActionFooter({
37
103
  children,
38
104
  modes = EMPTY_MODES,
39
105
  style,
40
- accessibilityLabel = undefined
106
+ accessibilityLabel
41
107
  }) {
42
- // Resolve design tokens
43
- const backgroundColor = getVariableByName('actionFooter/background', modes) ?? '#ffffff';
44
- const gap = getVariableByName('actionFooter/gap', modes) ?? 8;
45
- const paddingHorizontal = getVariableByName('actionFooter/padding/horizontal', modes) ?? 16;
46
- const paddingTop = getVariableByName('actionFooter/padding/top', modes) ?? 10;
47
- const paddingBottom = getVariableByName('actionFooter/padding/bottom', modes) ?? 41;
48
-
49
- // Shadow styles - cross-platform
50
- const shadowStyle = Platform.select({
51
- ios: {
52
- shadowColor: 'rgba(12, 13, 16, 1)',
53
- shadowOffset: {
54
- width: 0,
55
- height: -12
56
- },
57
- shadowOpacity: 0.16,
58
- shadowRadius: 24
59
- },
60
- android: {
61
- elevation: 16
62
- },
63
- default: {
64
- // Web shadow using boxShadow (RNW supports this)
65
- }
66
- });
67
- const containerStyle = {
68
- backgroundColor,
69
- paddingLeft: paddingHorizontal,
70
- paddingRight: paddingHorizontal,
71
- paddingTop,
72
- paddingBottom,
73
- ...shadowStyle
74
- };
75
-
76
- // Slot container style for horizontal layout of action items
77
- const slotStyle = {
78
- flexDirection: 'row',
79
- alignItems: 'flex-start',
80
- gap,
81
- flex: 1
82
- };
108
+ // -------------------------------------------------------------------------
109
+ // Keep the footer locked in place behind the software keyboard (Android).
110
+ // -------------------------------------------------------------------------
111
+ //
112
+ // The Android activity is configured with `windowSoftInputMode="adjustResize"`,
113
+ // which shrinks the app window by the keyboard height when the keyboard
114
+ // opens. A bottom-anchored footer therefore gets lifted UP by the keyboard
115
+ // height exactly the jump the design does not want.
116
+ //
117
+ // To counteract that, we translate the footer back DOWN by the same keyboard
118
+ // height so it visually stays exactly where it was (now sitting behind the
119
+ // keyboard). iOS does not resize the window for the keyboard, so the footer
120
+ // already stays put there; we only run this on Android to avoid pushing the
121
+ // footer off-screen on platforms that don't lift it in the first place.
122
+ const keyboardOffset = useRef(new Animated.Value(0)).current;
123
+ useEffect(() => {
124
+ if (Platform.OS !== 'android') return undefined;
125
+ const animateTo = (toValue, duration) => {
126
+ Animated.timing(keyboardOffset, {
127
+ toValue,
128
+ // Match the OS keyboard animation so the resize and our counter-shift
129
+ // cancel out smoothly with no visible footer movement.
130
+ duration: typeof duration === 'number' && duration > 0 ? duration : 150,
131
+ useNativeDriver: true
132
+ }).start();
133
+ };
134
+ const showSub = Keyboard.addListener('keyboardDidShow', e => {
135
+ animateTo(e?.endCoordinates?.height ?? 0, e?.duration);
136
+ });
137
+ const hideSub = Keyboard.addListener('keyboardDidHide', e => {
138
+ animateTo(0, e?.duration);
139
+ });
140
+ return () => {
141
+ showSub.remove();
142
+ hideSub.remove();
143
+ };
144
+ }, [keyboardOffset]);
83
145
 
84
- // Web-specific box-shadow
85
- const webShadow = Platform.OS === 'web' ? {
86
- boxShadow: '0px -12px 24px 0px rgba(12, 13, 16, 0.12), 0px -16px 48px 0px rgba(12, 13, 16, 0.16)'
87
- } : {};
88
- const flatChildren = flattenChildren(children);
89
- const processedChildren = cloneChildrenWithModes(flatChildren, modes);
90
- const enhancedChildren = processedChildren.map((child, index) => {
91
- if (! /*#__PURE__*/React.isValidElement(child)) return child;
92
- const element = child;
93
- const isIconButton = element.type === IconButton;
94
- const stretchStyle = isIconButton ? undefined : {
95
- flex: 1
146
+ // All token reads collapsed into a single useMemo keyed on `modes`. With
147
+ // the shared `EMPTY_MODES` default this resolves once for the common path
148
+ // and never re-allocates the container/slot style objects between renders.
149
+ const {
150
+ containerStyle,
151
+ slotStyle
152
+ } = useMemo(() => {
153
+ const backgroundColor = getVariableByName('actionFooter/background', modes) ?? '#ffffff';
154
+ const gap = getVariableByName('actionFooter/gap', modes) ?? 8;
155
+ const paddingHorizontal = getVariableByName('actionFooter/padding/horizontal', modes) ?? 16;
156
+ const paddingTop = getVariableByName('actionFooter/padding/top', modes) ?? 10;
157
+ const paddingBottom = getVariableByName('actionFooter/padding/bottom', modes) ?? 41;
158
+ const container = {
159
+ // `alignSelf: 'stretch'` is the cross-platform way to ask "fill the
160
+ // parent's cross axis" — in the common case (column parent) this gives
161
+ // us full-width without the caller needing to pass `width: '100%'`.
162
+ alignSelf: 'stretch',
163
+ backgroundColor,
164
+ paddingLeft: paddingHorizontal,
165
+ paddingRight: paddingHorizontal,
166
+ paddingTop,
167
+ paddingBottom,
168
+ ...NATIVE_SHADOW
96
169
  };
97
- return /*#__PURE__*/React.cloneElement(element, {
98
- key: element.key ?? index,
99
- style: [stretchStyle, element.props.style]
170
+ const slot = {
171
+ flexDirection: 'row',
172
+ // Vertically center the IconButton against the slightly taller Buttons
173
+ // so the row reads as a single optical baseline.
174
+ alignItems: 'center',
175
+ gap
176
+ };
177
+ return {
178
+ containerStyle: container,
179
+ slotStyle: slot
180
+ };
181
+ }, [modes]);
182
+
183
+ // Process children once per (children, modes) tuple:
184
+ // 1. Flatten Fragments so each action is its own keyed sibling.
185
+ // 2. Push `modes` down so callers don't have to thread it manually.
186
+ // 3. Auto-stretch every non-IconButton with the Yoga-safe stretch style.
187
+ //
188
+ // The result identity is stable across re-renders when the inputs don't
189
+ // change, which keeps the `React.memo`-wrapped Button/IconButton children
190
+ // from re-rendering for no reason.
191
+ const enhancedChildren = useMemo(() => {
192
+ const flat = flattenChildren(children);
193
+ const withModes = cloneChildrenWithModes(flat, modes);
194
+ return withModes.map((child, index) => {
195
+ if (! /*#__PURE__*/React.isValidElement(child)) return child;
196
+ const element = child;
197
+ if (isIconButtonElement(element)) return element;
198
+ return /*#__PURE__*/React.cloneElement(element, {
199
+ key: element.key ?? `action-footer-item-${index}`,
200
+ style: [STRETCH_STYLE, element.props.style]
201
+ });
100
202
  });
101
- });
102
- return /*#__PURE__*/_jsx(View, {
103
- style: [containerStyle, webShadow, style],
203
+ }, [children, modes]);
204
+ return /*#__PURE__*/_jsx(Animated.View, {
205
+ style: [containerStyle, WEB_SHADOW, style,
206
+ // Counter-translate by the keyboard height on Android so `adjustResize`
207
+ // can't lift the footer above the keyboard (no-op on iOS/web where the
208
+ // value stays at 0).
209
+ {
210
+ transform: [{
211
+ translateY: keyboardOffset
212
+ }]
213
+ }],
104
214
  accessibilityRole: "toolbar",
105
- accessibilityLabel: undefined,
215
+ accessibilityLabel: accessibilityLabel,
106
216
  children: /*#__PURE__*/_jsx(View, {
107
217
  style: slotStyle,
108
218
  children: enhancedChildren
109
219
  })
110
220
  });
111
221
  }
112
- export default ActionFooter;
222
+ export default /*#__PURE__*/React.memo(ActionFooter);
@@ -4,6 +4,8 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
4
4
  import { Pressable, View, Image, Text, Platform } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import { EMPTY_MODES } from '../../utils/react-utils';
7
+ import Skeleton from '../../skeleton/Skeleton';
8
+ import { useSkeleton } from '../../skeleton/SkeletonGroup';
7
9
  import { jsx as _jsx } from "react/jsx-runtime";
8
10
  const avatarImage = require('./31595e70c4181263f9971590224b12934b280c9b.png');
9
11
 
@@ -123,11 +125,19 @@ function Avatar({
123
125
  // component intentionally renders `accessibilityLabel={undefined}` on the
124
126
  // wrapper (the inner Text/Image carry the label instead).
125
127
  accessibilityLabel: _accessibilityLabel,
128
+ loading,
126
129
  ...rest
127
130
  }) {
128
131
  const isMonogram = style === 'Monogram';
129
132
  const tokens = useMemo(() => resolveAvatarTokens(modes, isMonogram), [modes, isMonogram]);
130
133
 
134
+ // Skeleton context — read unconditionally; the actual short-circuit
135
+ // happens AFTER all remaining hooks below.
136
+ const {
137
+ active: groupActive
138
+ } = useSkeleton();
139
+ const isLoading = loading ?? groupActive;
140
+
131
141
  // Focus is a sustained visible state — keep mirroring on web; gate the
132
142
  // setter so it never fires on native (where focus events don't fire on
133
143
  // these elements anyway).
@@ -158,6 +168,15 @@ function Avatar({
158
168
  pressed
159
169
  }) => [tokens.containerStyle, pressed ? pressedOverlayStyle : null, isFocused ? focusOverlayStyle : null], [tokens.containerStyle, isFocused]);
160
170
  const staticContainerStyle = useMemo(() => [tokens.containerStyle, isFocused ? focusOverlayStyle : null], [tokens.containerStyle, isFocused]);
171
+ if (isLoading) {
172
+ const size = tokens.containerStyle.width;
173
+ return /*#__PURE__*/_jsx(Skeleton, {
174
+ kind: "other",
175
+ width: size,
176
+ height: size,
177
+ modes: modes
178
+ });
179
+ }
161
180
 
162
181
  // The inner content varies; everything else (wrapper, handlers, style) is shared.
163
182
  const innerContent = isMonogram ? /*#__PURE__*/_jsx(View, {
@@ -4,6 +4,8 @@ import React from 'react';
4
4
  import { View, Text, Pressable } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import { EMPTY_MODES } from '../../utils/react-utils';
7
+ import Skeleton from '../../skeleton/Skeleton';
8
+ import { useSkeleton } from '../../skeleton/SkeletonGroup';
7
9
  import { jsx as _jsx } from "react/jsx-runtime";
8
10
  function Badge({
9
11
  label = 'Label',
@@ -12,6 +14,7 @@ function Badge({
12
14
  accessibilityLabel,
13
15
  style,
14
16
  labelStyle,
17
+ loading,
15
18
  ...rest
16
19
  }) {
17
20
  // Resolve token values (fall back to sensible defaults)
@@ -24,6 +27,26 @@ function Badge({
24
27
  const paddingVertical = Number(getVariableByName('badge/padding/vertical', modes)) || 4;
25
28
  const borderRadius = Number(getVariableByName('badge/radius', modes)) || 4;
26
29
  const lineHeight = Number(getVariableByName('badge/label/lineHeight', modes)) || Math.round(fontSize * 1.2);
30
+
31
+ // Skeleton short-circuit. Size derived from the same tokens the loaded
32
+ // badge would use so the placeholder occupies the same box.
33
+ const {
34
+ active: groupActive
35
+ } = useSkeleton();
36
+ const isLoading = loading ?? groupActive;
37
+ if (isLoading) {
38
+ const charWidth = fontSize * 0.55;
39
+ const labelWidth = Math.max(label.length, 3) * charWidth;
40
+ return /*#__PURE__*/_jsx(Skeleton, {
41
+ kind: "badge",
42
+ width: paddingHorizontal * 2 + labelWidth,
43
+ height: paddingVertical * 2 + lineHeight,
44
+ style: {
45
+ alignSelf: 'flex-start'
46
+ },
47
+ modes: modes
48
+ });
49
+ }
27
50
  const Container = onPress ? Pressable : View;
28
51
  const containerStyle = {
29
52
  backgroundColor,