jfs-components 0.0.77 → 0.0.79
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.
- package/CHANGELOG.md +28 -0
- package/lib/commonjs/components/Accordion/Accordion.js +55 -55
- package/lib/commonjs/components/ActionFooter/ActionFooter.js +48 -2
- package/lib/commonjs/components/Attached/Attached.js +144 -0
- package/lib/commonjs/components/Card/Card.js +25 -2
- package/lib/commonjs/components/Checkbox/Checkbox.js +21 -9
- package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -16
- package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +167 -0
- package/lib/commonjs/components/FormField/FormField.js +14 -1
- package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +353 -0
- package/lib/commonjs/components/ListItem/ListItem.js +46 -24
- package/lib/commonjs/components/MessageField/MessageField.js +318 -0
- package/lib/commonjs/components/NavArrow/NavArrow.js +58 -17
- package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +328 -0
- package/lib/commonjs/components/Slot/Slot.js +73 -0
- package/lib/commonjs/components/Stepper/Step.js +47 -60
- package/lib/commonjs/components/Stepper/StepLabel.js +40 -10
- package/lib/commonjs/components/Stepper/Stepper.js +15 -17
- package/lib/commonjs/components/SuggestiveSearch/SuggestiveSearch.js +487 -0
- package/lib/commonjs/components/TextInput/TextInput.js +16 -1
- package/lib/commonjs/components/Title/Title.js +10 -2
- package/lib/commonjs/components/index.js +49 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/Accordion/Accordion.js +56 -56
- package/lib/module/components/ActionFooter/ActionFooter.js +50 -4
- package/lib/module/components/Attached/Attached.js +139 -0
- package/lib/module/components/Card/Card.js +25 -2
- package/lib/module/components/Checkbox/Checkbox.js +22 -10
- package/lib/module/components/DropdownInput/DropdownInput.js +30 -16
- package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +161 -0
- package/lib/module/components/FormField/FormField.js +16 -3
- package/lib/module/components/FullscreenModal/FullscreenModal.js +348 -0
- package/lib/module/components/ListItem/ListItem.js +46 -24
- package/lib/module/components/MessageField/MessageField.js +313 -0
- package/lib/module/components/NavArrow/NavArrow.js +59 -18
- package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +322 -0
- package/lib/module/components/Slot/Slot.js +68 -0
- package/lib/module/components/Stepper/Step.js +48 -61
- package/lib/module/components/Stepper/StepLabel.js +40 -10
- package/lib/module/components/Stepper/Stepper.js +15 -17
- package/lib/module/components/SuggestiveSearch/SuggestiveSearch.js +481 -0
- package/lib/module/components/TextInput/TextInput.js +17 -2
- package/lib/module/components/Title/Title.js +10 -2
- package/lib/module/components/index.js +7 -0
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -20
- package/lib/typescript/src/components/Attached/Attached.d.ts +61 -0
- package/lib/typescript/src/components/Card/Card.d.ts +9 -2
- package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +63 -0
- package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +99 -0
- package/lib/typescript/src/components/ListItem/ListItem.d.ts +15 -5
- package/lib/typescript/src/components/MessageField/MessageField.d.ts +81 -0
- package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +10 -5
- package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +64 -0
- package/lib/typescript/src/components/Slot/Slot.d.ts +52 -0
- package/lib/typescript/src/components/Stepper/Step.d.ts +4 -1
- package/lib/typescript/src/components/Stepper/StepLabel.d.ts +4 -1
- package/lib/typescript/src/components/Stepper/Stepper.d.ts +3 -1
- package/lib/typescript/src/components/SuggestiveSearch/SuggestiveSearch.d.ts +123 -0
- package/lib/typescript/src/components/index.d.ts +10 -3
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/Accordion/Accordion.tsx +113 -73
- package/src/components/ActionFooter/ActionFooter.tsx +56 -4
- package/src/components/Attached/Attached.tsx +181 -0
- package/src/components/Card/Card.tsx +28 -1
- package/src/components/Checkbox/Checkbox.tsx +22 -9
- package/src/components/DropdownInput/DropdownInput.tsx +67 -39
- package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +237 -0
- package/src/components/FormField/FormField.tsx +19 -3
- package/src/components/FullscreenModal/FullscreenModal.tsx +414 -0
- package/src/components/ListItem/ListItem.tsx +55 -25
- package/src/components/MessageField/MessageField.tsx +543 -0
- package/src/components/NavArrow/NavArrow.tsx +81 -17
- package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +426 -0
- package/src/components/Slot/Slot.tsx +91 -0
- package/src/components/Stepper/Step.tsx +52 -51
- package/src/components/Stepper/StepLabel.tsx +46 -9
- package/src/components/Stepper/Stepper.tsx +20 -15
- package/src/components/SuggestiveSearch/SuggestiveSearch.tsx +756 -0
- package/src/components/TextInput/TextInput.tsx +14 -1
- package/src/components/Title/Title.tsx +13 -2
- package/src/components/index.ts +10 -3
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/icons/registry.ts +1 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
type LayoutChangeEvent,
|
|
5
|
+
type StyleProp,
|
|
6
|
+
type ViewProps,
|
|
7
|
+
type ViewStyle,
|
|
8
|
+
} from 'react-native'
|
|
9
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
10
|
+
import { cloneChildrenWithModes, EMPTY_MODES } from '../../utils/react-utils'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Anchor point on the main content where the attached `badge` is centered.
|
|
14
|
+
* Mirrors the nine Figma `position` variants (corners, edge midpoints, center).
|
|
15
|
+
*/
|
|
16
|
+
export type AttachedPosition =
|
|
17
|
+
| 'top-left'
|
|
18
|
+
| 'top'
|
|
19
|
+
| 'top-right'
|
|
20
|
+
| 'left'
|
|
21
|
+
| 'center'
|
|
22
|
+
| 'right'
|
|
23
|
+
| 'bottom-left'
|
|
24
|
+
| 'bottom'
|
|
25
|
+
| 'bottom-right'
|
|
26
|
+
|
|
27
|
+
export type AttachedProps = Omit<ViewProps, 'children'> & {
|
|
28
|
+
/**
|
|
29
|
+
* Main content the badge attaches to (the Figma "main slot"). Any node —
|
|
30
|
+
* typically an `IconCapsule`, `Avatar`, image, etc. `modes` are cascaded to
|
|
31
|
+
* every child via {@link cloneChildrenWithModes}.
|
|
32
|
+
*/
|
|
33
|
+
children?: React.ReactNode
|
|
34
|
+
/**
|
|
35
|
+
* The element attached on top of `children` (the Figma "slot"). Centered on
|
|
36
|
+
* the anchor point given by `position` so it straddles the edge/corner.
|
|
37
|
+
* `modes` are cascaded into it as well.
|
|
38
|
+
*/
|
|
39
|
+
badge?: React.ReactNode
|
|
40
|
+
/**
|
|
41
|
+
* Anchor point for the `badge` relative to the main content.
|
|
42
|
+
* @default 'bottom-right'
|
|
43
|
+
*/
|
|
44
|
+
position?: AttachedPosition
|
|
45
|
+
/**
|
|
46
|
+
* How the anchor point is computed for diagonal (corner) positions:
|
|
47
|
+
* - `false` (default): treat the main content as a **square** — corner
|
|
48
|
+
* anchors sit on the bounding-box corners.
|
|
49
|
+
* - `true`: treat the main content as a **circle** inscribed in its bounding
|
|
50
|
+
* box — corner anchors sit on the circle's circumference (the 45° point),
|
|
51
|
+
* so badges hug round content like a circular `IconCapsule` or `Avatar`.
|
|
52
|
+
*
|
|
53
|
+
* Edge (`top`/`bottom`/`left`/`right`) and `center` anchors are unaffected,
|
|
54
|
+
* since the circle meets the bounding box at those points.
|
|
55
|
+
* @default false
|
|
56
|
+
*/
|
|
57
|
+
circular?: boolean
|
|
58
|
+
/** Mode configuration cascaded to the token resolver and all children. */
|
|
59
|
+
modes?: Record<string, any>
|
|
60
|
+
style?: StyleProp<ViewStyle>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type Size = { width: number; height: number }
|
|
64
|
+
|
|
65
|
+
const ZERO_SIZE: Size = { width: 0, height: 0 }
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Fraction (0 | 0.5 | 1) of the main content's width/height at which the badge
|
|
69
|
+
* center should sit, derived from the `position` anchor.
|
|
70
|
+
*/
|
|
71
|
+
function resolveAnchorFractions(position: AttachedPosition): { fx: number; fy: number } {
|
|
72
|
+
const fx = position.includes('left') ? 0 : position.includes('right') ? 1 : 0.5
|
|
73
|
+
const fy = position.startsWith('top') ? 0 : position.startsWith('bottom') ? 1 : 0.5
|
|
74
|
+
return { fx, fy }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Attached — overlays a small `badge` on top of arbitrary main content,
|
|
79
|
+
* centered on one of nine anchor points (corners, edge midpoints, or center).
|
|
80
|
+
*
|
|
81
|
+
* The badge straddles the chosen anchor regardless of either element's size:
|
|
82
|
+
* both the main content and the badge are measured via `onLayout`, then the
|
|
83
|
+
* badge is absolutely positioned so its center lands exactly on the anchor.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```tsx
|
|
87
|
+
* <Attached position="bottom-right" badge={<InstitutionBadge modes={modes} />} modes={modes}>
|
|
88
|
+
* <IconCapsule iconName="ic_card" modes={modes} />
|
|
89
|
+
* </Attached>
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
function Attached({
|
|
93
|
+
children,
|
|
94
|
+
badge,
|
|
95
|
+
position = 'bottom-right',
|
|
96
|
+
circular = true,
|
|
97
|
+
modes: propModes = EMPTY_MODES,
|
|
98
|
+
style,
|
|
99
|
+
...rest
|
|
100
|
+
}: AttachedProps) {
|
|
101
|
+
const { modes: globalModes } = useTokens()
|
|
102
|
+
const modes = useMemo(
|
|
103
|
+
() =>
|
|
104
|
+
globalModes === EMPTY_MODES && propModes === EMPTY_MODES
|
|
105
|
+
? EMPTY_MODES
|
|
106
|
+
: { ...globalModes, ...propModes },
|
|
107
|
+
[globalModes, propModes]
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const [mainSize, setMainSize] = useState<Size>(ZERO_SIZE)
|
|
111
|
+
const [badgeSize, setBadgeSize] = useState<Size>(ZERO_SIZE)
|
|
112
|
+
|
|
113
|
+
const onMainLayout = useCallback((e: LayoutChangeEvent) => {
|
|
114
|
+
const { width, height } = e.nativeEvent.layout
|
|
115
|
+
setMainSize((prev) => (prev.width === width && prev.height === height ? prev : { width, height }))
|
|
116
|
+
}, [])
|
|
117
|
+
|
|
118
|
+
const onBadgeLayout = useCallback((e: LayoutChangeEvent) => {
|
|
119
|
+
const { width, height } = e.nativeEvent.layout
|
|
120
|
+
setBadgeSize((prev) => (prev.width === width && prev.height === height ? prev : { width, height }))
|
|
121
|
+
}, [])
|
|
122
|
+
|
|
123
|
+
const mainChildren = useMemo(
|
|
124
|
+
() => (children != null ? cloneChildrenWithModes(children, modes) : null),
|
|
125
|
+
[children, modes]
|
|
126
|
+
)
|
|
127
|
+
const badgeChildren = useMemo(
|
|
128
|
+
() => (badge != null ? cloneChildrenWithModes(badge, modes) : null),
|
|
129
|
+
[badge, modes]
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const badgePlacement = useMemo<ViewStyle>(() => {
|
|
133
|
+
const { fx, fy } = resolveAnchorFractions(position)
|
|
134
|
+
const measured = mainSize.width > 0 && badgeSize.width > 0
|
|
135
|
+
|
|
136
|
+
let anchorX: number
|
|
137
|
+
let anchorY: number
|
|
138
|
+
if (circular) {
|
|
139
|
+
// Project the anchor onto the circle inscribed in the bounding box, so
|
|
140
|
+
// corner badges land on the circumference (45°) instead of the box corner.
|
|
141
|
+
const cx = mainSize.width / 2
|
|
142
|
+
const cy = mainSize.height / 2
|
|
143
|
+
const radius = Math.min(mainSize.width, mainSize.height) / 2
|
|
144
|
+
const dx = (fx - 0.5) * 2 // -1 | 0 | 1
|
|
145
|
+
const dy = (fy - 0.5) * 2 // -1 | 0 | 1
|
|
146
|
+
const len = Math.hypot(dx, dy) || 1 // 'center' → 0, guard against /0
|
|
147
|
+
anchorX = cx + (dx / len) * radius
|
|
148
|
+
anchorY = cy + (dy / len) * radius
|
|
149
|
+
} else {
|
|
150
|
+
anchorX = mainSize.width * fx
|
|
151
|
+
anchorY = mainSize.height * fy
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
position: 'absolute',
|
|
156
|
+
left: anchorX - badgeSize.width / 2,
|
|
157
|
+
top: anchorY - badgeSize.height / 2,
|
|
158
|
+
// Hide until both elements are measured to avoid a one-frame flash at (0,0).
|
|
159
|
+
opacity: measured ? 1 : 0,
|
|
160
|
+
}
|
|
161
|
+
}, [position, circular, mainSize, badgeSize])
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<View style={[styles.container, style]} {...rest}>
|
|
165
|
+
<View onLayout={onMainLayout}>{mainChildren}</View>
|
|
166
|
+
{badgeChildren != null && (
|
|
167
|
+
<View style={badgePlacement} onLayout={onBadgeLayout} pointerEvents="box-none">
|
|
168
|
+
{badgeChildren}
|
|
169
|
+
</View>
|
|
170
|
+
)}
|
|
171
|
+
</View>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const styles = {
|
|
176
|
+
// alignSelf flex-start so the wrapper hugs the main content; anchors are then
|
|
177
|
+
// computed relative to the content size rather than a stretched parent.
|
|
178
|
+
container: { position: 'relative', alignSelf: 'flex-start' } as ViewStyle,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export default React.memo(Attached)
|
|
@@ -11,6 +11,11 @@ import { EMPTY_MODES } from '../../utils/react-utils';
|
|
|
11
11
|
const CardContext = createContext<{ modes?: Record<string, any> }>({});
|
|
12
12
|
|
|
13
13
|
export interface CardProps {
|
|
14
|
+
/**
|
|
15
|
+
* Content rendered in the header slot at the top of the card (e.g. a brand logo).
|
|
16
|
+
* Sits above the media slot, with its own padding.
|
|
17
|
+
*/
|
|
18
|
+
header?: React.ReactNode;
|
|
14
19
|
/**
|
|
15
20
|
* The content to be rendered in the media slot (e.g. an Image).
|
|
16
21
|
* This content is wrapped in a container that respects the `aspectRatio`.
|
|
@@ -39,9 +44,11 @@ export interface CardProps {
|
|
|
39
44
|
* Card component implementation from Figma node 765:6186.
|
|
40
45
|
*
|
|
41
46
|
* Supports a `media` slot (with aspect ratio) and a content area.
|
|
47
|
+
* Supports an optional `header` slot (e.g. a brand logo), a `media` slot
|
|
48
|
+
* (with aspect ratio) and a content area.
|
|
42
49
|
* Usage:
|
|
43
50
|
* ```tsx
|
|
44
|
-
* <Card media={<Image source={...} />} modes={modes}>
|
|
51
|
+
* <Card header={<GoldLogo />} media={<Image source={...} />} modes={modes}>
|
|
45
52
|
* <Card.SupportText>Support text</Card.SupportText>
|
|
46
53
|
* <Card.Title>Title</Card.Title>
|
|
47
54
|
* <Card.SupportText>Support text</Card.SupportText>
|
|
@@ -49,6 +56,7 @@ export interface CardProps {
|
|
|
49
56
|
* ```
|
|
50
57
|
*/
|
|
51
58
|
export function Card({
|
|
59
|
+
header,
|
|
52
60
|
media,
|
|
53
61
|
children,
|
|
54
62
|
modes = EMPTY_MODES,
|
|
@@ -74,6 +82,11 @@ export function Card({
|
|
|
74
82
|
? cloneElement(media as any, { modes: { ...(media.props as any).modes, ...modes } })
|
|
75
83
|
: media;
|
|
76
84
|
|
|
85
|
+
// Clone header to pass modes if it's a valid element
|
|
86
|
+
const headerWithModes = isValidElement(header)
|
|
87
|
+
? cloneElement(header as any, { modes: { ...(header.props as any).modes, ...modes } })
|
|
88
|
+
: header;
|
|
89
|
+
|
|
77
90
|
const containerStyle: ViewStyle = {
|
|
78
91
|
backgroundColor,
|
|
79
92
|
borderColor,
|
|
@@ -85,6 +98,15 @@ export function Card({
|
|
|
85
98
|
overflow: 'hidden', // Ensure border radius clips content
|
|
86
99
|
};
|
|
87
100
|
|
|
101
|
+
// Header wrap uses fixed padding from Figma (no dedicated tokens defined).
|
|
102
|
+
const headerWrapperStyle: ViewStyle = {
|
|
103
|
+
width: '100%',
|
|
104
|
+
flexDirection: 'row',
|
|
105
|
+
alignItems: 'flex-start',
|
|
106
|
+
paddingHorizontal: 12,
|
|
107
|
+
paddingVertical: 16,
|
|
108
|
+
};
|
|
109
|
+
|
|
88
110
|
const mediaWrapperStyle: ViewStyle = {
|
|
89
111
|
width: '100%',
|
|
90
112
|
aspectRatio: mediaAspectRatio,
|
|
@@ -104,6 +126,11 @@ export function Card({
|
|
|
104
126
|
return (
|
|
105
127
|
<CardContext.Provider value={{ modes }}>
|
|
106
128
|
<View style={[containerStyle, style]}>
|
|
129
|
+
{header && (
|
|
130
|
+
<View style={headerWrapperStyle}>
|
|
131
|
+
{headerWithModes}
|
|
132
|
+
</View>
|
|
133
|
+
)}
|
|
107
134
|
{media && (
|
|
108
135
|
<View style={mediaWrapperStyle}>
|
|
109
136
|
{mediaWithModes}
|
|
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
|
2
2
|
import {
|
|
3
3
|
Pressable,
|
|
4
4
|
Platform,
|
|
5
|
+
View,
|
|
5
6
|
type StyleProp,
|
|
6
7
|
type ViewStyle,
|
|
7
8
|
} from 'react-native'
|
|
@@ -50,6 +51,16 @@ function useFocusVisible() {
|
|
|
50
51
|
return { isFocusVisible, focusHandlers: { onFocus, onBlur } }
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
/** Minimum touch target per iOS HIG / Material accessibility guidance. */
|
|
55
|
+
const MIN_TOUCH_TARGET = 44
|
|
56
|
+
|
|
57
|
+
const touchTargetStyle: ViewStyle = {
|
|
58
|
+
minWidth: MIN_TOUCH_TARGET,
|
|
59
|
+
minHeight: MIN_TOUCH_TARGET,
|
|
60
|
+
alignItems: 'center',
|
|
61
|
+
justifyContent: 'center',
|
|
62
|
+
}
|
|
63
|
+
|
|
53
64
|
export interface CheckboxProps {
|
|
54
65
|
/** Whether the checkbox is checked (controlled) */
|
|
55
66
|
checked?: boolean
|
|
@@ -207,7 +218,7 @@ function Checkbox({
|
|
|
207
218
|
|
|
208
219
|
return (
|
|
209
220
|
<Pressable
|
|
210
|
-
style={[
|
|
221
|
+
style={[touchTargetStyle, style]}
|
|
211
222
|
onPress={handlePress}
|
|
212
223
|
disabled={disabled}
|
|
213
224
|
onHoverIn={() => setIsHovered(true)}
|
|
@@ -217,14 +228,16 @@ function Checkbox({
|
|
|
217
228
|
accessibilityState={{ checked: isChecked, disabled }}
|
|
218
229
|
accessibilityLabel={accessibilityLabel}
|
|
219
230
|
>
|
|
220
|
-
{
|
|
221
|
-
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
231
|
+
<View style={resolveStyle()}>
|
|
232
|
+
{isChecked && (
|
|
233
|
+
<Svg width={12} height={9} viewBox="0 0 12 9" fill="none">
|
|
234
|
+
<Path
|
|
235
|
+
d="M4.00091 8.66939C3.91321 8.6699 3.82628 8.65309 3.74509 8.61991C3.6639 8.58673 3.59006 8.53785 3.52779 8.47606L0.195972 5.14273C0.0704931 5.01719 -1.86978e-09 4.84693 0 4.66939C1.86978e-09 4.49186 0.0704931 4.3216 0.195972 4.19606C0.321451 4.07053 0.491636 4 0.66909 4C0.846544 4 1.01673 4.07053 1.14221 4.19606L4.00091 7.06273L10.8578 0.196061C10.9833 0.0705253 11.1535 0 11.3309 0C11.5084 0 11.6785 0.0705253 11.804 0.196061C11.9295 0.321597 12 0.49186 12 0.669394C12 0.846929 11.9295 1.01719 11.804 1.14273L4.47403 8.47606C4.41176 8.53785 4.33792 8.58673 4.25673 8.61991C4.17554 8.65309 4.08861 8.6699 4.00091 8.66939Z"
|
|
236
|
+
fill={markColor}
|
|
237
|
+
/>
|
|
238
|
+
</Svg>
|
|
239
|
+
)}
|
|
240
|
+
</View>
|
|
228
241
|
</Pressable>
|
|
229
242
|
)
|
|
230
243
|
}
|
|
@@ -162,51 +162,60 @@ function useChevronTokens(modes: Record<string, any>) {
|
|
|
162
162
|
}, [modes])
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
function toNumber(value: unknown, fallback: number): number {
|
|
166
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
167
|
+
if (typeof value === 'string') {
|
|
168
|
+
const parsed = parseFloat(value)
|
|
169
|
+
if (Number.isFinite(parsed)) return parsed
|
|
170
|
+
}
|
|
171
|
+
return fallback
|
|
172
|
+
}
|
|
173
|
+
|
|
165
174
|
function useFormFieldTokens(modes: Record<string, any>) {
|
|
166
175
|
return useMemo(() => {
|
|
167
176
|
const labelColor =
|
|
168
177
|
(getVariableByName('formField/label/color', modes) as string) ||
|
|
169
|
-
'#
|
|
178
|
+
'#000000'
|
|
170
179
|
const labelFontFamily =
|
|
171
180
|
(getVariableByName('formField/label/fontFamily', modes) as string) ||
|
|
172
181
|
'JioType Var'
|
|
173
|
-
const labelFontSize =
|
|
174
|
-
|
|
182
|
+
const labelFontSize = toNumber(
|
|
183
|
+
getVariableByName('formField/label/fontSize', modes),
|
|
175
184
|
14
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
185
|
+
)
|
|
186
|
+
const labelLineHeight = toNumber(
|
|
187
|
+
getVariableByName('formField/label/lineHeight', modes),
|
|
188
|
+
17
|
|
189
|
+
)
|
|
181
190
|
const labelFontWeight =
|
|
182
191
|
(getVariableByName('formField/label/fontWeight', modes) as string) ||
|
|
183
192
|
'500'
|
|
184
193
|
|
|
185
|
-
const gap =
|
|
186
|
-
|
|
187
|
-
const inputPaddingH =
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
parseInt(getVariableByName('formField/input/gap', modes), 10) || 8
|
|
194
|
-
const inputRadius =
|
|
195
|
-
parseInt(getVariableByName('formField/input/radius', modes), 10) ||
|
|
194
|
+
const gap = toNumber(getVariableByName('formField/gap', modes), 8)
|
|
195
|
+
|
|
196
|
+
const inputPaddingH = toNumber(
|
|
197
|
+
getVariableByName('formField/input/padding/horizontal', modes),
|
|
198
|
+
12
|
|
199
|
+
)
|
|
200
|
+
const inputGap = toNumber(
|
|
201
|
+
getVariableByName('formField/input/gap', modes),
|
|
196
202
|
8
|
|
203
|
+
)
|
|
204
|
+
const inputRadius = toNumber(
|
|
205
|
+
getVariableByName('formField/input/radius', modes),
|
|
206
|
+
8
|
|
207
|
+
)
|
|
197
208
|
const inputBackground =
|
|
198
209
|
(getVariableByName('formField/input/background', modes) as string) ||
|
|
199
210
|
'#ffffff'
|
|
200
|
-
const inputFontSize =
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
10
|
|
209
|
-
) || 45
|
|
211
|
+
const inputFontSize = toNumber(
|
|
212
|
+
getVariableByName('formField/input/label/fontSize', modes),
|
|
213
|
+
16
|
|
214
|
+
)
|
|
215
|
+
const inputLineHeight = toNumber(
|
|
216
|
+
getVariableByName('formField/input/label/lineHeight', modes),
|
|
217
|
+
45
|
|
218
|
+
)
|
|
210
219
|
const inputFontFamily =
|
|
211
220
|
(getVariableByName(
|
|
212
221
|
'formField/input/label/fontFamily',
|
|
@@ -231,11 +240,13 @@ function useFormFieldTokens(modes: Record<string, any>) {
|
|
|
231
240
|
) as string) ||
|
|
232
241
|
(getVariableByName('formField/input/border/color', modes) as string) ||
|
|
233
242
|
'#b5b6b7'
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
)
|
|
243
|
+
// Figma spec: 1.5px. Using parseFloat (via toNumber) preserves the
|
|
244
|
+
// fractional value — parseInt was truncating it to 1, leaving the
|
|
245
|
+
// resolved row height ~1px shorter than the Figma reference.
|
|
246
|
+
const inputBorderSize = toNumber(
|
|
247
|
+
getVariableByName('formField/input/border/size', modes),
|
|
248
|
+
1.5
|
|
249
|
+
)
|
|
239
250
|
|
|
240
251
|
return {
|
|
241
252
|
labelColor,
|
|
@@ -314,7 +325,7 @@ function DropdownInput({
|
|
|
314
325
|
supportText,
|
|
315
326
|
errorMessage,
|
|
316
327
|
menuMaxHeight = 240,
|
|
317
|
-
menuOffset =
|
|
328
|
+
menuOffset = 6,
|
|
318
329
|
matchTriggerWidth = true,
|
|
319
330
|
closeOnBackdropPress = true,
|
|
320
331
|
modes: propModes = EMPTY_MODES,
|
|
@@ -594,19 +605,23 @@ function DropdownInput({
|
|
|
594
605
|
}
|
|
595
606
|
|
|
596
607
|
// Focus ring uses the resolved input border color from FormField States so
|
|
597
|
-
// active/error look consistent with TextInput-based FormField.
|
|
598
|
-
//
|
|
608
|
+
// active/error look consistent with TextInput-based FormField. Only the
|
|
609
|
+
// color changes between states — width stays constant to avoid layout
|
|
610
|
+
// shift when opening the menu (a shift would invalidate the measured
|
|
611
|
+
// trigger rect and visually shove the popup).
|
|
599
612
|
const inputRowStyle: ViewStyle = {
|
|
600
613
|
flexDirection: 'row',
|
|
601
614
|
alignItems: 'center',
|
|
602
615
|
backgroundColor: tokens.inputBackground,
|
|
603
616
|
borderColor: tokens.inputBorderColor,
|
|
604
|
-
borderWidth:
|
|
617
|
+
borderWidth: tokens.inputBorderSize,
|
|
618
|
+
borderStyle: 'solid',
|
|
605
619
|
borderRadius: tokens.inputRadius,
|
|
606
620
|
paddingHorizontal: tokens.inputPaddingH,
|
|
607
621
|
paddingVertical: 0,
|
|
608
622
|
gap: tokens.inputGap,
|
|
609
623
|
minHeight: tokens.inputLineHeight,
|
|
624
|
+
width: '100%',
|
|
610
625
|
}
|
|
611
626
|
|
|
612
627
|
const valueTextStyle: TextStyle = {
|
|
@@ -763,12 +778,25 @@ function DropdownInput({
|
|
|
763
778
|
/>
|
|
764
779
|
)}
|
|
765
780
|
|
|
781
|
+
{/*
|
|
782
|
+
IMPORTANT: do NOT pass `statusBarTranslucent` to this Modal.
|
|
783
|
+
On Android, a `statusBarTranslucent` Modal opens its own window
|
|
784
|
+
that spans the entire screen (origin at screen-top, including
|
|
785
|
+
the status bar), but `measureInWindow` on the trigger returns
|
|
786
|
+
coordinates relative to the *activity* window — which on a
|
|
787
|
+
default Android setup starts BELOW the status bar. The two
|
|
788
|
+
coordinate spaces then differ by `StatusBar.currentHeight`, so
|
|
789
|
+
`triggerRect.y + triggerRect.height + menuOffset` lands roughly
|
|
790
|
+
one status-bar-height ABOVE the visible input, making the
|
|
791
|
+
popup overlap the input row. Leaving `statusBarTranslucent`
|
|
792
|
+
off keeps the Modal's window aligned with the activity
|
|
793
|
+
window, which is what every measurement here assumes.
|
|
794
|
+
*/}
|
|
766
795
|
<Modal
|
|
767
796
|
visible={isOpen}
|
|
768
797
|
transparent
|
|
769
798
|
animationType="fade"
|
|
770
799
|
onRequestClose={closeMenu}
|
|
771
|
-
statusBarTranslucent
|
|
772
800
|
>
|
|
773
801
|
<Pressable
|
|
774
802
|
style={StyleSheet.absoluteFill}
|