jfs-components 0.0.77 → 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 (70) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/lib/commonjs/components/Accordion/Accordion.js +55 -55
  3. package/lib/commonjs/components/ActionFooter/ActionFooter.js +48 -2
  4. package/lib/commonjs/components/Checkbox/Checkbox.js +21 -9
  5. package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -16
  6. package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +167 -0
  7. package/lib/commonjs/components/FormField/FormField.js +14 -1
  8. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +355 -0
  9. package/lib/commonjs/components/ListItem/ListItem.js +25 -10
  10. package/lib/commonjs/components/MessageField/MessageField.js +318 -0
  11. package/lib/commonjs/components/NavArrow/NavArrow.js +58 -17
  12. package/lib/commonjs/components/Stepper/Step.js +47 -60
  13. package/lib/commonjs/components/Stepper/StepLabel.js +40 -10
  14. package/lib/commonjs/components/Stepper/Stepper.js +15 -17
  15. package/lib/commonjs/components/SuggestiveSearch/SuggestiveSearch.js +487 -0
  16. package/lib/commonjs/components/TextInput/TextInput.js +16 -1
  17. package/lib/commonjs/components/Title/Title.js +10 -2
  18. package/lib/commonjs/components/index.js +28 -0
  19. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  20. package/lib/commonjs/icons/registry.js +1 -1
  21. package/lib/module/components/Accordion/Accordion.js +56 -56
  22. package/lib/module/components/ActionFooter/ActionFooter.js +50 -4
  23. package/lib/module/components/Checkbox/Checkbox.js +22 -10
  24. package/lib/module/components/DropdownInput/DropdownInput.js +30 -16
  25. package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +161 -0
  26. package/lib/module/components/FormField/FormField.js +16 -3
  27. package/lib/module/components/FullscreenModal/FullscreenModal.js +350 -0
  28. package/lib/module/components/ListItem/ListItem.js +25 -10
  29. package/lib/module/components/MessageField/MessageField.js +313 -0
  30. package/lib/module/components/NavArrow/NavArrow.js +59 -18
  31. package/lib/module/components/Stepper/Step.js +48 -61
  32. package/lib/module/components/Stepper/StepLabel.js +40 -10
  33. package/lib/module/components/Stepper/Stepper.js +15 -17
  34. package/lib/module/components/SuggestiveSearch/SuggestiveSearch.js +481 -0
  35. package/lib/module/components/TextInput/TextInput.js +17 -2
  36. package/lib/module/components/Title/Title.js +10 -2
  37. package/lib/module/components/index.js +4 -0
  38. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  39. package/lib/module/icons/registry.js +1 -1
  40. package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -20
  41. package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +63 -0
  42. package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +99 -0
  43. package/lib/typescript/src/components/MessageField/MessageField.d.ts +81 -0
  44. package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +10 -5
  45. package/lib/typescript/src/components/Stepper/Step.d.ts +4 -1
  46. package/lib/typescript/src/components/Stepper/StepLabel.d.ts +4 -1
  47. package/lib/typescript/src/components/Stepper/Stepper.d.ts +3 -1
  48. package/lib/typescript/src/components/SuggestiveSearch/SuggestiveSearch.d.ts +123 -0
  49. package/lib/typescript/src/components/index.d.ts +7 -3
  50. package/lib/typescript/src/icons/registry.d.ts +1 -1
  51. package/package.json +1 -1
  52. package/src/components/Accordion/Accordion.tsx +113 -73
  53. package/src/components/ActionFooter/ActionFooter.tsx +56 -4
  54. package/src/components/Checkbox/Checkbox.tsx +22 -9
  55. package/src/components/DropdownInput/DropdownInput.tsx +67 -39
  56. package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +237 -0
  57. package/src/components/FormField/FormField.tsx +19 -3
  58. package/src/components/FullscreenModal/FullscreenModal.tsx +414 -0
  59. package/src/components/ListItem/ListItem.tsx +21 -10
  60. package/src/components/MessageField/MessageField.tsx +543 -0
  61. package/src/components/NavArrow/NavArrow.tsx +81 -17
  62. package/src/components/Stepper/Step.tsx +52 -51
  63. package/src/components/Stepper/StepLabel.tsx +46 -9
  64. package/src/components/Stepper/Stepper.tsx +20 -15
  65. package/src/components/SuggestiveSearch/SuggestiveSearch.tsx +756 -0
  66. package/src/components/TextInput/TextInput.tsx +14 -1
  67. package/src/components/Title/Title.tsx +13 -2
  68. package/src/components/index.ts +7 -3
  69. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  70. package/src/icons/registry.ts +1 -1
@@ -0,0 +1,414 @@
1
+ import React, { useMemo } from 'react'
2
+ import {
3
+ View,
4
+ Text,
5
+ ScrollView,
6
+ type StyleProp,
7
+ type ViewStyle,
8
+ type TextStyle,
9
+ } from 'react-native'
10
+ import Animated, {
11
+ Extrapolation,
12
+ interpolate,
13
+ useAnimatedScrollHandler,
14
+ useAnimatedStyle,
15
+ useSharedValue,
16
+ } from 'react-native-reanimated'
17
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
18
+ import { useTokens } from '../../design-tokens/JFSThemeProvider'
19
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
20
+ import Button from '../Button/Button'
21
+ import Disclaimer from '../Disclaimer/Disclaimer'
22
+ import IconButton from '../IconButton/IconButton'
23
+ import ActionFooter from '../ActionFooter/ActionFooter'
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Forced modes
27
+ //
28
+ // `FullscreenModal` always themes itself with the `context5: 'Fullscreen Modal'`
29
+ // collection mode. This is what flips the section / list-item / hero text
30
+ // tokens to their white-on-dark values (see the Figma "Fullscreen Modal"
31
+ // context). It is intentionally NON-overridable: callers can pass any other
32
+ // modes (Color Mode, AppearanceBrand, …) but never context5. The frozen
33
+ // object keeps its identity stable so the token resolver's per-modes cache
34
+ // stays hot, and so `cloneChildrenWithModes` can use it as the
35
+ // always-wins `forcedModes` argument.
36
+ // ---------------------------------------------------------------------------
37
+ const FULLSCREEN_MODAL_FORCED_MODES = Object.freeze({ context5: 'Fullscreen Modal' })
38
+
39
+ // Reanimated-driven ScrollView so the parallax handler runs on the UI thread.
40
+ // Module scope so the wrapped component identity is stable across renders.
41
+ const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView)
42
+
43
+ // Parallax tuning. The hero collapses by HEIGHT only as the user scrolls up —
44
+ // its full width is preserved and the media keeps a fixed aspect ratio (it is
45
+ // cropped, never scaled or squished, like a `cover` background). When no
46
+ // explicit `heroMinHeight` is given, the hero collapses to this fraction of
47
+ // its resting height.
48
+ const HERO_MIN_HEIGHT_RATIO = 0.45
49
+
50
+ export type FullscreenModalProps = {
51
+ /** Small eyebrow line above the headline. */
52
+ eyebrow?: string
53
+ /** Large hero headline. */
54
+ headline?: string
55
+ /** Supporting paragraph shown below the headline. */
56
+ supportingText?: string
57
+ /** Secondary line below the supporting paragraph (e.g. a price / timeline). */
58
+ priceText?: string
59
+ /**
60
+ * Media rendered full-bleed behind the hero text and driven by the parallax
61
+ * scroll effect. Bring any renderer — most commonly a `LottiePlayer`, but an
62
+ * `Image`, `Video`, or `SvgXml` works too. Size it to fill the hero box
63
+ * (`heroHeight` tall, full modal width) and let it `cover` so that as the
64
+ * hero collapses in height the art is cropped, never distorted. `modes` are
65
+ * cascaded into it.
66
+ */
67
+ heroMedia?: React.ReactNode
68
+ /** Resting height of the hero region. Defaults to 420. */
69
+ heroHeight?: number
70
+ /**
71
+ * Collapsed height the hero shrinks to at full scroll. Defaults to
72
+ * `heroHeight * 0.45`. Only the height changes — the width is always full.
73
+ */
74
+ heroMinHeight?: number
75
+ /** Enable the scroll-driven hero collapse. Defaults to true. */
76
+ parallax?: boolean
77
+ /** Whether to render the floating close button (top-right). Defaults to true. */
78
+ showClose?: boolean
79
+ /** Press handler for the close button. */
80
+ onClose?: () => void
81
+ /** Accessibility label for the close button. */
82
+ closeAccessibilityLabel?: string
83
+ /**
84
+ * Fully custom footer content rendered inside the sticky `ActionFooter`.
85
+ * When provided, `primaryActionLabel` / `disclaimer` are ignored.
86
+ */
87
+ footer?: React.ReactNode
88
+ /** Label for the default primary action button in the footer. */
89
+ primaryActionLabel?: string
90
+ /** Press handler for the default primary action button. */
91
+ onPrimaryAction?: () => void
92
+ /** Disclaimer text shown below the default primary action button. */
93
+ disclaimer?: string
94
+ /** Solid backdrop color for the scrollable body. Defaults to a near-black. */
95
+ backgroundColor?: string
96
+ /** Body content (typically `Section`s). `modes` are cascaded automatically. */
97
+ children?: React.ReactNode
98
+ /** Mode configuration. `context5` is always forced to `'Fullscreen Modal'`. */
99
+ modes?: Record<string, any>
100
+ /** Style overrides for the outer container. */
101
+ style?: StyleProp<ViewStyle>
102
+ /** Style overrides for the scroll body wrapper (the dark content area). */
103
+ contentContainerStyle?: StyleProp<ViewStyle>
104
+ testID?: string
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Hero text — the eyebrow / headline / supporting / price block. Built inline
109
+ // (rather than reusing <PageHero>) so we can render BOTH a supporting
110
+ // paragraph AND a price line with the exact PageHero token gaps, and overlay
111
+ // it on the parallax media without PageHero's media/button scaffolding.
112
+ // ---------------------------------------------------------------------------
113
+ type HeroTextProps = {
114
+ eyebrow?: string
115
+ headline?: string
116
+ supportingText?: string
117
+ priceText?: string
118
+ modes: Record<string, any>
119
+ }
120
+
121
+ function HeroText({ eyebrow, headline, supportingText, priceText, modes }: HeroTextProps) {
122
+ const styles = useMemo(() => {
123
+ const gap = Number(getVariableByName('PageHero/gap', modes)) || 16
124
+ const textWrapGap = Number(getVariableByName('PageHero/textWrap/gap', modes)) || 8
125
+
126
+ const eyebrowStyle: TextStyle = {
127
+ color: (getVariableByName('PageHero/eyebrow/color', modes) as string) || '#ffffff',
128
+ fontFamily: (getVariableByName('PageHero/eyebrow/fontFamily', modes) as string) || 'System',
129
+ fontSize: Number(getVariableByName('PageHero/eyebrow/fontSize', modes)) || 18,
130
+ fontWeight: String(getVariableByName('PageHero/eyebrow/fontWeight', modes) || 700) as TextStyle['fontWeight'],
131
+ lineHeight: Number(getVariableByName('PageHero/eyebrow/lineHeight', modes)) || 20,
132
+ textAlign: 'center',
133
+ }
134
+ const headlineStyle: TextStyle = {
135
+ color: (getVariableByName('PageHero/headline/color', modes) as string) || '#ffffff',
136
+ fontFamily: (getVariableByName('PageHero/headline/fontFamily', modes) as string) || 'System',
137
+ fontSize: Number(getVariableByName('PageHero/headline/fontSize', modes)) || 29,
138
+ fontWeight: String(getVariableByName('PageHero/headline/fontWeight', modes) || 900) as TextStyle['fontWeight'],
139
+ lineHeight: Number(getVariableByName('PageHero/headline/lineHeight', modes)) || 29,
140
+ textAlign: 'center',
141
+ width: '100%',
142
+ }
143
+ const supportingTextStyle: TextStyle = {
144
+ color: (getVariableByName('PageHero/supportingText/color', modes) as string) || '#ffffff',
145
+ fontFamily: (getVariableByName('PageHero/supportingText/fontFamily', modes) as string) || 'System',
146
+ fontSize: Number(getVariableByName('PageHero/supportingText/fontSize', modes)) || 12,
147
+ fontWeight: String(getVariableByName('PageHero/supportingText/fontWeight', modes) || 500) as TextStyle['fontWeight'],
148
+ lineHeight: Number(getVariableByName('PageHero/supportingText/lineHeight', modes)) || 16,
149
+ textAlign: 'center',
150
+ }
151
+ const priceTextStyle: TextStyle = {
152
+ color: (getVariableByName('PageHero/body/color', modes) as string) || '#ffffff',
153
+ fontFamily: (getVariableByName('PageHero/body/fontFamily', modes) as string) || 'System',
154
+ fontSize: Number(getVariableByName('PageHero/body/fontSize', modes)) || 12,
155
+ fontWeight: String(getVariableByName('PageHero/body/fontWeight', modes) || 500) as TextStyle['fontWeight'],
156
+ lineHeight: Number(getVariableByName('PageHero/body/lineHeight', modes)) || 16,
157
+ textAlign: 'center',
158
+ }
159
+
160
+ return {
161
+ container: { width: '100%', alignItems: 'center', gap } as ViewStyle,
162
+ textWrap: { width: '100%', alignItems: 'center', gap: textWrapGap } as ViewStyle,
163
+ eyebrowStyle,
164
+ headlineStyle,
165
+ supportingTextStyle,
166
+ priceTextStyle,
167
+ }
168
+ }, [modes])
169
+
170
+ return (
171
+ <View style={styles.container}>
172
+ <View style={styles.textWrap}>
173
+ {eyebrow ? <Text style={styles.eyebrowStyle}>{eyebrow}</Text> : null}
174
+ {headline ? <Text style={styles.headlineStyle}>{headline}</Text> : null}
175
+ </View>
176
+ {supportingText ? <Text style={styles.supportingTextStyle}>{supportingText}</Text> : null}
177
+ {priceText ? <Text style={styles.priceTextStyle}>{priceText}</Text> : null}
178
+ </View>
179
+ )
180
+ }
181
+
182
+ /**
183
+ * FullscreenModal — a full-screen takeover surface with a parallax media hero,
184
+ * a scrollable body, a floating close button, and a sticky `ActionFooter`.
185
+ *
186
+ * The component always themes itself with `context5: 'Fullscreen Modal'`
187
+ * (non-overridable) so every nested component (Section, ListItem, Button,
188
+ * Disclaimer, …) resolves the white-on-dark "fullscreen modal" token values.
189
+ * That mode is cascaded into `children`, the footer, and the hero text via
190
+ * `cloneChildrenWithModes` / the merged `modes` object.
191
+ *
192
+ * ### Parallax
193
+ * As the user scrolls up, the hero collapses by **height only** (from
194
+ * `heroHeight` to `heroMinHeight`) — its **full width is always preserved**.
195
+ * The `heroMedia` is pinned to the top at a fixed size and `cover`-cropped by
196
+ * the collapsing clip, so it keeps a perfect aspect ratio the whole time
197
+ * (never scaled or squished). Because it collapses slower than the content
198
+ * scrolls, the media lags behind for the parallax depth cue. Disable with
199
+ * `parallax={false}`.
200
+ *
201
+ * @component
202
+ * @example
203
+ * ```tsx
204
+ * <FullscreenModal
205
+ * eyebrow="Upgrade to JioFinance+"
206
+ * headline="Get more from your money."
207
+ * supportingText="JioFinance+ is your upgraded financial experience…"
208
+ * priceText="₹999/year · ₹0 until 2027"
209
+ * heroMedia={<LottiePlayer source={hero} size={{ width: 360, height: 420 }} />}
210
+ * primaryActionLabel="Upgrade for free"
211
+ * disclaimer="By upgrading, we'll check your eligibility with Experian."
212
+ * onPrimaryAction={() => upgrade()}
213
+ * onClose={() => navigation.goBack()}
214
+ * >
215
+ * <Section title="Key Benefits" slotDirection="column" slot={…} />
216
+ * <Section title="Compare plans" slotDirection="column" slot={…} />
217
+ * </FullscreenModal>
218
+ * ```
219
+ */
220
+ function FullscreenModal({
221
+ eyebrow = 'Upgrade to JioFinance+',
222
+ headline = 'Get more from your money.',
223
+ supportingText = 'JioFinance+ is your upgraded financial experience, designed to work harder in the background so your money works smarter in real life.',
224
+ priceText = '₹999/year · ₹0 until 2027',
225
+ heroMedia,
226
+ heroHeight = 420,
227
+ heroMinHeight,
228
+ parallax = true,
229
+ showClose = true,
230
+ onClose,
231
+ closeAccessibilityLabel = 'Close',
232
+ footer,
233
+ primaryActionLabel = 'Upgrade for free',
234
+ onPrimaryAction,
235
+ disclaimer = "By upgrading, we'll check your eligibility with Experian.",
236
+ backgroundColor = '#0f0d0a',
237
+ children,
238
+ modes: propModes = EMPTY_MODES,
239
+ style,
240
+ contentContainerStyle,
241
+ testID,
242
+ }: FullscreenModalProps) {
243
+ const { modes: globalModes } = useTokens()
244
+
245
+ // context5 is appended last so it always wins, regardless of what the
246
+ // caller (or the global theme) passes.
247
+ const modes = useMemo(
248
+ () => ({ ...globalModes, ...propModes, ...FULLSCREEN_MODAL_FORCED_MODES }),
249
+ [globalModes, propModes]
250
+ )
251
+
252
+ const rootGap = Number(getVariableByName('fullScreenModal/gap', modes)) || 16
253
+
254
+ const minHeight = heroMinHeight ?? Math.round(heroHeight * HERO_MIN_HEIGHT_RATIO)
255
+
256
+ const scrollY = useSharedValue(0)
257
+ const onScroll = useAnimatedScrollHandler((event) => {
258
+ scrollY.value = event.contentOffset.y
259
+ })
260
+
261
+ // Collapse the hero by HEIGHT only as the user scrolls up. The clip's width
262
+ // never changes and the media inside is pinned full-size at the top, so the
263
+ // art is cropped (cover) rather than scaled or narrowed — it keeps a perfect
264
+ // aspect ratio the whole time. Pull-down (negative offset) is clamped, so the
265
+ // hero never grows past its resting height.
266
+ const heroAnimatedStyle = useAnimatedStyle(() => {
267
+ const height = interpolate(
268
+ scrollY.value,
269
+ [0, heroHeight],
270
+ [heroHeight, minHeight],
271
+ Extrapolation.CLAMP
272
+ )
273
+ return { height }
274
+ })
275
+
276
+ const processedHeroMedia = useMemo(
277
+ () =>
278
+ heroMedia ? cloneChildrenWithModes(heroMedia, modes, FULLSCREEN_MODAL_FORCED_MODES) : null,
279
+ [heroMedia, modes]
280
+ )
281
+
282
+ const processedChildren = useMemo(
283
+ () =>
284
+ children ? cloneChildrenWithModes(children, modes, FULLSCREEN_MODAL_FORCED_MODES) : null,
285
+ [children, modes]
286
+ )
287
+
288
+ // The clip is full-width and top-pinned; its height is what animates. Width
289
+ // is intentionally never animated.
290
+ const heroClipBaseStyle = useMemo<ViewStyle>(
291
+ () => ({
292
+ position: 'absolute',
293
+ top: 0,
294
+ left: 0,
295
+ right: 0,
296
+ overflow: 'hidden',
297
+ }),
298
+ []
299
+ )
300
+
301
+ // The media sits at a fixed full-size box pinned to the top of the clip, so
302
+ // the collapsing clip crops it from the bottom (cover) instead of resizing
303
+ // it. Full width, fixed height — a perfect, constant aspect ratio.
304
+ const heroMediaWrapStyle = useMemo<ViewStyle>(
305
+ () => ({
306
+ position: 'absolute',
307
+ top: 0,
308
+ left: 0,
309
+ right: 0,
310
+ height: heroHeight,
311
+ alignItems: 'stretch',
312
+ }),
313
+ [heroHeight]
314
+ )
315
+
316
+ const heroTextRegionStyle = useMemo<ViewStyle>(
317
+ () => ({
318
+ height: heroHeight,
319
+ justifyContent: 'flex-end',
320
+ paddingHorizontal: 16,
321
+ paddingBottom: 16,
322
+ }),
323
+ [heroHeight]
324
+ )
325
+
326
+ const bodyStyle = useMemo<StyleProp<ViewStyle>>(
327
+ () => [
328
+ {
329
+ backgroundColor,
330
+ gap: rootGap,
331
+ paddingTop: rootGap,
332
+ paddingBottom: 24,
333
+ },
334
+ contentContainerStyle,
335
+ ],
336
+ [backgroundColor, rootGap, contentContainerStyle]
337
+ )
338
+
339
+ const heroClip = (
340
+ <Animated.View
341
+ style={[heroClipBaseStyle, parallax ? heroAnimatedStyle : { height: heroHeight }]}
342
+ pointerEvents="none"
343
+ >
344
+ <View style={heroMediaWrapStyle}>{processedHeroMedia}</View>
345
+ </Animated.View>
346
+ )
347
+
348
+ // Footer: a fully custom node, or the default Button + Disclaimer column.
349
+ let footerContent: React.ReactNode = null
350
+ if (footer) {
351
+ footerContent = footer
352
+ } else if (primaryActionLabel) {
353
+ footerContent = (
354
+ <View style={footerColumnStyle}>
355
+ <Button
356
+ label={primaryActionLabel}
357
+ modes={modes}
358
+ style={fullWidthStyle}
359
+ {...(onPrimaryAction ? { onPress: onPrimaryAction } : {})}
360
+ />
361
+ {disclaimer ? <Disclaimer disclaimer={disclaimer} modes={modes} /> : null}
362
+ </View>
363
+ )
364
+ }
365
+
366
+ return (
367
+ <View style={[rootStyle, { backgroundColor }, style]} testID={testID}>
368
+ {processedHeroMedia ? heroClip : null}
369
+
370
+ <AnimatedScrollView
371
+ style={scrollViewStyle}
372
+ contentContainerStyle={scrollContentStyle}
373
+ showsVerticalScrollIndicator={false}
374
+ onScroll={onScroll}
375
+ scrollEventThrottle={16}
376
+ >
377
+ <View style={heroTextRegionStyle}>
378
+ <HeroText
379
+ eyebrow={eyebrow}
380
+ headline={headline}
381
+ supportingText={supportingText}
382
+ priceText={priceText}
383
+ modes={modes}
384
+ />
385
+ </View>
386
+ <View style={bodyStyle}>{processedChildren}</View>
387
+ </AnimatedScrollView>
388
+
389
+ {footerContent ? (
390
+ <ActionFooter modes={modes}>{footerContent}</ActionFooter>
391
+ ) : null}
392
+
393
+ {showClose ? (
394
+ <IconButton
395
+ iconName="ic_close"
396
+ modes={modes}
397
+ accessibilityLabel={closeAccessibilityLabel}
398
+ style={closeButtonStyle}
399
+ {...(onClose ? { onPress: onClose } : {})}
400
+ />
401
+ ) : null}
402
+ </View>
403
+ )
404
+ }
405
+
406
+ // Module-scope style constants — never re-allocated per render.
407
+ const rootStyle: ViewStyle = { flex: 1, width: '100%', position: 'relative' }
408
+ const scrollViewStyle: ViewStyle = { flex: 1 }
409
+ const scrollContentStyle: ViewStyle = { flexGrow: 1 }
410
+ const footerColumnStyle: ViewStyle = { width: '100%', gap: 8 }
411
+ const fullWidthStyle: ViewStyle = { width: '100%' }
412
+ const closeButtonStyle: ViewStyle = { position: 'absolute', top: 12, right: 12 }
413
+
414
+ export default FullscreenModal
@@ -72,8 +72,19 @@ interface ListItemTokens {
72
72
  }
73
73
 
74
74
  function resolveListItemTokens(modes: Record<string, any>): ListItemTokens {
75
+ // Modes used to cascade into slot children (leading / supportSlot / endSlot).
76
+ // We do NOT inject an `AppearanceBrand` default here: slot content such as
77
+ // Buttons or Badges carry their own intended appearance, so forcing one onto
78
+ // them would be surprising.
75
79
  const resolvedModes = { ...modes, Context: 'ListItem' }
76
80
 
81
+ // Modes used to resolve the ListItem's OWN title + support text. Within this
82
+ // component, `AppearanceBrand` only affects `listItem/title/color` and
83
+ // `listItem/supportText/color`, so the text defaults to the "Neutral"
84
+ // appearance (in both Vertical and Horizontal layouts). A caller-supplied
85
+ // `AppearanceBrand` still wins; `Context` is always forced to 'ListItem'.
86
+ const textModes = { AppearanceBrand: 'Neutral', ...modes, Context: 'ListItem' }
87
+
77
88
  const gap = (getVariableByName('listItem/gap', resolvedModes) ?? 8) as number
78
89
  const paddingTop = getVariableByName('listItem/padding/top', resolvedModes) ?? 0
79
90
  const paddingBottom = getVariableByName('listItem/padding/bottom', resolvedModes) ?? 0
@@ -81,19 +92,19 @@ function resolveListItemTokens(modes: Record<string, any>): ListItemTokens {
81
92
  const paddingRight = getVariableByName('listItem/padding/right', resolvedModes) ?? 0
82
93
  const textWrapGap = (getVariableByName('listItem/text wrap', resolvedModes) ?? 0) as number
83
94
 
84
- const titleColor = getVariableByName('listItem/title/color', resolvedModes) || '#0f0d0a'
85
- const titleFontSize = getVariableByName('listItem/title/fontSize', resolvedModes) || 14
86
- const titleLineHeight = getVariableByName('listItem/title/lineHeight', resolvedModes) || 16
87
- const titleFontFamily = getVariableByName('listItem/title/fontFamily', resolvedModes) || 'System'
88
- const titleFontWeightRaw = getVariableByName('listItem/title/fontWeight', resolvedModes) || 700
95
+ const titleColor = getVariableByName('listItem/title/color', textModes) || '#0f0d0a'
96
+ const titleFontSize = getVariableByName('listItem/title/fontSize', textModes) || 14
97
+ const titleLineHeight = getVariableByName('listItem/title/lineHeight', textModes) || 16
98
+ const titleFontFamily = getVariableByName('listItem/title/fontFamily', textModes) || 'System'
99
+ const titleFontWeightRaw = getVariableByName('listItem/title/fontWeight', textModes) || 700
89
100
  const titleFontWeight =
90
101
  typeof titleFontWeightRaw === 'number' ? titleFontWeightRaw.toString() : titleFontWeightRaw
91
102
 
92
- const supportColor = getVariableByName('listItem/supportText/color', resolvedModes) || '#1f1a14'
93
- const supportFontSize = getVariableByName('listItem/supportText/fontSize', resolvedModes) || 12
94
- const supportLineHeight = getVariableByName('listItem/supportText/lineHeight', resolvedModes) || 14
95
- const supportFontFamily = getVariableByName('listItem/supportText/fontFamily', resolvedModes) || 'System'
96
- const supportFontWeightRaw = getVariableByName('listItem/supportText/fontWeight', resolvedModes) || 500
103
+ const supportColor = getVariableByName('listItem/supportText/color', textModes) || '#1f1a14'
104
+ const supportFontSize = getVariableByName('listItem/supportText/fontSize', textModes) || 12
105
+ const supportLineHeight = getVariableByName('listItem/supportText/lineHeight', textModes) || 14
106
+ const supportFontFamily = getVariableByName('listItem/supportText/fontFamily', textModes) || 'System'
107
+ const supportFontWeightRaw = getVariableByName('listItem/supportText/fontWeight', textModes) || 500
97
108
  const supportFontWeight =
98
109
  typeof supportFontWeightRaw === 'number' ? supportFontWeightRaw.toString() : supportFontWeightRaw
99
110