jfs-components 0.1.30 → 0.1.33
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 +29 -0
- package/D2C.md +118 -0
- package/lib/commonjs/components/CategoryCard/CategoryCard.js +264 -0
- package/lib/commonjs/components/CompareTable/CompareTable.js +140 -84
- package/lib/commonjs/components/ContentSheet/ContentSheet.js +28 -5
- package/lib/commonjs/components/CounterBadge/CounterBadge.js +78 -0
- package/lib/commonjs/components/FavoriteToggle/FavoriteToggle.js +180 -0
- package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +287 -0
- package/lib/commonjs/components/ValueBackMetric/ValueBackMetric.js +219 -0
- package/lib/commonjs/components/index.js +35 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +40095 -1
- package/lib/commonjs/design-tokens/figma-modes.generated.js +3 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/CategoryCard/CategoryCard.js +258 -0
- package/lib/module/components/CompareTable/CompareTable.js +140 -84
- package/lib/module/components/ContentSheet/ContentSheet.js +29 -6
- package/lib/module/components/CounterBadge/CounterBadge.js +73 -0
- package/lib/module/components/FavoriteToggle/FavoriteToggle.js +174 -0
- package/lib/module/components/HelloJioInput/HelloJioInput.js +281 -0
- package/lib/module/components/ValueBackMetric/ValueBackMetric.js +214 -0
- package/lib/module/components/index.js +6 -1
- package/lib/module/design-tokens/Coin Variables-variables-full.json +40095 -1
- package/lib/module/design-tokens/figma-modes.generated.js +3 -0
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/CategoryCard/CategoryCard.d.ts +72 -0
- package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +16 -1
- package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +7 -1
- package/lib/typescript/src/components/CounterBadge/CounterBadge.d.ts +19 -0
- package/lib/typescript/src/components/FavoriteToggle/FavoriteToggle.d.ts +66 -0
- package/lib/typescript/src/components/HelloJioInput/HelloJioInput.d.ts +111 -0
- package/lib/typescript/src/components/ValueBackMetric/ValueBackMetric.d.ts +86 -0
- package/lib/typescript/src/components/index.d.ts +5 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +7 -1
- package/scripts/extract-figma-modes.js +359 -0
- package/src/components/CategoryCard/CategoryCard.tsx +404 -0
- package/src/components/CompareTable/CompareTable.tsx +201 -101
- package/src/components/ContentSheet/ContentSheet.tsx +40 -11
- package/src/components/CounterBadge/CounterBadge.tsx +95 -0
- package/src/components/FavoriteToggle/FavoriteToggle.tsx +243 -0
- package/src/components/HelloJioInput/HelloJioInput.tsx +513 -0
- package/src/components/ValueBackMetric/ValueBackMetric.tsx +298 -0
- package/src/components/index.ts +5 -0
- package/src/design-tokens/Coin Variables-variables-full.json +40095 -1
- package/src/design-tokens/figma-modes.generated.ts +3 -0
- package/src/icons/registry.ts +1 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { View, Text } from 'react-native';
|
|
5
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
|
+
import { EMPTY_MODES } from '../../utils/react-utils';
|
|
7
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
8
|
+
/**
|
|
9
|
+
* Counter Badge — a small token-driven pill that displays a value.
|
|
10
|
+
*
|
|
11
|
+
* All visual attributes resolve from the Figma `counterBadge/*` tokens via
|
|
12
|
+
* `getVariableByName`.
|
|
13
|
+
*/
|
|
14
|
+
function CounterBadge({
|
|
15
|
+
value = '99',
|
|
16
|
+
modes = EMPTY_MODES,
|
|
17
|
+
style,
|
|
18
|
+
...rest
|
|
19
|
+
}) {
|
|
20
|
+
// Resolve token values (fall back to the Figma defaults for the single
|
|
21
|
+
// `Default` mode so the badge still renders if a token is missing).
|
|
22
|
+
const backgroundColor = getVariableByName('counterBadge/background', modes) ?? '#ebebec';
|
|
23
|
+
const foreground = getVariableByName('counterBadge/foreground', modes) ?? '#0c0d10';
|
|
24
|
+
const fontFamily = getVariableByName('counterBadge/fontFamily', modes) ?? 'JioType Var';
|
|
25
|
+
const fontWeightRaw = getVariableByName('counterBadge/fontWeight', modes) ?? 500;
|
|
26
|
+
const fontWeight = typeof fontWeightRaw === 'number' ? String(fontWeightRaw) : fontWeightRaw;
|
|
27
|
+
const fontSize = Number(getVariableByName('counterBadge/fontSize', modes) ?? 8);
|
|
28
|
+
const lineHeight = Number(getVariableByName('counterBadge/lineHeight', modes) ?? 8);
|
|
29
|
+
const paddingHorizontal = Number(getVariableByName('counterBadge/padding/horizontal', modes) ?? 4);
|
|
30
|
+
// The Figma design pins vertical padding to 4px (matching horizontal), but the
|
|
31
|
+
// committed variables snapshot resolves `counterBadge/padding/vertical` to 5px
|
|
32
|
+
// (a stale export). Figma is the source of truth here, so hardcode 4px rather
|
|
33
|
+
// than trust the token, keeping the badge one pixel shorter to match the design.
|
|
34
|
+
const paddingVertical = 4;
|
|
35
|
+
const borderRadius = Number(getVariableByName('counterBadge/radius', modes) ?? 999);
|
|
36
|
+
const containerStyle = {
|
|
37
|
+
backgroundColor,
|
|
38
|
+
paddingHorizontal,
|
|
39
|
+
paddingVertical,
|
|
40
|
+
borderRadius,
|
|
41
|
+
alignSelf: 'flex-start',
|
|
42
|
+
alignItems: 'center',
|
|
43
|
+
justifyContent: 'center',
|
|
44
|
+
...(style || {})
|
|
45
|
+
};
|
|
46
|
+
const textStyle = {
|
|
47
|
+
color: foreground,
|
|
48
|
+
fontFamily,
|
|
49
|
+
fontWeight: fontWeight,
|
|
50
|
+
fontSize,
|
|
51
|
+
lineHeight,
|
|
52
|
+
textAlign: 'center'
|
|
53
|
+
};
|
|
54
|
+
return /*#__PURE__*/_jsx(View, {
|
|
55
|
+
style: containerStyle,
|
|
56
|
+
...rest,
|
|
57
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
58
|
+
style: WRAP_STYLE,
|
|
59
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
60
|
+
style: textStyle,
|
|
61
|
+
children: value
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Figma `wrap` node (data-node-id 7701:10879): min-width 10, centered.
|
|
68
|
+
const WRAP_STYLE = {
|
|
69
|
+
minWidth: 10,
|
|
70
|
+
alignItems: 'center',
|
|
71
|
+
justifyContent: 'center'
|
|
72
|
+
};
|
|
73
|
+
export default CounterBadge;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
4
|
+
import { Pressable, Platform } from 'react-native';
|
|
5
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
|
+
import Icon from '../../icons/Icon';
|
|
7
|
+
import GlassFill from '../../utils/GlassFill/GlassFill';
|
|
8
|
+
import { usePressableWebSupport } from '../../utils/web-platform-utils';
|
|
9
|
+
import { EMPTY_MODES } from '../../utils/react-utils';
|
|
10
|
+
import Skeleton from '../../skeleton/Skeleton';
|
|
11
|
+
import { useSkeleton } from '../../skeleton/SkeletonGroup';
|
|
12
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
|
+
const IS_WEB = Platform.OS === 'web';
|
|
14
|
+
const IS_IOS = Platform.OS === 'ios';
|
|
15
|
+
const PRESS_DELAY = IS_IOS ? 130 : 0;
|
|
16
|
+
const pressedOverlayStyle = {
|
|
17
|
+
opacity: 0.7
|
|
18
|
+
};
|
|
19
|
+
// The `favoriteToggle/*` tokens are not yet in the committed variables snapshot,
|
|
20
|
+
// so `getVariableByName` returns undefined and these Figma-node values are used
|
|
21
|
+
// as fallbacks. Once the token export is re-synced the resolver takes over.
|
|
22
|
+
function resolveTokens(modes, isActive) {
|
|
23
|
+
const width = Number(getVariableByName('favoriteToggle/width', modes) ?? 29);
|
|
24
|
+
const height = Number(getVariableByName('favoriteToggle/height', modes) ?? 29);
|
|
25
|
+
const radiusRaw = Number(getVariableByName('favoriteToggle/icon/radius', modes) ?? 9999);
|
|
26
|
+
const iconSize = Number(getVariableByName('favoriteToggle/icon/width', modes) ?? 20);
|
|
27
|
+
// 9999 is the design-token sentinel for "perfect circle".
|
|
28
|
+
const size = Math.max(width, height);
|
|
29
|
+
const borderRadius = radiusRaw >= 9999 ? size / 2 : radiusRaw;
|
|
30
|
+
|
|
31
|
+
// Background + icon color differ per variant. In Figma this is driven by the
|
|
32
|
+
// `isActive` collection (modes `'False'` / `'True'`), so we pass that mode
|
|
33
|
+
// through by its string name. Until the `color/favoriteToggle/*` tokens are
|
|
34
|
+
// exported the resolver returns undefined and the fallbacks below — which
|
|
35
|
+
// encode the two Figma variants directly — take over.
|
|
36
|
+
const isActiveMode = {
|
|
37
|
+
...modes,
|
|
38
|
+
isActive: isActive ? 'True' : 'False'
|
|
39
|
+
};
|
|
40
|
+
const backgroundColor = getVariableByName('color/favoriteToggle/background/color', isActiveMode) ?? (isActive ? '#ffffff' : '#ffffff33');
|
|
41
|
+
const iconColor = getVariableByName('color/favoriteToggle/icon/color', isActiveMode) ?? (isActive ? '#cea15a' : '#ffffff');
|
|
42
|
+
|
|
43
|
+
// Figma `blur/minimal` (px radius) -> GlassFill's 0-100 intensity scale, the
|
|
44
|
+
// same mapping Badge's glass path uses.
|
|
45
|
+
const blurRaw = Number(getVariableByName('blur/minimal', modes) ?? 29);
|
|
46
|
+
const blurIntensity = Math.max(0, Math.min(100, Math.round(blurRaw)));
|
|
47
|
+
return {
|
|
48
|
+
size,
|
|
49
|
+
borderRadius,
|
|
50
|
+
backgroundColor,
|
|
51
|
+
iconColor,
|
|
52
|
+
iconSize,
|
|
53
|
+
blurIntensity
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Circular glass favorite (heart) toggle, mirroring the Figma `Favorite Toggle`
|
|
59
|
+
* component set's boolean `isActive` variant:
|
|
60
|
+
* - inactive: translucent frosted circle (`#ffffff33` over a `blur/minimal`
|
|
61
|
+
* backdrop) with a white heart.
|
|
62
|
+
* - active: solid white circle with a gold (`#cea15a`) heart.
|
|
63
|
+
* Both variants share the identical filled-heart glyph; only the background and
|
|
64
|
+
* icon colors change between states.
|
|
65
|
+
*
|
|
66
|
+
* Controlled or uncontrolled: pass `isActive` (+ `onChange`) to control it, or
|
|
67
|
+
* omit `isActive` and use `defaultActive` to let the component own the state.
|
|
68
|
+
*
|
|
69
|
+
* Visual attributes resolve from the `favoriteToggle/*` / `color/favoriteToggle/*`
|
|
70
|
+
* tokens via `getVariableByName`. Those tokens are not yet in the committed
|
|
71
|
+
* variables snapshot, so the component currently runs on the Figma-node values
|
|
72
|
+
* as fallbacks; once the tokens are exported (under the `isActive` collection,
|
|
73
|
+
* modes `False`/`True`) the resolver takes over.
|
|
74
|
+
*/
|
|
75
|
+
function FavoriteToggle({
|
|
76
|
+
isActive: controlledActive,
|
|
77
|
+
defaultActive = false,
|
|
78
|
+
icon = 'ic_favorite',
|
|
79
|
+
onChange,
|
|
80
|
+
onPress,
|
|
81
|
+
modes = EMPTY_MODES,
|
|
82
|
+
disabled = false,
|
|
83
|
+
accessibilityLabel,
|
|
84
|
+
accessibilityHint,
|
|
85
|
+
accessibilityState,
|
|
86
|
+
webAccessibilityProps,
|
|
87
|
+
style,
|
|
88
|
+
loading,
|
|
89
|
+
...rest
|
|
90
|
+
}) {
|
|
91
|
+
// Controlled when `isActive` is supplied; otherwise own the state internally
|
|
92
|
+
// (mirrors the Toggle / SegmentedControl controlled-or-uncontrolled pattern).
|
|
93
|
+
const isControlled = controlledActive !== undefined;
|
|
94
|
+
const [internalActive, setInternalActive] = useState(defaultActive);
|
|
95
|
+
const isActive = isControlled ? controlledActive : internalActive;
|
|
96
|
+
const tokens = useMemo(() => resolveTokens(modes, isActive), [modes, isActive]);
|
|
97
|
+
const {
|
|
98
|
+
active: groupActive
|
|
99
|
+
} = useSkeleton();
|
|
100
|
+
const isLoading = loading ?? groupActive;
|
|
101
|
+
const handlePress = useCallback(() => {
|
|
102
|
+
const next = !isActive;
|
|
103
|
+
if (!isControlled) {
|
|
104
|
+
setInternalActive(next);
|
|
105
|
+
}
|
|
106
|
+
onChange?.(next);
|
|
107
|
+
onPress?.();
|
|
108
|
+
}, [isActive, isControlled, onChange, onPress]);
|
|
109
|
+
const label = accessibilityLabel || 'Favorite';
|
|
110
|
+
const webProps = usePressableWebSupport({
|
|
111
|
+
restProps: rest,
|
|
112
|
+
onPress: disabled ? undefined : handlePress,
|
|
113
|
+
disabled,
|
|
114
|
+
accessibilityLabel: label,
|
|
115
|
+
webAccessibilityProps
|
|
116
|
+
});
|
|
117
|
+
const baseContainerStyle = {
|
|
118
|
+
width: tokens.size,
|
|
119
|
+
height: tokens.size,
|
|
120
|
+
borderRadius: tokens.borderRadius,
|
|
121
|
+
// Active is opaque white, so no glass is drawn and the background paints
|
|
122
|
+
// directly. Inactive keeps the container transparent so the GlassFill blur
|
|
123
|
+
// (clipped by overflow:hidden) shows through.
|
|
124
|
+
backgroundColor: isActive ? tokens.backgroundColor : 'transparent',
|
|
125
|
+
overflow: 'hidden',
|
|
126
|
+
alignItems: 'center',
|
|
127
|
+
justifyContent: 'center',
|
|
128
|
+
opacity: disabled ? 0.5 : 1
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Declared before the loading short-circuit so hook order stays stable.
|
|
132
|
+
const styleCallback = useCallback(({
|
|
133
|
+
pressed
|
|
134
|
+
}) => [baseContainerStyle, style, pressed && !disabled ? pressedOverlayStyle : null], [baseContainerStyle, style, disabled]);
|
|
135
|
+
if (isLoading) {
|
|
136
|
+
return /*#__PURE__*/_jsx(Skeleton, {
|
|
137
|
+
kind: "other",
|
|
138
|
+
width: tokens.size,
|
|
139
|
+
height: tokens.size,
|
|
140
|
+
style: [{
|
|
141
|
+
borderRadius: tokens.borderRadius
|
|
142
|
+
}, style],
|
|
143
|
+
modes: modes
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return /*#__PURE__*/_jsxs(Pressable, {
|
|
147
|
+
accessibilityRole: IS_WEB ? 'switch' : 'button',
|
|
148
|
+
accessibilityLabel: undefined,
|
|
149
|
+
accessibilityHint: accessibilityHint,
|
|
150
|
+
accessibilityState: {
|
|
151
|
+
disabled,
|
|
152
|
+
checked: isActive,
|
|
153
|
+
...accessibilityState
|
|
154
|
+
},
|
|
155
|
+
onPress: disabled ? undefined : handlePress,
|
|
156
|
+
disabled: disabled,
|
|
157
|
+
unstable_pressDelay: PRESS_DELAY,
|
|
158
|
+
style: styleCallback,
|
|
159
|
+
...webProps,
|
|
160
|
+
children: [!isActive ? /*#__PURE__*/_jsx(GlassFill, {
|
|
161
|
+
tint: "light",
|
|
162
|
+
intensity: tokens.blurIntensity,
|
|
163
|
+
overlayColor: tokens.backgroundColor,
|
|
164
|
+
androidTintWash: false
|
|
165
|
+
}) : null, /*#__PURE__*/_jsx(Icon, {
|
|
166
|
+
name: icon,
|
|
167
|
+
size: tokens.iconSize,
|
|
168
|
+
color: tokens.iconColor,
|
|
169
|
+
accessibilityElementsHidden: true,
|
|
170
|
+
importantForAccessibility: "no"
|
|
171
|
+
})]
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
export default /*#__PURE__*/React.memo(FavoriteToggle);
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React, { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { Platform, Pressable, View, TextInput as RNTextInput } from 'react-native';
|
|
5
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider';
|
|
7
|
+
import Icon from '../../icons/Icon';
|
|
8
|
+
import { EMPTY_MODES, cloneChildrenWithModes, mergeRefs } from '../../utils/react-utils';
|
|
9
|
+
import GlassFill from '../../utils/GlassFill/GlassFill';
|
|
10
|
+
import IconButton from '../IconButton/IconButton';
|
|
11
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
12
|
+
const IS_WEB = Platform.OS === 'web';
|
|
13
|
+
const STATE_COLLECTION = 'HelloJioInput State';
|
|
14
|
+
|
|
15
|
+
/** Default IconButton modes matching the Figma send control. */
|
|
16
|
+
const DEFAULT_SEND_BUTTON_MODES = Object.freeze({
|
|
17
|
+
AppearanceBrand: 'Primary',
|
|
18
|
+
'Button / Size': 'S'
|
|
19
|
+
});
|
|
20
|
+
const toNumber = (value, fallback) => {
|
|
21
|
+
if (typeof value === 'number') {
|
|
22
|
+
return Number.isFinite(value) ? value : fallback;
|
|
23
|
+
}
|
|
24
|
+
if (typeof value === 'string') {
|
|
25
|
+
const parsed = Number(value);
|
|
26
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
27
|
+
}
|
|
28
|
+
return fallback;
|
|
29
|
+
};
|
|
30
|
+
const toFontWeight = (value, fallback) => {
|
|
31
|
+
if (typeof value === 'number') return String(value);
|
|
32
|
+
if (typeof value === 'string') return value;
|
|
33
|
+
return fallback;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Fallback values from the Figma `HelloJioInput` component when local token
|
|
37
|
+
* JSON has not been synced yet. Mode-driven `getVariableByName` wins when
|
|
38
|
+
* tokens resolve.
|
|
39
|
+
*/
|
|
40
|
+
const STATE_FALLBACKS = {
|
|
41
|
+
Idle: {
|
|
42
|
+
background: '#f5f5f5',
|
|
43
|
+
borderColor: '#f5f5f5',
|
|
44
|
+
labelColor: '#545961',
|
|
45
|
+
iconColor: '#545961'
|
|
46
|
+
},
|
|
47
|
+
Active: {
|
|
48
|
+
background: '#ffffff',
|
|
49
|
+
borderColor: '#b5b5b5',
|
|
50
|
+
labelColor: '#24262b',
|
|
51
|
+
iconColor: '#24262b'
|
|
52
|
+
},
|
|
53
|
+
IdleJioPlus: {
|
|
54
|
+
background: '#f5f5f5',
|
|
55
|
+
borderColor: '#f5f5f5',
|
|
56
|
+
labelColor: '#545961',
|
|
57
|
+
iconColor: '#545961'
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
function resolveHelloJioInputTokens(modes, visualState) {
|
|
61
|
+
const fallback = STATE_FALLBACKS[visualState];
|
|
62
|
+
const background = getVariableByName('helloJioInput/background', modes) ?? fallback.background;
|
|
63
|
+
const borderColor = getVariableByName('helloJioInput/border/color', modes) ?? fallback.borderColor;
|
|
64
|
+
const strokeWidth = toNumber(getVariableByName('helloJioInput/strokeWidth', modes), 1);
|
|
65
|
+
const gap = toNumber(getVariableByName('helloJioInput/gap', modes), 8);
|
|
66
|
+
const paddingLeft = toNumber(getVariableByName('helloJioInput/padding/left', modes), 12);
|
|
67
|
+
const paddingRight = toNumber(getVariableByName('helloJioInput/padding/right', modes), 4);
|
|
68
|
+
const paddingTop = toNumber(getVariableByName('helloJioInput/padding/top', modes), 4);
|
|
69
|
+
const paddingBottom = toNumber(getVariableByName('helloJioInput/padding/bottom', modes), 4);
|
|
70
|
+
const height = toNumber(getVariableByName('helloJioInput/height', modes), 36);
|
|
71
|
+
const radiusRaw = toNumber(getVariableByName('helloJioInput/radius', modes), 99999);
|
|
72
|
+
const borderRadius = radiusRaw >= 9999 ? 99999 : radiusRaw;
|
|
73
|
+
const labelColor = getVariableByName('helloJioInput/label/color', modes) ?? fallback.labelColor;
|
|
74
|
+
const fontSize = toNumber(getVariableByName('helloJioInput/fontSize', modes), 14);
|
|
75
|
+
const lineHeight = toNumber(getVariableByName('helloJioInput/lineHeight', modes), 18);
|
|
76
|
+
const fontFamily = getVariableByName('helloJioInput/fontFamily', modes) ?? 'JioType Var';
|
|
77
|
+
const fontWeight = toFontWeight(getVariableByName('helloJioInput/fontWeight', modes), '400');
|
|
78
|
+
const iconColor = getVariableByName('helloJioInput/icon/color', modes) ?? fallback.iconColor;
|
|
79
|
+
const iconSize = toNumber(getVariableByName('textInput/icon/size', modes) ?? getVariableByName('helloJioInput/icon/size', modes), 18);
|
|
80
|
+
const blurMinimal = toNumber(getVariableByName('blur/minimal', modes), 29);
|
|
81
|
+
const blurIntensity = Math.max(0, Math.min(100, Math.round(blurMinimal)));
|
|
82
|
+
const useGlass = visualState === 'IdleJioPlus';
|
|
83
|
+
return {
|
|
84
|
+
containerStyle: {
|
|
85
|
+
position: 'relative',
|
|
86
|
+
flexDirection: 'row',
|
|
87
|
+
alignItems: 'center',
|
|
88
|
+
// Glass keeps the container transparent so GlassFill shows through.
|
|
89
|
+
backgroundColor: useGlass ? 'transparent' : background,
|
|
90
|
+
borderColor,
|
|
91
|
+
borderWidth: strokeWidth,
|
|
92
|
+
borderStyle: 'solid',
|
|
93
|
+
borderRadius,
|
|
94
|
+
paddingLeft,
|
|
95
|
+
paddingRight,
|
|
96
|
+
paddingTop,
|
|
97
|
+
paddingBottom,
|
|
98
|
+
gap,
|
|
99
|
+
height,
|
|
100
|
+
overflow: 'hidden',
|
|
101
|
+
width: '100%'
|
|
102
|
+
},
|
|
103
|
+
inputStyle: {
|
|
104
|
+
flex: 1,
|
|
105
|
+
color: labelColor,
|
|
106
|
+
fontSize,
|
|
107
|
+
fontFamily,
|
|
108
|
+
fontWeight,
|
|
109
|
+
// iOS single-line TextInput + explicit lineHeight sits below center —
|
|
110
|
+
// same quirk TextInput works around. Omit on iOS.
|
|
111
|
+
...(Platform.OS === 'ios' ? null : {
|
|
112
|
+
lineHeight
|
|
113
|
+
}),
|
|
114
|
+
padding: 0,
|
|
115
|
+
margin: 0,
|
|
116
|
+
minHeight: lineHeight,
|
|
117
|
+
outlineStyle: 'none',
|
|
118
|
+
outlineWidth: 0,
|
|
119
|
+
outlineColor: 'transparent'
|
|
120
|
+
},
|
|
121
|
+
iconColor,
|
|
122
|
+
iconSize,
|
|
123
|
+
blurIntensity,
|
|
124
|
+
useGlass
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* `HelloJioInput` is the pill-shaped HelloJio prompt field: leading brand
|
|
130
|
+
* icon, single-line text input, and a gold send `IconButton` on the right.
|
|
131
|
+
*
|
|
132
|
+
* Visual state is derived automatically:
|
|
133
|
+
* - **Idle** — gray fill, gray label (default)
|
|
134
|
+
* - **Active** — white fill + border while focused
|
|
135
|
+
* - **IdleJioPlus** — frosted glass when `jioPlus` and unfocused
|
|
136
|
+
*
|
|
137
|
+
* @component
|
|
138
|
+
*/
|
|
139
|
+
const HelloJioInput = /*#__PURE__*/forwardRef(function HelloJioInput({
|
|
140
|
+
placeholder = 'Ask me anything',
|
|
141
|
+
value,
|
|
142
|
+
defaultValue = '',
|
|
143
|
+
onChangeText,
|
|
144
|
+
onSubmit,
|
|
145
|
+
leadingIconName = 'ic_hellojio',
|
|
146
|
+
leading,
|
|
147
|
+
trailing,
|
|
148
|
+
sendIconName = 'ic_send_message',
|
|
149
|
+
jioPlus = false,
|
|
150
|
+
disabled = false,
|
|
151
|
+
modes: propModes = EMPTY_MODES,
|
|
152
|
+
style,
|
|
153
|
+
inputStyle,
|
|
154
|
+
accessibilityLabel,
|
|
155
|
+
accessibilityHint,
|
|
156
|
+
testID,
|
|
157
|
+
onFocus,
|
|
158
|
+
onBlur,
|
|
159
|
+
returnKeyType = 'send',
|
|
160
|
+
...rest
|
|
161
|
+
}, ref) {
|
|
162
|
+
const {
|
|
163
|
+
modes: globalModes
|
|
164
|
+
} = useTokens();
|
|
165
|
+
const inputRef = useRef(null);
|
|
166
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
167
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
168
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
|
|
169
|
+
const isControlled = value !== undefined;
|
|
170
|
+
const currentValue = isControlled ? value : uncontrolledValue;
|
|
171
|
+
const visualState = isFocused ? 'Active' : jioPlus ? 'IdleJioPlus' : 'Idle';
|
|
172
|
+
const modes = useMemo(() => ({
|
|
173
|
+
...globalModes,
|
|
174
|
+
...propModes,
|
|
175
|
+
[STATE_COLLECTION]: visualState
|
|
176
|
+
}), [globalModes, propModes, visualState]);
|
|
177
|
+
const sendButtonModes = useMemo(() => ({
|
|
178
|
+
...DEFAULT_SEND_BUTTON_MODES,
|
|
179
|
+
...modes
|
|
180
|
+
}), [modes]);
|
|
181
|
+
const tokens = useMemo(() => resolveHelloJioInputTokens(modes, visualState), [modes, visualState]);
|
|
182
|
+
const handleChangeText = useCallback(text => {
|
|
183
|
+
if (!isControlled) setUncontrolledValue(text);
|
|
184
|
+
onChangeText?.(text);
|
|
185
|
+
}, [isControlled, onChangeText]);
|
|
186
|
+
const handleSubmit = useCallback(() => {
|
|
187
|
+
if (disabled) return;
|
|
188
|
+
onSubmit?.(currentValue);
|
|
189
|
+
}, [disabled, onSubmit, currentValue]);
|
|
190
|
+
const handleFocus = useCallback(e => {
|
|
191
|
+
setIsFocused(true);
|
|
192
|
+
onFocus?.(e);
|
|
193
|
+
}, [onFocus]);
|
|
194
|
+
const handleBlur = useCallback(e => {
|
|
195
|
+
setIsFocused(false);
|
|
196
|
+
onBlur?.(e);
|
|
197
|
+
}, [onBlur]);
|
|
198
|
+
const leadingElement = leading ?? (leadingIconName ? /*#__PURE__*/_jsx(Icon, {
|
|
199
|
+
name: leadingIconName,
|
|
200
|
+
size: tokens.iconSize,
|
|
201
|
+
color: tokens.iconColor
|
|
202
|
+
}) : null);
|
|
203
|
+
const processedLeading = leadingElement ? cloneChildrenWithModes(React.Children.toArray(leadingElement), modes) : null;
|
|
204
|
+
const defaultTrailing = /*#__PURE__*/_jsx(IconButton, {
|
|
205
|
+
iconName: sendIconName,
|
|
206
|
+
modes: sendButtonModes,
|
|
207
|
+
onPress: handleSubmit,
|
|
208
|
+
disabled: disabled,
|
|
209
|
+
accessibilityLabel: "Send"
|
|
210
|
+
});
|
|
211
|
+
const trailingElement = trailing === undefined ? defaultTrailing : trailing === null ? null : trailing;
|
|
212
|
+
const processedTrailing = trailingElement ? cloneChildrenWithModes(React.Children.toArray(trailingElement), modes) : null;
|
|
213
|
+
const a11yLabel = accessibilityLabel ?? (placeholder || 'HelloJio input');
|
|
214
|
+
const hoverStyle = isHovered && !disabled && !isFocused ? {
|
|
215
|
+
opacity: 0.95
|
|
216
|
+
} : {};
|
|
217
|
+
const containerStyleArray = [tokens.containerStyle, hoverStyle, disabled ? {
|
|
218
|
+
opacity: 0.5
|
|
219
|
+
} : null, style];
|
|
220
|
+
const glassOverlay = getVariableByName('helloJioInput/background', modes) ?? STATE_FALLBACKS.IdleJioPlus.background;
|
|
221
|
+
const inner = /*#__PURE__*/_jsxs(_Fragment, {
|
|
222
|
+
children: [tokens.useGlass ? /*#__PURE__*/_jsx(GlassFill, {
|
|
223
|
+
tint: "light",
|
|
224
|
+
intensity: tokens.blurIntensity,
|
|
225
|
+
overlayColor: glassOverlay,
|
|
226
|
+
androidTintWash: false
|
|
227
|
+
}) : null, processedLeading ? /*#__PURE__*/_jsx(View, {
|
|
228
|
+
accessibilityElementsHidden: true,
|
|
229
|
+
importantForAccessibility: "no",
|
|
230
|
+
style: SLOT_CENTER,
|
|
231
|
+
children: processedLeading
|
|
232
|
+
}) : null, /*#__PURE__*/_jsx(RNTextInput, {
|
|
233
|
+
ref: mergeRefs(inputRef, ref),
|
|
234
|
+
accessibilityLabel: a11yLabel,
|
|
235
|
+
accessibilityHint: accessibilityHint,
|
|
236
|
+
placeholder: isFocused ? '' : placeholder,
|
|
237
|
+
placeholderTextColor: tokens.inputStyle.color,
|
|
238
|
+
value: currentValue,
|
|
239
|
+
onChangeText: handleChangeText,
|
|
240
|
+
onFocus: handleFocus,
|
|
241
|
+
onBlur: handleBlur,
|
|
242
|
+
onSubmitEditing: handleSubmit,
|
|
243
|
+
returnKeyType: returnKeyType,
|
|
244
|
+
editable: !disabled,
|
|
245
|
+
style: [tokens.inputStyle, inputStyle],
|
|
246
|
+
testID: testID,
|
|
247
|
+
...rest
|
|
248
|
+
}), processedTrailing ? /*#__PURE__*/_jsx(View
|
|
249
|
+
// Keep the send button in the a11y tree — it has its own label.
|
|
250
|
+
, {
|
|
251
|
+
style: SLOT_CENTER,
|
|
252
|
+
children: processedTrailing
|
|
253
|
+
}) : null]
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Same Android focus rule as TextInput: never wrap native TextInput in
|
|
257
|
+
// Pressable on native (steals the first tap). Web keeps Pressable for
|
|
258
|
+
// hover + click-anywhere-to-focus.
|
|
259
|
+
if (IS_WEB) {
|
|
260
|
+
return /*#__PURE__*/_jsx(Pressable, {
|
|
261
|
+
style: containerStyleArray,
|
|
262
|
+
onHoverIn: () => setIsHovered(true),
|
|
263
|
+
onHoverOut: () => setIsHovered(false),
|
|
264
|
+
onPress: () => {
|
|
265
|
+
if (!disabled) inputRef.current?.focus();
|
|
266
|
+
},
|
|
267
|
+
accessible: false,
|
|
268
|
+
disabled: disabled,
|
|
269
|
+
children: inner
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return /*#__PURE__*/_jsx(View, {
|
|
273
|
+
style: containerStyleArray,
|
|
274
|
+
children: inner
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
const SLOT_CENTER = {
|
|
278
|
+
justifyContent: 'center',
|
|
279
|
+
alignItems: 'center'
|
|
280
|
+
};
|
|
281
|
+
export default /*#__PURE__*/React.memo(HelloJioInput);
|