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
@@ -32,6 +32,23 @@ export type PageHeroProps = {
32
32
  * `modes` are automatically cascaded into this slot.
33
33
  */
34
34
  buttonSlot?: React.ReactNode
35
+ /**
36
+ * Optional media element shown above the text block (eyebrow + headline).
37
+ *
38
+ * Intentionally typed as `React.ReactNode` so the consumer can bring any
39
+ * renderer they like — `<Image />`, `<IconCapsule />`, a Lottie player
40
+ * (`lottie-react-native` / `@lottiefiles/dotlottie-react`), an `<SvgXml />`
41
+ * from `react-native-svg`, a `<Video />` from `react-native-video`, a
42
+ * gradient view, or a custom illustration. The library deliberately does
43
+ * NOT wrap Lottie / video runtimes (they require native modules + pod
44
+ * autolinking on every consumer's app), so PageHero just allocates a
45
+ * token-sized container and lets the slot render whatever it wants.
46
+ *
47
+ * The slot is rendered inside a `width × height` box driven by
48
+ * `media/width` and `media/height` tokens (default 117×117). `modes` are
49
+ * automatically cascaded into the slot via `cloneChildrenWithModes`.
50
+ */
51
+ media?: React.ReactNode
35
52
  /** Mode configuration for design-token theming. */
36
53
  modes?: Record<string, any>
37
54
  /** Style overrides applied to the outer container. */
@@ -41,12 +58,14 @@ export type PageHeroProps = {
41
58
 
42
59
  /**
43
60
  * PageHero displays a centered hero block typically used at the top of a page
44
- * or feature screen. It contains an eyebrow line, a large headline, an optional
45
- * supporting line (e.g. price/timeline), and an optional action button.
61
+ * or feature screen. It contains an optional media slot (illustration / image
62
+ * / Lottie / SVG / video — consumer's choice), an eyebrow line, a large
63
+ * headline, an optional supporting line (e.g. price / timeline), and an
64
+ * optional action button.
46
65
  *
47
66
  * All visual values are resolved from Figma design tokens via
48
- * `getVariableByName`. The button slot cascades the active `modes` to its
49
- * children through `cloneChildrenWithModes`.
67
+ * `getVariableByName`. Slots cascade the active `modes` to their children
68
+ * through `cloneChildrenWithModes`.
50
69
  *
51
70
  * @component
52
71
  * @example
@@ -57,6 +76,13 @@ export type PageHeroProps = {
57
76
  * supportingText="₹999/year · ₹0 until 2027"
58
77
  * buttonLabel="Renew for free"
59
78
  * onButtonPress={() => navigate('Upgrade')}
79
+ * media={
80
+ * <Image
81
+ * imageSource={require('./assets/upgrade.png')}
82
+ * width={117}
83
+ * height={117}
84
+ * />
85
+ * }
60
86
  * />
61
87
  * ```
62
88
  */
@@ -69,6 +95,7 @@ function PageHero({
69
95
  onButtonPress,
70
96
  showButton = true,
71
97
  buttonSlot,
98
+ media,
72
99
  modes: propModes = EMPTY_MODES,
73
100
  style,
74
101
  testID,
@@ -86,6 +113,12 @@ function PageHero({
86
113
  const textWrapGap =
87
114
  Number(getVariableByName('PageHero/textWrap/gap', modes)) || 8
88
115
 
116
+ // Media slot box — matches the 117×117 frame in Figma (node 4540:7845).
117
+ // Tokens fall back to 117 when not defined in the variables collection,
118
+ // so the layout stays stable on consumers that haven't tokenized this yet.
119
+ const mediaWidth = Number(getVariableByName('media/width', modes)) || 117
120
+ const mediaHeight = Number(getVariableByName('media/height', modes)) || 117
121
+
89
122
  const eyebrowColor =
90
123
  getVariableByName('PageHero/eyebrow/color', modes) || '#ffffff'
91
124
  const eyebrowFontFamily =
@@ -183,8 +216,32 @@ function PageHero({
183
216
  // eslint-disable-next-line react-hooks/exhaustive-deps
184
217
  }, [buttonSlot, showButton, buttonLabel, onButtonPress, modes])
185
218
 
219
+ // Sized container for the media slot. Always rendered when `media` is
220
+ // provided, so the slot has a predictable box (matches Figma frame
221
+ // 4540:7845 — 117×117 by default) even if the inner element omits its
222
+ // own width/height. `overflow: 'hidden'` mirrors the Figma frame's
223
+ // `clipsContent` so a slightly oversized illustration doesn't break
224
+ // the centered layout.
225
+ const mediaContent = useMemo<React.ReactNode>(() => {
226
+ if (media === undefined || media === null) return null
227
+ return (
228
+ <View
229
+ style={{
230
+ width: mediaWidth,
231
+ height: mediaHeight,
232
+ alignItems: 'center',
233
+ justifyContent: 'center',
234
+ overflow: 'hidden',
235
+ }}
236
+ >
237
+ {cloneChildrenWithModes(media, modes)}
238
+ </View>
239
+ )
240
+ }, [media, mediaWidth, mediaHeight, modes])
241
+
186
242
  return (
187
243
  <View style={[containerStyle, style]} testID={testID}>
244
+ {mediaContent}
188
245
  <View style={textWrapStyle}>
189
246
  {eyebrow ? <Text style={eyebrowStyle}>{eyebrow}</Text> : null}
190
247
  {headline ? <Text style={headlineStyle}>{headline}</Text> : null}
@@ -1,11 +1,26 @@
1
- import React, { isValidElement, cloneElement } from 'react';
2
- import { View, Text, StyleSheet, type ViewStyle } from 'react-native';
1
+ import React from 'react';
2
+ import { View, Text, type ViewStyle } from 'react-native';
3
3
  import ButtonGroup from '../ButtonGroup/ButtonGroup';
4
4
  import AvatarGroup from '../AvatarGroup/AvatarGroup';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import { EMPTY_MODES } from '../../utils/react-utils';
7
7
  import MoneyValue from '../MoneyValue/MoneyValue';
8
- import Button from '../Button/Button';
8
+
9
+ // Defaults applied to the inner ButtonGroup so the card matches Figma out of
10
+ // the box. They are spread *before* the caller's `modes` so any consumer can
11
+ // override an individual key (e.g. swap the size to "M").
12
+ const DEFAULT_BUTTON_GROUP_MODES: Record<string, string> = {
13
+ AppearanceBrand: 'Secondary',
14
+ 'Button / Size': 'S',
15
+ Emphasis: 'High',
16
+ };
17
+
18
+ // Defaults applied to the inner MoneyValue so the price renders at the
19
+ // 36 px / 900-weight scale defined for cards in Figma. Same merge rule as
20
+ // the button group — consumer `modes` override these.
21
+ const DEFAULT_MONEY_VALUE_MODES: Record<string, string> = {
22
+ Context3: 'Balance & Cards',
23
+ };
9
24
 
10
25
 
11
26
  export type RechargeCardProps = {
@@ -66,17 +81,19 @@ export default function RechargeCard({
66
81
  modes = EMPTY_MODES,
67
82
  style,
68
83
  }: RechargeCardProps) {
69
- // Container Tokens
70
- const backgroundColor = getVariableByName('rechargeCard/background', modes) || '#f6f3ff';
84
+ // Container Tokens (defaults mirror Figma node 2235:937).
85
+ const backgroundColor = getVariableByName('rechargeCard/background', modes) || '#ffffff';
71
86
  const paddingHorizontal = parseInt(getVariableByName('rechargeCard/padding/horizontal', modes) || 16, 10);
72
87
  const paddingVertical = parseInt(getVariableByName('rechargeCard/padding/vertical', modes) || 20, 10);
73
88
  const gap = parseInt(getVariableByName('rechargeCard/gap', modes) || 20, 10);
74
89
  const radius = parseInt(getVariableByName('rechargeCard/radius', modes) || 20, 10);
75
- const minWidth = parseInt(getVariableByName('rechargeCard/minWidth', modes) || 328, 10);
90
+ const minWidth = parseInt(getVariableByName('rechargeCard/minWidth', modes) || 312, 10);
91
+ const strokeWidth = parseInt(getVariableByName('rechargeCard/strokeWidth', modes) || 1, 10);
92
+ const strokeColor = getVariableByName('rechargeCard/stroke/color', modes) || '#ebebed';
76
93
 
77
94
  // Header Tokens
78
95
  const headerGap = parseInt(getVariableByName('rechargeCard/header/gap', modes) || 4, 10);
79
- const titleColor = getVariableByName('rechargeCard/title/color', modes) || '#13002d';
96
+ const titleColor = getVariableByName('rechargeCard/title/color', modes) || '#000000';
80
97
  const titleFontSize = parseInt(getVariableByName('rechargeCard/title/fontSize', modes) || 12, 10);
81
98
  const titleFontFamily = getVariableByName('rechargeCard/title/fontFamily', modes) || 'JioType Var';
82
99
  const titleLineHeight = parseInt(getVariableByName('rechargeCard/title/lineHeight', modes) || 14, 10);
@@ -87,30 +104,26 @@ export default function RechargeCard({
87
104
  const specItemGap = parseInt(getVariableByName('rechargeCard/specItem/gap', modes) || 4, 10);
88
105
 
89
106
  // Spec Label Tokens
90
- const specLabelColor = getVariableByName('rechargeCard/specItem/label/color', modes) || '#13002d';
107
+ const specLabelColor = getVariableByName('rechargeCard/specItem/label/color', modes) || '#000000';
91
108
  const specLabelFontSize = parseInt(getVariableByName('rechargeCard/specItem/label/fontSize', modes) || 12, 10);
92
109
  const specLabelFontFamily = getVariableByName('rechargeCard/specItem/label/fontFamily', modes) || 'JioType Var';
93
110
  const specLabelLineHeight = parseInt(getVariableByName('rechargeCard/specItem/label/lineHeight', modes) || 14, 10);
94
111
  const specLabelFontWeight = getVariableByName('rechargeCard/specItem/label/fontWeight', modes) || '500';
95
112
 
96
113
  // Spec Value Tokens
97
- const specValueColor = getVariableByName('rechargeCard/specItem/value/color', modes) || '#310064';
114
+ const specValueColor = getVariableByName('rechargeCard/specItem/value/color', modes) || '#000000';
98
115
  const specValueFontSize = parseInt(getVariableByName('rechargeCard/specItem/value/fontSize', modes) || 14, 10);
99
116
  const specValueFontFamily = getVariableByName('rechargeCard/specItem/value/fontFamily', modes) || 'JioType Var';
100
117
  const specValueLineHeight = parseInt(getVariableByName('rechargeCard/specItem/value/lineHeight', modes) || 17, 10);
101
118
  const specValueFontWeight = getVariableByName('rechargeCard/specItem/value/fontWeight', modes) || '500';
102
119
 
103
120
  // Disclaimer Tokens
104
- const disclaimerColor = getVariableByName('rechargeCard/disclaimer/color', modes) || '#22004a';
121
+ const disclaimerColor = getVariableByName('rechargeCard/disclaimer/color', modes) || '#000000';
105
122
  const disclaimerFontSize = parseInt(getVariableByName('rechargeCard/disclaimer/fontSize', modes) || 10, 10);
106
123
  const disclaimerFontFamily = getVariableByName('rechargeCard/disclaimer/fontFamily', modes) || 'JioType Var';
107
124
  const disclaimerLineHeight = parseInt(getVariableByName('rechargeCard/disclaimer/lineHeight', modes) || 13, 10);
108
125
  const disclaimerFontWeight = getVariableByName('rechargeCard/disclaimer/fontWeight', modes) || '400';
109
126
 
110
- // Button Group Tokens
111
- // Handled by ButtonGroup component directly
112
-
113
- // Helpers
114
127
  const resolveFontWeight = (weight: string | number) => typeof weight === 'number' ? weight.toString() : weight;
115
128
 
116
129
  // Pass modes to subscription children (e.g. AvatarGroup)
@@ -125,6 +138,8 @@ export default function RechargeCard({
125
138
  paddingVertical,
126
139
  gap,
127
140
  borderRadius: radius,
141
+ borderWidth: strokeWidth,
142
+ borderColor: strokeColor,
128
143
  minWidth,
129
144
  alignItems: 'flex-start',
130
145
  }, style]}>
@@ -142,7 +157,7 @@ export default function RechargeCard({
142
157
  <MoneyValue
143
158
  value={price}
144
159
  currency="₹"
145
- modes={modes}
160
+ modes={{ ...DEFAULT_MONEY_VALUE_MODES, ...modes }}
146
161
  />
147
162
  </View>
148
163
 
@@ -224,15 +239,8 @@ export default function RechargeCard({
224
239
  {disclaimer}
225
240
  </Text>
226
241
 
227
- {/* Button Group */}
228
- <ButtonGroup
229
- modes={{
230
- ...modes,
231
- "Appearance.Brand": "Secondary",
232
- "Button / Size": "S",
233
- "Emphasis": "High"
234
- }}
235
- >
242
+ {/* Button Group: defaults are overridable via the consumer's `modes` */}
243
+ <ButtonGroup modes={{ ...DEFAULT_BUTTON_GROUP_MODES, ...modes }}>
236
244
  {actions}
237
245
  </ButtonGroup>
238
246
  </View>
@@ -3,7 +3,7 @@ import { View, Text, type ViewStyle, type TextStyle } from 'react-native'
3
3
  import Svg, { Path } from 'react-native-svg'
4
4
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
5
5
  import { StepLabel } from './StepLabel'
6
- import { EMPTY_MODES } from '../../utils/react-utils'
6
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
7
7
 
8
8
  export type StepStatus = 'number' | 'complete' | 'error' | 'warning'
9
9
 
@@ -14,14 +14,21 @@ export type StepProps = {
14
14
  // Injected by Stepper, or provided manually
15
15
  index?: number
16
16
 
17
- // Custom style for the connecting line, allowing Stepper to hide it for the last item
17
+ // Connector line visibility explicit prop (Figma `showLine`).
18
+ // Stepper sets this to false on the last item.
19
+ showLine?: boolean
20
+
21
+ // Custom style for the connecting line. Kept for backwards compatibility;
22
+ // when provided, it is merged on top of the default line style.
18
23
  connectorStyle?: ViewStyle
19
24
 
20
- // Props matching Figma component interface directly
25
+ // Content props matching Figma component interface
21
26
  title?: string
22
27
  supportingText?: string
23
- subtitle?: boolean // Toggle for subtitle visibility if passed via props
24
- status?: StepStatus // 'number' | 'complete' | 'error' | 'warning'
28
+ metaText?: string
29
+ subtitle?: boolean
30
+ meta?: boolean
31
+ status?: StepStatus
25
32
  }
26
33
 
27
34
  export function Step({
@@ -29,35 +36,37 @@ export function Step({
29
36
  modes = EMPTY_MODES,
30
37
  style,
31
38
  index = 0,
39
+ showLine = true,
32
40
  connectorStyle,
33
41
  title,
34
42
  supportingText,
43
+ metaText,
35
44
  subtitle = true,
45
+ meta = true,
36
46
  status = 'number',
37
47
  }: StepProps) {
38
- // Container styles
39
48
  const minHeight =
40
49
  Number(getVariableByName('steperItem/minHeight', modes) as unknown) || 52
41
50
  const gap =
42
- Number(getVariableByName('steperItem/gap', modes) as unknown) || 8
51
+ Number(getVariableByName('steperItem/gap', modes) as unknown) || 16
43
52
 
44
- // Indicator dimensions and styles
45
53
  const indicatorSize =
46
- Number(getVariableByName('stepIndicator/size', modes) as unknown) || 24
54
+ Number(getVariableByName('stepIndicator/size', modes) as unknown) || 36
47
55
  const indicatorRadius =
48
56
  Number(getVariableByName('stepIndicator/radius', modes) as unknown) || 9999
49
57
  const indicatorBg =
50
58
  (getVariableByName('stepIndicator/background', modes) as string) ||
51
- '#5c00b5'
59
+ '#5d00b5'
52
60
  const indicatorBorderColor =
53
61
  (getVariableByName('stepIndicator/border/color', modes) as string) ||
54
- '#5c00b5'
62
+ '#5d00b5'
55
63
  const indicatorBorderSize =
56
64
  Number(getVariableByName('stepIndicator/border/size', modes) as unknown) || 1
57
65
  const iconColor =
58
66
  (getVariableByName('stepIndicator/icon/color', modes) as string) || '#ffffff'
67
+ const stepStatusSize =
68
+ Number(getVariableByName('stepStatus/size', modes) as unknown) || 18
59
69
 
60
- // Label styles (for number)
61
70
  const labelFontSize =
62
71
  Number(getVariableByName('stepIndicator/label/fontSize', modes) as unknown) || 12
63
72
  const labelFontFamily =
@@ -67,28 +76,28 @@ export function Step({
67
76
  const labelFontWeight =
68
77
  (getVariableByName('stepIndicator/label/fontWeight', modes) as string) || '500'
69
78
 
70
- // Vertical line styles
71
79
  const lineSize =
72
80
  Number(getVariableByName('steperItem/lineSize', modes) as unknown) || 2
73
81
  const lineColor =
74
- (getVariableByName('steperItem/line', modes) as string) || '#5c00b5'
82
+ (getVariableByName('steperItem/line', modes) as string) || '#5d00b5'
75
83
  const badgeWrapGap =
76
84
  Number(getVariableByName('steperItem/badgeWrap/gap', modes) as unknown) || 2
77
85
 
78
86
  const containerStyle: ViewStyle = {
79
87
  flexDirection: 'row',
80
88
  minHeight,
89
+ gap,
81
90
  ...style,
82
91
  }
83
92
 
84
- // FIX: This wrapper should NOT expand. It should be exact width of the indicator.
85
93
  const indicatorWrapperStyle: ViewStyle = {
86
94
  flexDirection: 'column',
87
95
  alignItems: 'center',
88
96
  width: indicatorSize,
89
- flexGrow: 0, // Do NOT grow
97
+ flexGrow: 0,
90
98
  flexShrink: 0,
91
99
  gap: badgeWrapGap,
100
+ alignSelf: 'stretch',
92
101
  }
93
102
 
94
103
  const indicatorStyle: ViewStyle = {
@@ -103,6 +112,13 @@ export function Step({
103
112
  overflow: 'hidden',
104
113
  }
105
114
 
115
+ const stepStatusContainerStyle: ViewStyle = {
116
+ width: stepStatusSize,
117
+ height: stepStatusSize,
118
+ alignItems: 'center',
119
+ justifyContent: 'center',
120
+ }
121
+
106
122
  const labelStyle: TextStyle = {
107
123
  color: iconColor,
108
124
  fontSize: labelFontSize,
@@ -112,35 +128,31 @@ export function Step({
112
128
  textAlign: 'center',
113
129
  }
114
130
 
115
- // Combine base line style with injected connectorStyle
116
131
  const lineStyle: ViewStyle = {
117
132
  width: lineSize,
118
133
  backgroundColor: lineColor,
119
- flexGrow: 1, // Line should take up remaining vertical space in this column
120
- ...connectorStyle, // Allow overriding display, color, etc.
134
+ flexGrow: 1,
135
+ minHeight: 1,
136
+ ...connectorStyle,
121
137
  }
122
138
 
123
- // Helper for icons
124
139
  const renderIcon = () => {
125
140
  switch (status) {
126
141
  case 'complete':
127
- // Checkmark
128
142
  return (
129
- <Svg width={12} height={12} viewBox="0 0 24 24" fill="none">
143
+ <Svg width={stepStatusSize} height={stepStatusSize} viewBox="0 0 24 24" fill="none">
130
144
  <Path d="M20 6L9 17L4 12" stroke={iconColor} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
131
145
  </Svg>
132
146
  )
133
147
  case 'error':
134
- // X mark
135
148
  return (
136
- <Svg width={12} height={12} viewBox="0 0 24 24" fill="none">
149
+ <Svg width={stepStatusSize} height={stepStatusSize} viewBox="0 0 24 24" fill="none">
137
150
  <Path d="M18 6L6 18M6 6l12 12" stroke={iconColor} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
138
151
  </Svg>
139
152
  )
140
153
  case 'warning':
141
- // Exclamation / Triangle
142
154
  return (
143
- <Svg width={12} height={12} viewBox="0 0 24 24" fill="none">
155
+ <Svg width={stepStatusSize} height={stepStatusSize} viewBox="0 0 24 24" fill="none">
144
156
  <Path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" stroke={iconColor} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
145
157
  </Svg>
146
158
  )
@@ -150,42 +162,31 @@ export function Step({
150
162
  }
151
163
  }
152
164
 
153
- // Clone children to pass modes if they are StepLabel
154
- const childrenWithProps = React.Children.map(children, (child) => {
155
- if (React.isValidElement(child)) {
156
- return React.cloneElement(child, { modes } as any)
157
- }
158
- return child
159
- })
160
-
161
- // Determine if we have content to pad
162
- // Default padding bottom to 16 if line is shown, but also respect connectorStyle display
163
- // If display is none, we probably don't want the padding bottom either.
164
- const isLineHidden = connectorStyle?.display === 'none'
165
- const contentPaddingBottom = isLineHidden ? 0 : 16
165
+ const hasSlotChildren = React.Children.count(children) > 0
166
166
 
167
167
  return (
168
168
  <View style={containerStyle}>
169
169
  <View style={indicatorWrapperStyle}>
170
170
  <View style={indicatorStyle}>
171
- {renderIcon()}
171
+ <View style={stepStatusContainerStyle}>{renderIcon()}</View>
172
172
  </View>
173
- <View style={lineStyle} />
173
+ {showLine ? <View style={lineStyle} /> : null}
174
174
  </View>
175
175
 
176
- {/* Spacer gap */}
177
- <View style={{ width: gap }} />
178
-
179
- {/* Content Area */}
180
- <View style={{ flex: 1, paddingBottom: contentPaddingBottom }}>
181
- {title ? (
176
+ <View style={{ flex: 1 }}>
177
+ {hasSlotChildren ? (
178
+ cloneChildrenWithModes(children, modes)
179
+ ) : (
182
180
  <StepLabel
183
- title={title}
184
- supportingText={subtitle ? (supportingText || undefined) : undefined}
181
+ {...(title !== undefined ? { title } : {})}
182
+ {...(supportingText !== undefined
183
+ ? { supportingText }
184
+ : {})}
185
+ {...(metaText !== undefined ? { metaText } : {})}
186
+ subtitle={subtitle}
187
+ meta={meta}
185
188
  modes={modes}
186
189
  />
187
- ) : (
188
- childrenWithProps
189
190
  )}
190
191
  </View>
191
192
  </View>
@@ -6,6 +6,9 @@ import { EMPTY_MODES } from '../../utils/react-utils'
6
6
  export type StepLabelProps = {
7
7
  title?: string
8
8
  supportingText?: string
9
+ metaText?: string
10
+ subtitle?: boolean
11
+ meta?: boolean
9
12
  modes?: Record<string, any>
10
13
  style?: ViewStyle
11
14
  }
@@ -13,10 +16,12 @@ export type StepLabelProps = {
13
16
  export function StepLabel({
14
17
  title = 'Stepper Item',
15
18
  supportingText,
19
+ metaText,
20
+ subtitle = true,
21
+ meta = true,
16
22
  modes = EMPTY_MODES,
17
23
  style,
18
24
  }: StepLabelProps) {
19
- // Title styles
20
25
  const titleColor =
21
26
  (getVariableByName('steperItem/title/color', modes) as string) || '#0d0d0f'
22
27
  const titleFontSize =
@@ -30,26 +35,46 @@ export function StepLabel({
30
35
  const titleFontWeight =
31
36
  (getVariableByName('steperItem/title/fontWeight', modes) as string) || '700'
32
37
 
33
- // Subtitle styles
38
+ // The Subtitle (supportingText) and Meta both default to the "Neutral"
39
+ // AppearanceBrand. A caller-supplied `AppearanceBrand` still wins (it is
40
+ // spread after the default), so each remains overridable. The Title keeps
41
+ // its own appearance resolution.
42
+ const subtitleModes = { AppearanceBrand: 'Neutral', ...modes }
43
+ const metaModes = { AppearanceBrand: 'Neutral', ...modes }
44
+
34
45
  const subtitleColor =
35
- (getVariableByName('steperItem/subtitle/color', modes) as string) ||
46
+ (getVariableByName('steperItem/subtitle/color', subtitleModes) as string) ||
36
47
  '#3d4047'
37
48
  const subtitleFontSize =
38
49
  Number(
39
- getVariableByName('steperItem/subtitle/fontSize', modes) as unknown
50
+ getVariableByName('steperItem/subtitle/fontSize', subtitleModes) as unknown
40
51
  ) || 12
41
52
  const subtitleFontFamily =
42
- (getVariableByName('steperItem/subtitle/fontFamily', modes) as string) ||
53
+ (getVariableByName('steperItem/subtitle/fontFamily', subtitleModes) as string) ||
43
54
  undefined
44
55
  const subtitleLineHeight =
45
56
  Number(
46
- getVariableByName('steperItem/subtitle/lineHeight', modes) as unknown
57
+ getVariableByName('steperItem/subtitle/lineHeight', subtitleModes) as unknown
47
58
  ) || 16
48
59
  const subtitleFontWeight =
49
- (getVariableByName('steperItem/subtitle/fontWeight', modes) as string) ||
60
+ (getVariableByName('steperItem/subtitle/fontWeight', subtitleModes) as string) ||
50
61
  '400'
51
62
 
52
- // Layout gap
63
+ const metaColor =
64
+ (getVariableByName('steperItem/meta/color', metaModes) as string) || '#f7ab21'
65
+ const metaFontSize =
66
+ Number(getVariableByName('steperItem/meta/fontSize', metaModes) as unknown) || 10
67
+ // The Figma variable is authored as "fontFamily Copy" (with a space + suffix).
68
+ // Match the literal Figma name to avoid a missing-variable warning.
69
+ const metaFontFamily =
70
+ (getVariableByName('steperItem/meta/fontFamily Copy', metaModes) as string) ||
71
+ undefined
72
+ const metaLineHeight =
73
+ Number(getVariableByName('steperItem/meta/lineHeight', metaModes) as unknown) ||
74
+ 12
75
+ const metaFontWeight =
76
+ (getVariableByName('steperItem/meta/fontWeight', metaModes) as string) || '700'
77
+
53
78
  const textGap =
54
79
  Number(getVariableByName('steperItem/textWrap/gap', modes) as unknown) || 2
55
80
 
@@ -69,12 +94,24 @@ export function StepLabel({
69
94
  lineHeight: subtitleLineHeight,
70
95
  }
71
96
 
97
+ const metaStyle: TextStyle = {
98
+ color: metaColor,
99
+ fontSize: metaFontSize,
100
+ fontFamily: metaFontFamily,
101
+ fontWeight: metaFontWeight as TextStyle['fontWeight'],
102
+ lineHeight: metaLineHeight,
103
+ }
104
+
105
+ const showSubtitle = subtitle && !!supportingText
106
+ const showMeta = meta && !!metaText
107
+
72
108
  return (
73
109
  <View style={[{ gap: textGap, flex: 1 }, style]}>
74
110
  <Text style={titleStyle}>{title}</Text>
75
- {supportingText ? (
111
+ {showSubtitle ? (
76
112
  <Text style={subtitleStyle}>{supportingText}</Text>
77
113
  ) : null}
114
+ {showMeta ? <Text style={metaStyle}>{metaText}</Text> : null}
78
115
  </View>
79
116
  )
80
117
  }
@@ -6,8 +6,10 @@ import { StepLabel } from './StepLabel'
6
6
  import { EMPTY_MODES } from '../../utils/react-utils'
7
7
 
8
8
  export { Step, StepLabel }
9
+ export type { StepProps, StepStatus } from './Step'
10
+ export type { StepLabelProps } from './StepLabel'
9
11
 
10
- type StepperProps = {
12
+ export type StepperProps = {
11
13
  children?: React.ReactNode
12
14
  modes?: Record<string, any>
13
15
  style?: ViewStyle
@@ -18,7 +20,6 @@ export default function Stepper({
18
20
  modes = EMPTY_MODES,
19
21
  style,
20
22
  }: StepperProps) {
21
- // Stepper container styles
22
23
  const paddingHorizontal =
23
24
  Number(getVariableByName('stepper/padding/horizontal', modes) as unknown) ||
24
25
  8
@@ -33,20 +34,24 @@ export default function Stepper({
33
34
  ...style,
34
35
  }
35
36
 
36
- // Inject index and connectorStyle logic into Step children
37
37
  const steps = React.Children.toArray(children)
38
- const childrenWithProps = steps.map((child, index) => {
39
- if (React.isValidElement(child)) {
40
- const isLast = index === steps.length - 1
41
-
42
- return React.cloneElement(child, {
43
- index,
44
- modes, // Pass modes down
45
- connectorStyle: isLast ? { display: 'none' } : undefined,
46
- } as any)
47
- }
48
- return child
38
+ const stepsWithProps = steps.map((child, stepIndex) => {
39
+ if (!React.isValidElement(child)) return child
40
+
41
+ const isLast = stepIndex === steps.length - 1
42
+ const childProps = (child.props as any) || {}
43
+ const childModes = childProps.modes
44
+ ? { ...modes, ...childProps.modes }
45
+ : modes
46
+
47
+ return React.cloneElement(child, {
48
+ ...childProps,
49
+ index: childProps.index ?? stepIndex,
50
+ modes: childModes,
51
+ showLine:
52
+ childProps.showLine !== undefined ? childProps.showLine : !isLast,
53
+ } as any)
49
54
  })
50
55
 
51
- return <View style={containerStyle}>{childrenWithProps}</View>
56
+ return <View style={containerStyle}>{stepsWithProps}</View>
52
57
  }