jfs-components 0.1.34 → 0.1.35

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.
@@ -23,10 +23,10 @@ export type LottiePlayerProps = {
23
23
  * Override the rendered size. Pass a number for a square box, or
24
24
  * `{ width, height }` for non-square.
25
25
  *
26
- * When omitted, size is resolved from the `media/width` and `media/height`
27
- * design tokens (default `117 × 117`). The `Media / Output` collection
26
+ * When omitted, size is resolved from the `video/width` and `video/height`
27
+ * design tokens (default `117 × 117`). The `Video / Output` collection
28
28
  * exposes `L | M | S` modes (117 / 70 / 20) — pass
29
- * `modes={{ 'Media / Output': 'M' }}` to render at 70×70, etc.
29
+ * `modes={{ 'Video / Output': 'M' }}` to render at 70×70, etc.
30
30
  */
31
31
  size?: number | {
32
32
  width: number;
@@ -64,8 +64,8 @@ export type LottiePlayerProps = {
64
64
  * webpack via platform extensions — same pattern as `MediaCard/GlassFill`.
65
65
  *
66
66
  * Token-driven sizing: when `size` is omitted, `LottiePlayer` reads
67
- * `media/width` and `media/height` from the Figma variables resolver, so the
68
- * animation matches the surrounding component's `Media / Output` mode
67
+ * `video/width` and `video/height` from the Figma variables resolver, so the
68
+ * animation matches the surrounding component's `Video / Output` mode
69
69
  * automatically. This is the same sizing contract `PageHero` and
70
70
  * `LottieIntroBlock` use for their `media` slots.
71
71
  *
@@ -76,7 +76,7 @@ export type LottiePlayerProps = {
76
76
  *
77
77
  * <LottiePlayer source={animation} /> // 117 × 117 (default)
78
78
  * <LottiePlayer source={animation} size={70} /> // 70 × 70
79
- * <LottiePlayer source={animation} modes={{ 'Media / Output': 'S' }} /> // 20 × 20
79
+ * <LottiePlayer source={animation} modes={{ 'Video / Output': 'S' }} /> // 20 × 20
80
80
  * <PageHero media={<LottiePlayer source={animation} />} />
81
81
  * ```
82
82
  */
@@ -4,12 +4,18 @@ import type { Modes } from '../../design-tokens';
4
4
  export type PageHeroProps = {
5
5
  /** Small eyebrow text shown above the headline. */
6
6
  eyebrow?: string;
7
+ /** Whether to render the eyebrow line. */
8
+ showEyebrow?: boolean;
7
9
  /** Main headline text. Centered and bold. */
8
10
  headline?: string;
9
- /** Optional supporting text shown below the headline. */
11
+ /** Supporting text shown directly below the headline (e.g. price). */
10
12
  supportingText?: string;
11
13
  /** Whether to render the supporting text. */
12
14
  showSupportingText?: boolean;
15
+ /** Optional second supporting line shown below `supportingText`. */
16
+ body?: string;
17
+ /** Whether to render the body line. */
18
+ showBody?: boolean;
13
19
  /** Label for the default action button. Ignored when `buttonSlot` is provided. */
14
20
  buttonLabel?: string;
15
21
  /** Press handler for the default action button. Ignored when `buttonSlot` is provided. */
@@ -17,9 +23,10 @@ export type PageHeroProps = {
17
23
  /** Whether to render the default action button. Ignored when `buttonSlot` is provided. */
18
24
  showButton?: boolean;
19
25
  /**
20
- * Optional slot to fully override the action button.
21
- * When provided, `showButton`, `buttonLabel`, and `onButtonPress` are ignored.
22
- * `modes` are automatically cascaded into this slot.
26
+ * Optional slot to fully override the action button area (e.g. a
27
+ * `ButtonGroup` with several buttons). When provided, `showButton`,
28
+ * `buttonLabel`, and `onButtonPress` are ignored. `modes` are automatically
29
+ * cascaded into this slot.
23
30
  */
24
31
  buttonSlot?: React.ReactNode;
25
32
  /**
@@ -27,16 +34,18 @@ export type PageHeroProps = {
27
34
  *
28
35
  * Intentionally typed as `React.ReactNode` so the consumer can bring any
29
36
  * renderer they like — `<Image />`, `<IconCapsule />`, a Lottie player
30
- * (`lottie-react-native` / `@lottiefiles/dotlottie-react`), an `<SvgXml />`
31
- * from `react-native-svg`, a `<Video />` from `react-native-video`, a
32
- * gradient view, or a custom illustration. The library deliberately does
33
- * NOT wrap Lottie / video runtimes (they require native modules + pod
34
- * autolinking on every consumer's app), so PageHero just allocates a
35
- * token-sized container and lets the slot render whatever it wants.
37
+ * (`LottiePlayer` / `lottie-react-native`), an `<SvgXml />` from
38
+ * `react-native-svg`, a `<Video />` from `react-native-video`, a gradient
39
+ * view, or a custom illustration. The library deliberately does NOT wrap
40
+ * Lottie / video runtimes (they require native modules + pod autolinking on
41
+ * every consumer's app), so PageHero just allocates a token-sized container
42
+ * and lets the slot render whatever it wants.
36
43
  *
37
- * The slot is rendered inside a `width × height` box driven by
38
- * `media/width` and `media/height` tokens (default 117×117). `modes` are
39
- * automatically cascaded into the slot via `cloneChildrenWithModes`.
44
+ * The slot is rendered inside a `width × height` box driven by the
45
+ * `video/width` and `video/height` tokens from the `Video / Output`
46
+ * collection (`L | M | S` → 117 / 70 / 20; default `L` = 117×117).
47
+ * `modes` are automatically cascaded into the slot via
48
+ * `cloneChildrenWithModes`.
40
49
  */
41
50
  media?: React.ReactNode;
42
51
  /** Mode configuration for design-token theming. */
@@ -49,12 +58,16 @@ export type PageHeroProps = {
49
58
  * PageHero displays a centered hero block typically used at the top of a page
50
59
  * or feature screen. It contains an optional media slot (illustration / image
51
60
  * / Lottie / SVG / video — consumer's choice), an eyebrow line, a large
52
- * headline, an optional supporting line (e.g. price / timeline), and an
53
- * optional action button.
61
+ * headline, an optional supporting line (e.g. price), an optional body line
62
+ * (e.g. timeline), and an optional action button.
54
63
  *
55
64
  * All visual values are resolved from Figma design tokens via
56
- * `getVariableByName`. Slots cascade the active `modes` to their children
57
- * through `cloneChildrenWithModes`.
65
+ * `getVariableByName` (Figma node 4453:2348 "Page Hero"). Text colors react
66
+ * to the `Page type` (`MainPage | SubPage | JioPlus`) and `Color Mode`
67
+ * (`Light | Dark`) collections; type scale reacts to `PageHero Size`
68
+ * (`M | S`); the media box reacts to `Video / Output` (`L | M | S`). Slots
69
+ * cascade the active `modes` to their children through
70
+ * `cloneChildrenWithModes`.
58
71
  *
59
72
  * @component
60
73
  * @example
@@ -65,16 +78,11 @@ export type PageHeroProps = {
65
78
  * supportingText="₹999/year · ₹0 until 2027"
66
79
  * buttonLabel="Renew for free"
67
80
  * onButtonPress={() => navigate('Upgrade')}
68
- * media={
69
- * <Image
70
- * imageSource={require('./assets/upgrade.png')}
71
- * width={117}
72
- * height={117}
73
- * />
74
- * }
81
+ * modes={{ 'Page type': 'JioPlus' }}
82
+ * media={<LottiePlayer source={animation} />}
75
83
  * />
76
84
  * ```
77
85
  */
78
- declare function PageHero({ eyebrow, headline, supportingText, showSupportingText, buttonLabel, onButtonPress, showButton, buttonSlot, media, modes: propModes, style, testID, }: PageHeroProps): import("react/jsx-runtime").JSX.Element;
86
+ declare function PageHero({ eyebrow, showEyebrow, headline, supportingText, showSupportingText, body, showBody, buttonLabel, onButtonPress, showButton, buttonSlot, media, modes: propModes, style, testID, }: PageHeroProps): import("react/jsx-runtime").JSX.Element;
79
87
  export default PageHero;
80
88
  //# sourceMappingURL=PageHero.d.ts.map
@@ -33,6 +33,7 @@ export declare const FIGMA_MODES: {
33
33
  readonly "BankAccountCard / Output": readonly ["Default"];
34
34
  readonly "Benefit Card": readonly ["Default"];
35
35
  readonly Blur: readonly ["Default"];
36
+ readonly "Border Boolean": readonly ["False", "True"];
36
37
  readonly "bottomNav / Output": readonly ["Default"];
37
38
  readonly "BottomNavItem / Output": readonly ["Default"];
38
39
  readonly "BottomNavItem / State": readonly ["Idle", "Active"];
@@ -47,6 +48,8 @@ export declare const FIGMA_MODES: {
47
48
  readonly "Card / Output": readonly ["Default"];
48
49
  readonly "Card Apperance": readonly ["Default", "Plain"];
49
50
  readonly "Card Feedback / Output": readonly ["Default"];
51
+ readonly "Card Tab / output": readonly ["Default"];
52
+ readonly "Card Tab State": readonly ["Default", "Active"];
50
53
  readonly "Card.Media": readonly ["Default"];
51
54
  readonly "Card/CTA / Output": readonly ["Default"];
52
55
  readonly "Card/Insight": readonly ["Default"];
@@ -112,6 +115,9 @@ export declare const FIGMA_MODES: {
112
115
  readonly "Emphasis / DataViz": readonly ["High", "Medium", "Low"];
113
116
  readonly "EmptyState / Output": readonly ["Default"];
114
117
  readonly "ExpandableCheckbox / Output": readonly ["Default"];
118
+ readonly "Favorite Toggle Color": readonly ["Default", "Active"];
119
+ readonly "Favorite Toggle Size": readonly ["S", "M", "L"];
120
+ readonly "Favorite Toggle/ output": readonly ["Default"];
115
121
  readonly "Feedback / Output": readonly ["Default"];
116
122
  readonly "filterBar / Output": readonly ["Default"];
117
123
  readonly "FinancialConditionCard / Output": readonly ["Default"];
@@ -122,9 +128,13 @@ export declare const FIGMA_MODES: {
122
128
  readonly Gap: readonly ["S", "M", "L", "None"];
123
129
  readonly "Gauge / Output": readonly ["Default"];
124
130
  readonly "Grid / Output": readonly ["Default"];
131
+ readonly "Grid Gap": readonly ["S", "M"];
125
132
  readonly "Handle Boolean": readonly ["True", "False"];
126
133
  readonly "Heading / Output": readonly ["Default"];
127
134
  readonly "Heading text": readonly ["Default", "Slot"];
135
+ readonly "hello Jio / Output": readonly ["Default"];
136
+ readonly "Hello Jio Input Size": readonly ["M", "L"];
137
+ readonly "Hello Jio Input State": readonly ["Idle", "Active"];
128
138
  readonly "HeroSection / Output": readonly ["Default"];
129
139
  readonly "Holdings card / Output": readonly ["Default"];
130
140
  readonly "HStack / Output": readonly ["Default"];
@@ -175,6 +185,7 @@ export declare const FIGMA_MODES: {
175
185
  readonly "Page Hero / Output": readonly ["Default"];
176
186
  readonly "Page type": readonly ["MainPage", "SubPage", "JioPlus"];
177
187
  readonly "PageHero / Output": readonly ["Default"];
188
+ readonly "PageHero Size": readonly ["M", "S"];
178
189
  readonly "PaymnetFeedback / Output": readonly ["Default"];
179
190
  readonly "PDP cc card": readonly ["Default"];
180
191
  readonly peekOffset: readonly ["Medium", "Small", "Large", "None"];
@@ -196,6 +207,7 @@ export declare const FIGMA_MODES: {
196
207
  readonly "RangeTrack / Output": readonly ["Default"];
197
208
  readonly "Rating / Output": readonly ["Default"];
198
209
  readonly "Rating Star / Output": readonly ["Default"];
210
+ readonly "Rating Star Size": readonly ["S", "M", "L"];
199
211
  readonly "Rating Star State": readonly ["idle", "selected"];
200
212
  readonly "RechargeCard / Output": readonly ["Default"];
201
213
  readonly rotfl: readonly ["Default"];
@@ -240,7 +252,7 @@ export declare const FIGMA_MODES: {
240
252
  readonly "Tabs / Output": readonly ["Default"];
241
253
  readonly TestimonialsCard: readonly ["Default"];
242
254
  readonly "Text / Output": readonly ["Default"];
243
- readonly "Text Appearance": readonly ["Neutral", "Primary", "Secondary", "Tertiary"];
255
+ readonly "Text Appearance": readonly ["Neutral", "Primary", "Secondary", "Tertiary", "Success", "Warning", "Error"];
244
256
  readonly "Text Sizes": readonly ["Medium", "Small", "Large"];
245
257
  readonly "textInput / Output": readonly ["Default"];
246
258
  readonly "TextSegment / Output": readonly ["Default"];
@@ -259,6 +271,7 @@ export declare const FIGMA_MODES: {
259
271
  readonly "TransationDetails / Output": readonly ["Default"];
260
272
  readonly "UPI Handle / Output": readonly ["Default"];
261
273
  readonly "UPI Handle Image": readonly ["True", "False"];
274
+ readonly "valueBack metric": readonly ["Default"];
262
275
  readonly "ValueBar / Output": readonly ["Default"];
263
276
  readonly "Video / Output": readonly ["L", "M", "S"];
264
277
  readonly "VStack / Output": readonly ["Default"];
@@ -4,7 +4,7 @@
4
4
  * Auto-generated from SVG files in src/icons/
5
5
  * DO NOT EDIT MANUALLY - Run "npm run icons:generate" to regenerate
6
6
  *
7
- * Generated: 2026-07-20T11:16:24.422Z
7
+ * Generated: 2026-07-20T12:46:13.819Z
8
8
  */
9
9
  export declare const iconRegistry: Record<string, {
10
10
  path: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jfs-components",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "description": "React Native Jio Finance Components Library",
5
5
  "author": "sunshuaiqi@gmail.com",
6
6
  "license": "MIT",
@@ -29,10 +29,10 @@ export type LottiePlayerProps = {
29
29
  * Override the rendered size. Pass a number for a square box, or
30
30
  * `{ width, height }` for non-square.
31
31
  *
32
- * When omitted, size is resolved from the `media/width` and `media/height`
33
- * design tokens (default `117 × 117`). The `Media / Output` collection
32
+ * When omitted, size is resolved from the `video/width` and `video/height`
33
+ * design tokens (default `117 × 117`). The `Video / Output` collection
34
34
  * exposes `L | M | S` modes (117 / 70 / 20) — pass
35
- * `modes={{ 'Media / Output': 'M' }}` to render at 70×70, etc.
35
+ * `modes={{ 'Video / Output': 'M' }}` to render at 70×70, etc.
36
36
  */
37
37
  size?: number | { width: number; height: number }
38
38
  /** Play the animation on mount. Defaults to `true`. */
@@ -57,9 +57,9 @@ function resolveSize(
57
57
  if (typeof size === 'number') return { width: size, height: size }
58
58
  if (size && typeof size === 'object') return size
59
59
  const width =
60
- Number(getVariableByName('media/width', modes))
60
+ Number(getVariableByName('video/width', modes)) || DEFAULT_SIZE
61
61
  const height =
62
- Number(getVariableByName('media/height', modes))
62
+ Number(getVariableByName('video/height', modes)) || DEFAULT_SIZE
63
63
  return { width, height }
64
64
  }
65
65
 
@@ -83,8 +83,8 @@ function resolveSize(
83
83
  * webpack via platform extensions — same pattern as `MediaCard/GlassFill`.
84
84
  *
85
85
  * Token-driven sizing: when `size` is omitted, `LottiePlayer` reads
86
- * `media/width` and `media/height` from the Figma variables resolver, so the
87
- * animation matches the surrounding component's `Media / Output` mode
86
+ * `video/width` and `video/height` from the Figma variables resolver, so the
87
+ * animation matches the surrounding component's `Video / Output` mode
88
88
  * automatically. This is the same sizing contract `PageHero` and
89
89
  * `LottieIntroBlock` use for their `media` slots.
90
90
  *
@@ -95,7 +95,7 @@ function resolveSize(
95
95
  *
96
96
  * <LottiePlayer source={animation} /> // 117 × 117 (default)
97
97
  * <LottiePlayer source={animation} size={70} /> // 70 × 70
98
- * <LottiePlayer source={animation} modes={{ 'Media / Output': 'S' }} /> // 20 × 20
98
+ * <LottiePlayer source={animation} modes={{ 'Video / Output': 'S' }} /> // 20 × 20
99
99
  * <PageHero media={<LottiePlayer source={animation} />} />
100
100
  * ```
101
101
  */
@@ -28,9 +28,9 @@ function resolveSize(
28
28
  if (typeof size === 'number') return { width: size, height: size }
29
29
  if (size && typeof size === 'object') return size
30
30
  const width =
31
- Number(getVariableByName('media/width', modes))
31
+ Number(getVariableByName('video/width', modes)) || DEFAULT_SIZE
32
32
  const height =
33
- Number(getVariableByName('media/height', modes))
33
+ Number(getVariableByName('video/height', modes)) || DEFAULT_SIZE
34
34
  return { width, height }
35
35
  }
36
36
 
@@ -12,15 +12,26 @@ import Button from '../Button/Button'
12
12
  import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
13
13
  import type { Modes } from '../../design-tokens'
14
14
 
15
+ // Figma media frame is 117×117 ("Video / Output" L). Used when the token
16
+ // collection is missing from a consumer's variables JSON so the media slot
17
+ // never collapses to 0.
18
+ const MEDIA_FALLBACK = 117
19
+
15
20
  export type PageHeroProps = {
16
21
  /** Small eyebrow text shown above the headline. */
17
22
  eyebrow?: string
23
+ /** Whether to render the eyebrow line. */
24
+ showEyebrow?: boolean
18
25
  /** Main headline text. Centered and bold. */
19
26
  headline?: string
20
- /** Optional supporting text shown below the headline. */
27
+ /** Supporting text shown directly below the headline (e.g. price). */
21
28
  supportingText?: string
22
29
  /** Whether to render the supporting text. */
23
30
  showSupportingText?: boolean
31
+ /** Optional second supporting line shown below `supportingText`. */
32
+ body?: string
33
+ /** Whether to render the body line. */
34
+ showBody?: boolean
24
35
  /** Label for the default action button. Ignored when `buttonSlot` is provided. */
25
36
  buttonLabel?: string
26
37
  /** Press handler for the default action button. Ignored when `buttonSlot` is provided. */
@@ -28,9 +39,10 @@ export type PageHeroProps = {
28
39
  /** Whether to render the default action button. Ignored when `buttonSlot` is provided. */
29
40
  showButton?: boolean
30
41
  /**
31
- * Optional slot to fully override the action button.
32
- * When provided, `showButton`, `buttonLabel`, and `onButtonPress` are ignored.
33
- * `modes` are automatically cascaded into this slot.
42
+ * Optional slot to fully override the action button area (e.g. a
43
+ * `ButtonGroup` with several buttons). When provided, `showButton`,
44
+ * `buttonLabel`, and `onButtonPress` are ignored. `modes` are automatically
45
+ * cascaded into this slot.
34
46
  */
35
47
  buttonSlot?: React.ReactNode
36
48
  /**
@@ -38,16 +50,18 @@ export type PageHeroProps = {
38
50
  *
39
51
  * Intentionally typed as `React.ReactNode` so the consumer can bring any
40
52
  * renderer they like — `<Image />`, `<IconCapsule />`, a Lottie player
41
- * (`lottie-react-native` / `@lottiefiles/dotlottie-react`), an `<SvgXml />`
42
- * from `react-native-svg`, a `<Video />` from `react-native-video`, a
43
- * gradient view, or a custom illustration. The library deliberately does
44
- * NOT wrap Lottie / video runtimes (they require native modules + pod
45
- * autolinking on every consumer's app), so PageHero just allocates a
46
- * token-sized container and lets the slot render whatever it wants.
53
+ * (`LottiePlayer` / `lottie-react-native`), an `<SvgXml />` from
54
+ * `react-native-svg`, a `<Video />` from `react-native-video`, a gradient
55
+ * view, or a custom illustration. The library deliberately does NOT wrap
56
+ * Lottie / video runtimes (they require native modules + pod autolinking on
57
+ * every consumer's app), so PageHero just allocates a token-sized container
58
+ * and lets the slot render whatever it wants.
47
59
  *
48
- * The slot is rendered inside a `width × height` box driven by
49
- * `media/width` and `media/height` tokens (default 117×117). `modes` are
50
- * automatically cascaded into the slot via `cloneChildrenWithModes`.
60
+ * The slot is rendered inside a `width × height` box driven by the
61
+ * `video/width` and `video/height` tokens from the `Video / Output`
62
+ * collection (`L | M | S` → 117 / 70 / 20; default `L` = 117×117).
63
+ * `modes` are automatically cascaded into the slot via
64
+ * `cloneChildrenWithModes`.
51
65
  */
52
66
  media?: React.ReactNode
53
67
  /** Mode configuration for design-token theming. */
@@ -61,12 +75,16 @@ export type PageHeroProps = {
61
75
  * PageHero displays a centered hero block typically used at the top of a page
62
76
  * or feature screen. It contains an optional media slot (illustration / image
63
77
  * / Lottie / SVG / video — consumer's choice), an eyebrow line, a large
64
- * headline, an optional supporting line (e.g. price / timeline), and an
65
- * optional action button.
78
+ * headline, an optional supporting line (e.g. price), an optional body line
79
+ * (e.g. timeline), and an optional action button.
66
80
  *
67
81
  * All visual values are resolved from Figma design tokens via
68
- * `getVariableByName`. Slots cascade the active `modes` to their children
69
- * through `cloneChildrenWithModes`.
82
+ * `getVariableByName` (Figma node 4453:2348 "Page Hero"). Text colors react
83
+ * to the `Page type` (`MainPage | SubPage | JioPlus`) and `Color Mode`
84
+ * (`Light | Dark`) collections; type scale reacts to `PageHero Size`
85
+ * (`M | S`); the media box reacts to `Video / Output` (`L | M | S`). Slots
86
+ * cascade the active `modes` to their children through
87
+ * `cloneChildrenWithModes`.
70
88
  *
71
89
  * @component
72
90
  * @example
@@ -77,21 +95,19 @@ export type PageHeroProps = {
77
95
  * supportingText="₹999/year · ₹0 until 2027"
78
96
  * buttonLabel="Renew for free"
79
97
  * onButtonPress={() => navigate('Upgrade')}
80
- * media={
81
- * <Image
82
- * imageSource={require('./assets/upgrade.png')}
83
- * width={117}
84
- * height={117}
85
- * />
86
- * }
98
+ * modes={{ 'Page type': 'JioPlus' }}
99
+ * media={<LottiePlayer source={animation} />}
87
100
  * />
88
101
  * ```
89
102
  */
90
103
  function PageHero({
91
104
  eyebrow = 'Upgrade to JioFinance+',
105
+ showEyebrow = true,
92
106
  headline = 'Resume earning cashback, extra points, and 1% gold',
93
107
  supportingText = '₹999/year · ₹0 until 2027',
94
108
  showSupportingText = true,
109
+ body,
110
+ showBody = true,
95
111
  buttonLabel = 'Renew for free',
96
112
  onButtonPress,
97
113
  showButton = true,
@@ -108,51 +124,99 @@ function PageHero({
108
124
  )
109
125
 
110
126
  const gap = Number(getVariableByName('PageHero/gap', modes))
111
- const paddingHorizontal =
112
- Number(getVariableByName('PageHero/padding/horizontal', modes))
127
+ const textWrapGap = Number(getVariableByName('PageHero/textWrap/gap', modes))
113
128
 
114
- const textWrapGap =
115
- Number(getVariableByName('PageHero/textWrap/gap', modes))
129
+ // Media slot box — Figma frame 4540:7845 ("Video"), sized by the
130
+ // `Video / Output` collection (L=117, M=70, S=20). Falls back to 117 when
131
+ // the collection is absent so the slot never collapses to 0×0.
132
+ const mediaWidth =
133
+ Number(getVariableByName('video/width', modes)) || MEDIA_FALLBACK
134
+ const mediaHeight =
135
+ Number(getVariableByName('video/height', modes)) || MEDIA_FALLBACK
116
136
 
117
- // Media slot box — matches the 117×117 frame in Figma (node 4540:7845).
118
- // Tokens fall back to 117 when not defined in the variables collection,
119
- // so the layout stays stable on consumers that haven't tokenized this yet.
120
- const mediaWidth = Number(getVariableByName('media/width', modes))
121
- const mediaHeight = Number(getVariableByName('media/height', modes))
137
+ const eyebrowStyle = useMemo<TextStyle>(
138
+ () => ({
139
+ color: getVariableByName('PageHero/eyebrow/color', modes) as string,
140
+ fontFamily: getVariableByName(
141
+ 'PageHero/eyebrow/fontFamily',
142
+ modes
143
+ ) as string,
144
+ fontSize: Number(getVariableByName('PageHero/eyebrow/fontSize', modes)),
145
+ fontWeight: String(
146
+ getVariableByName('PageHero/eyebrow/fontWeight', modes)
147
+ ) as TextStyle['fontWeight'],
148
+ lineHeight: Number(
149
+ getVariableByName('PageHero/eyebrow/lineHeight', modes)
150
+ ),
151
+ textAlign: 'center',
152
+ }),
153
+ [modes]
154
+ )
122
155
 
123
- const eyebrowColor =
124
- getVariableByName('PageHero/eyebrow/color', modes)
125
- const eyebrowFontFamily =
126
- getVariableByName('PageHero/eyebrow/fontFamily', modes)
127
- const eyebrowFontSize =
128
- Number(getVariableByName('PageHero/eyebrow/fontSize', modes))
129
- const eyebrowFontWeight =
130
- getVariableByName('PageHero/eyebrow/fontWeight', modes)
131
- const eyebrowLineHeight =
132
- Number(getVariableByName('PageHero/eyebrow/lineHeight', modes))
156
+ const headlineStyle = useMemo<TextStyle>(
157
+ () => ({
158
+ color: getVariableByName('PageHero/headline/color', modes) as string,
159
+ fontFamily: getVariableByName(
160
+ 'PageHero/headline/fontFamily',
161
+ modes
162
+ ) as string,
163
+ fontSize: Number(getVariableByName('PageHero/headline/fontSize', modes)),
164
+ fontWeight: String(
165
+ getVariableByName('PageHero/headline/fontWeight', modes)
166
+ ) as TextStyle['fontWeight'],
167
+ lineHeight: Number(
168
+ getVariableByName('PageHero/headline/lineHeight', modes)
169
+ ),
170
+ textAlign: 'center',
171
+ width: '100%',
172
+ }),
173
+ [modes]
174
+ )
133
175
 
134
- const headlineColor =
135
- getVariableByName('PageHero/headline/color', modes)
136
- const headlineFontFamily =
137
- getVariableByName('PageHero/headline/fontFamily', modes)
138
- const headlineFontSize =
139
- Number(getVariableByName('PageHero/headline/fontSize', modes))
140
- const headlineFontWeight =
141
- getVariableByName('PageHero/headline/fontWeight', modes)
142
- const headlineLineHeight =
143
- Number(getVariableByName('PageHero/headline/lineHeight', modes))
176
+ const supportingTextStyle = useMemo<TextStyle>(
177
+ () => ({
178
+ color: getVariableByName(
179
+ 'PageHero/supportingText/color',
180
+ modes
181
+ ) as string,
182
+ fontFamily: getVariableByName(
183
+ 'PageHero/supportingText/fontFamily',
184
+ modes
185
+ ) as string,
186
+ fontSize: Number(
187
+ getVariableByName('PageHero/supportingText/fontSize', modes)
188
+ ),
189
+ fontWeight: String(
190
+ getVariableByName('PageHero/supportingText/fontWeight', modes)
191
+ ) as TextStyle['fontWeight'],
192
+ lineHeight: Number(
193
+ getVariableByName('PageHero/supportingText/lineHeight', modes)
194
+ ),
195
+ textAlign: 'center',
196
+ }),
197
+ [modes]
198
+ )
144
199
 
145
- // Only `lineHeight` is tokenized for the supporting text in the Figma source.
146
- // Color, font size and weight are inline literals in the design (12px medium
147
- // white) we mirror that here so the visual stays faithful when no token
148
- // exists.
149
- const supportingTextLineHeight =
150
- Number(getVariableByName('PageHero/supportingText/lineHeight', modes))
200
+ const bodyStyle = useMemo<TextStyle>(
201
+ () => ({
202
+ color: getVariableByName('PageHero/body/color', modes) as string,
203
+ fontFamily: getVariableByName(
204
+ 'PageHero/body/fontFamily',
205
+ modes
206
+ ) as string,
207
+ fontSize: Number(getVariableByName('PageHero/body/fontSize', modes)),
208
+ fontWeight: String(
209
+ getVariableByName('PageHero/body/fontWeight', modes)
210
+ ) as TextStyle['fontWeight'],
211
+ lineHeight: Number(getVariableByName('PageHero/body/lineHeight', modes)),
212
+ textAlign: 'center',
213
+ }),
214
+ [modes]
215
+ )
151
216
 
152
217
  const containerStyle: ViewStyle = {
153
218
  flexDirection: 'column',
154
219
  alignItems: 'center',
155
- paddingHorizontal,
156
220
  gap,
157
221
  width: '100%',
158
222
  }
@@ -164,38 +228,6 @@ function PageHero({
164
228
  width: '100%',
165
229
  }
166
230
 
167
- const eyebrowStyle: TextStyle = {
168
- color: eyebrowColor as string,
169
- fontFamily: eyebrowFontFamily as string,
170
- fontSize: eyebrowFontSize,
171
- fontWeight: String(eyebrowFontWeight) as TextStyle['fontWeight'],
172
- lineHeight: eyebrowLineHeight,
173
- textAlign: 'center',
174
- }
175
-
176
- const headlineStyle: TextStyle = {
177
- color: headlineColor as string,
178
- fontFamily: headlineFontFamily as string,
179
- fontSize: headlineFontSize,
180
- fontWeight: String(headlineFontWeight) as TextStyle['fontWeight'],
181
- lineHeight: headlineLineHeight,
182
- textAlign: 'center',
183
- width: '100%',
184
- }
185
-
186
- const supportingTextStyle: TextStyle = {
187
- color: '#ffffff',
188
- fontFamily: 'System',
189
- fontSize: 12,
190
- fontWeight: '500',
191
- lineHeight: supportingTextLineHeight,
192
- textAlign: 'center',
193
- }
194
-
195
- const buttonWrapStyle: ViewStyle = {
196
- width: '100%',
197
- }
198
-
199
231
  const buttonContent = useMemo<React.ReactNode>(() => {
200
232
  if (buttonSlot !== undefined && buttonSlot !== null) {
201
233
  return cloneChildrenWithModes(buttonSlot, modes)
@@ -208,21 +240,17 @@ function PageHero({
208
240
  label={buttonLabel}
209
241
  onPress={onButtonPress}
210
242
  modes={modes}
211
- style={buttonWrapStyle}
243
+ style={FULL_WIDTH}
212
244
  />
213
245
  )
214
- // buttonWrapStyle is a literal object created above; it is intentionally
215
- // omitted from deps because its identity changes on every render but its
216
- // shape never does.
217
- // eslint-disable-next-line react-hooks/exhaustive-deps
218
246
  }, [buttonSlot, showButton, buttonLabel, onButtonPress, modes])
219
247
 
220
248
  // Sized container for the media slot. Always rendered when `media` is
221
- // provided, so the slot has a predictable box (matches Figma frame
222
- // 4540:7845 — 117×117 by default) even if the inner element omits its
223
- // own width/height. `overflow: 'hidden'` mirrors the Figma frame's
224
- // `clipsContent` so a slightly oversized illustration doesn't break
225
- // the centered layout.
249
+ // provided, so the slot has a predictable box (Figma frame 4540:7845 —
250
+ // 117×117 at `Video / Output = L`) even if the inner element omits its own
251
+ // width/height. `overflow: 'hidden'` mirrors the Figma frame's
252
+ // `clipsContent` so a slightly oversized illustration doesn't break the
253
+ // centered layout.
226
254
  const mediaContent = useMemo<React.ReactNode>(() => {
227
255
  if (media === undefined || media === null) return null
228
256
  return (
@@ -240,19 +268,27 @@ function PageHero({
240
268
  )
241
269
  }, [media, mediaWidth, mediaHeight, modes])
242
270
 
271
+ const hasEyebrow = showEyebrow && !!eyebrow
272
+ const hasHeadline = !!headline
273
+
243
274
  return (
244
275
  <View style={[containerStyle, style]} testID={testID}>
245
276
  {mediaContent}
246
- <View style={textWrapStyle}>
247
- {eyebrow ? <Text style={eyebrowStyle}>{eyebrow}</Text> : null}
248
- {headline ? <Text style={headlineStyle}>{headline}</Text> : null}
249
- </View>
277
+ {hasEyebrow || hasHeadline ? (
278
+ <View style={textWrapStyle}>
279
+ {hasEyebrow ? <Text style={eyebrowStyle}>{eyebrow}</Text> : null}
280
+ {hasHeadline ? <Text style={headlineStyle}>{headline}</Text> : null}
281
+ </View>
282
+ ) : null}
250
283
  {showSupportingText && supportingText ? (
251
284
  <Text style={supportingTextStyle}>{supportingText}</Text>
252
285
  ) : null}
286
+ {showBody && body ? <Text style={bodyStyle}>{body}</Text> : null}
253
287
  {buttonContent}
254
288
  </View>
255
289
  )
256
290
  }
257
291
 
292
+ const FULL_WIDTH: ViewStyle = { width: '100%' }
293
+
258
294
  export default PageHero